package post

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
)

type DraftStore interface {

	// Set sets the draft Post's data into the storage, keyed by the draft
	// Post's ID.
	Set(post Post) error

	// Get returns count draft Posts, sorted id descending, offset by the
	// given page number. The returned boolean indicates if there are more pages
	// or not.
	Get(page, count int) ([]Post, bool, error)

	// GetByID will return the draft Post with the given ID, or ErrPostNotFound.
	GetByID(id string) (Post, error)

	// Delete will delete the draft Post with the given ID.
	Delete(id string) error
}

type draftStore struct {
	db *SQLDB
}

// NewDraftStore initializes a new DraftStore using an existing SQLDB.
func NewDraftStore(db *SQLDB) DraftStore {
	return &draftStore{
		db: db,
	}
}

func (s *draftStore) Set(post Post) error {

	if post.ID == "" {
		return errors.New("post ID can't be empty")
	}

	tagsJSON, err := json.Marshal(post.Tags)
	if err != nil {
		return fmt.Errorf("json marshaling tags %#v: %w", post.Tags, err)
	}

	_, err = s.db.db.Exec(
		`INSERT INTO post_drafts (
				id, title, description, tags, series, body
			)
			VALUES
			(?, ?, ?, ?, ?, ?)
			ON CONFLICT (id) DO UPDATE SET
				title=excluded.title,
				description=excluded.description,
				tags=excluded.tags,
				series=excluded.series,
				body=excluded.body`,
		post.ID,
		post.Title,
		&sql.NullString{String: post.Description, Valid: len(post.Description) > 0},
		&sql.NullString{String: string(tagsJSON), Valid: len(post.Tags) > 0},
		&sql.NullString{String: post.Series, Valid: post.Series != ""},
		post.Body,
	)

	if err != nil {
		return fmt.Errorf("inserting into post_drafts: %w", err)
	}

	return nil
}

func (s *draftStore) get(
	querier interface {
		Query(string, ...interface{}) (*sql.Rows, error)
	},
	limit, offset int,
	where string, whereArgs ...interface{},
) (
	[]Post, error,
) {

	query := `
		SELECT
			p.id, p.title, p.description, p.tags, p.series, p.body
		FROM post_drafts p
		` + where + `
		ORDER BY p.id ASC`

	if limit > 0 {
		query += fmt.Sprintf(" LIMIT %d", limit)
	}

	if offset > 0 {
		query += fmt.Sprintf(" OFFSET %d", offset)
	}

	rows, err := querier.Query(query, whereArgs...)

	if err != nil {
		return nil, fmt.Errorf("selecting: %w", err)
	}

	defer rows.Close()

	var posts []Post

	for rows.Next() {

		var (
			post                      Post
			description, tags, series sql.NullString
		)

		err := rows.Scan(
			&post.ID, &post.Title, &description, &tags, &series,
			&post.Body,
		)

		if err != nil {
			return nil, fmt.Errorf("scanning row: %w", err)
		}

		post.Description = description.String
		post.Series = series.String

		if tags.String != "" {

			if err := json.Unmarshal([]byte(tags.String), &post.Tags); err != nil {
				return nil, fmt.Errorf("json parsing %q: %w", tags.String, err)
			}
		}

		posts = append(posts, post)
	}

	return posts, nil
}

func (s *draftStore) Get(page, count int) ([]Post, bool, error) {

	posts, err := s.get(s.db.db, count+1, page*count, ``)

	if err != nil {
		return nil, false, fmt.Errorf("querying post_drafts: %w", err)
	}

	var hasMore bool

	if len(posts) > count {
		hasMore = true
		posts = posts[:count]
	}

	return posts, hasMore, nil
}

func (s *draftStore) GetByID(id string) (Post, error) {

	posts, err := s.get(s.db.db, 0, 0, `WHERE p.id=?`, id)

	if err != nil {
		return Post{}, fmt.Errorf("querying post_drafts: %w", err)
	}

	if len(posts) == 0 {
		return Post{}, ErrPostNotFound
	}

	if len(posts) > 1 {
		panic(fmt.Sprintf("got back multiple draft posts querying id %q: %+v", id, posts))
	}

	return posts[0], nil
}

func (s *draftStore) Delete(id string) error {

	if _, err := s.db.db.Exec(`DELETE FROM post_drafts WHERE id = ?`, id); err != nil {
		return fmt.Errorf("deleting from post_drafts: %w", err)
	}

	return nil
}
