package sqlite

import (
	"database/sql"
	_ "embed"
	"fmt"
	"time"

	"git.arav.su/Arav/dwelling-home/pkg/mindflow"
	"github.com/pkg/errors"
)

var (
	//go:embed queries/schema.sql
	querySchema string

	//go:embed queries/categoryGetAll.sql
	queryCategoryGetAll string
	//go:embed queries/categoryNew.sql
	queryCategoryNew string
	//go:embed queries/categoryEdit.sql
	queryCategoryEdit string
	//go:embed queries/categoryDelete.sql
	queryCategoryDelete string
	//go:embed queries/postGetAll.sql
	queryPostGetAll string
	//go:embed queries/postNew.sql
	queryPostNew string
	//go:embed queries/postEdit.sql
	queryPostEdit string
	//go:embed queries/postDelete.sql
	queryPostDelete string
)

var (
	stmtCategoryGetAll *sql.Stmt
	stmtCategoryNew    *sql.Stmt
	stmtCategoryEdit   *sql.Stmt
	stmtCategoryDelete *sql.Stmt
	stmtPostGetAll     *sql.Stmt
	stmtPostNew        *sql.Stmt
	stmtPostEdit       *sql.Stmt
	stmtPostDelete     *sql.Stmt
)

func initDBStatements(db *sql.DB) error {
	db.Exec("PRAGMA foreign_keys = ON;")

	_, err := db.Exec(querySchema)
	if err != nil {
		return errors.Wrap(err, "failed to init schema")
	}

	stmtCategoryGetAll, err = db.Prepare(queryCategoryGetAll)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryCategoryGetAll")
	}

	stmtCategoryNew, err = db.Prepare(queryCategoryNew)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryCategoryNew")
	}

	stmtCategoryEdit, err = db.Prepare(queryCategoryEdit)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryCategoryEdit")
	}

	stmtCategoryDelete, err = db.Prepare(queryCategoryDelete)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryCategoryDelete")
	}

	stmtPostGetAll, err = db.Prepare(queryPostGetAll)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryPostGetAll")
	}

	stmtPostNew, err = db.Prepare(queryPostNew)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryPostNew")
	}

	stmtPostEdit, err = db.Prepare(queryPostEdit)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryPostEdit")
	}

	stmtPostDelete, err = db.Prepare(queryPostDelete)
	if err != nil {
		return errors.Wrap(err, "failed to prepare queryPostDelete")
	}

	return nil
}

type SQLiteMindflow struct {
	db *sql.DB
}

func New(path string) (mindflow.Mindflow, error) {
	db, err := sql.Open("sqlite3", dsn(path))
	if err != nil {
		return nil, err
	}

	if err := initDBStatements(db); err != nil {
		return nil, err
	}

	return &SQLiteMindflow{db: db}, nil
}

func dsn(filePath string) string {
	return fmt.Sprintf("file:%s?_journal=WAL&_mutex=full", filePath)
}

func (s *SQLiteMindflow) NewPost(post *mindflow.Post) error {
	tx, err := s.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	r, err := tx.Stmt(stmtPostNew).Exec(post.Category.ID, post.Date.UTC().Unix(), post.Title, post.Body)
	if err != nil {
		return err
	}

	post.ID, err = r.LastInsertId()
	if err != nil {
		return err
	}

	tx.Commit()

	return nil
}

func (s *SQLiteMindflow) EditPost(post *mindflow.Post) error {
	tx, err := s.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	_, err = tx.Stmt(stmtPostEdit).Exec(post.Category.ID, post.Title, post.Body, post.ID)
	if err != nil {
		return err
	}

	tx.Commit()

	return nil
}

func (s *SQLiteMindflow) DeletePost(id int64) error {
	tx, err := s.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if _, err = tx.Stmt(stmtPostDelete).Exec(id); err != nil {
		return err
	}

	tx.Commit()

	return nil
}

func (s *SQLiteMindflow) Posts() (posts []mindflow.Post, err error) {
	tx, err := s.db.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	rows, err := tx.Stmt(stmtPostGetAll).Query()
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var post mindflow.Post
		var date_unix int64

		if err = rows.Scan(&post.ID, &post.Category.ID, &post.Category.Name, &date_unix, &post.Title, &post.Body); err != nil {
			return nil, err
		}

		post.Date = time.Unix(date_unix, 0)

		posts = append(posts, post)
	}

	tx.Commit()

	return posts, nil
}

func (s *SQLiteMindflow) NewCategory(category *mindflow.Category) (int64, error) {
	tx, err := s.db.Begin()
	if err != nil {
		return 0, err
	}
	defer tx.Rollback()

	r, err := tx.Stmt(stmtCategoryNew).Exec(category.Name)
	if err != nil {
		return 0, err
	}

	id, err := r.LastInsertId()
	if err != nil {
		return 0, err
	}

	tx.Commit()

	return id, nil
}

func (s *SQLiteMindflow) EditCategory(category *mindflow.Category) error {
	tx, err := s.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	_, err = tx.Stmt(stmtCategoryEdit).Exec(category.Name, category.ID)
	if err != nil {
		return err
	}

	tx.Commit()

	return nil
}

func (s *SQLiteMindflow) DeleteCategory(id int64) (err error) {
	tx, err := s.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if _, err = tx.Stmt(stmtCategoryDelete).Exec(id); err != nil {
		return err
	}

	tx.Commit()

	return nil
}

func (s *SQLiteMindflow) Categories() (categories []mindflow.Category, err error) {
	tx, err := s.db.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	rows, err := tx.Stmt(stmtCategoryGetAll).Query()
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var category mindflow.Category

		if err = rows.Scan(&category.ID, &category.Name); err != nil {
			return nil, err
		}

		categories = append(categories, category)
	}

	tx.Commit()

	return categories, nil
}

func (s *SQLiteMindflow) Close() error {
	stmtCategoryGetAll.Close()
	stmtCategoryNew.Close()
	stmtCategoryEdit.Close()
	stmtCategoryDelete.Close()
	stmtPostDelete.Close()
	stmtPostEdit.Close()
	stmtPostGetAll.Close()
	stmtPostNew.Close()

	return s.db.Close()
}
