package orm

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log/slog"
	"reflect"
	"strings"

	"github.com/mattn/go-sqlite3"
)

func IsUniqueViolation(err error) bool {
	var sqlErr sqlite3.Error
	if !errors.As(err, &sqlErr) {
		return false
	}
	return sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique ||
		sqlErr.ExtendedCode == sqlite3.ErrConstraintPrimaryKey
}

type migrationFn = func(*sql.Tx) error

func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
	logger = logger.With("migration", name)

	// check first, or a busy db fails every boot on the write lock
	var applied bool
	if err := c.QueryRowContext(context.Background(), "select exists (select 1 from migrations where name = ?)", name).Scan(&applied); err != nil {
		return err
	}
	if applied {
		logger.Warn("skipped migration, already applied")
		return nil
	}

	tx, err := c.BeginTx(context.Background(), nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	var exists bool
	err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
	if err != nil {
		return err
	}

	if !exists {
		// run migration
		err = migrationFn(tx)
		if err != nil {
			logger.Error("failed to run migration", "err", err)
			return err
		}

		// mark migration as complete
		_, err = tx.Exec("insert into migrations (name) values (?)", name)
		if err != nil {
			logger.Error("failed to mark migration as complete", "err", err)
			return err
		}

		// commit the transaction
		if err := tx.Commit(); err != nil {
			return err
		}

		logger.Info("migration applied successfully")
	} else {
		logger.Warn("skipped migration, already applied")
	}

	return nil
}

type Filter struct {
	Key string
	arg any
	Cmp string
}

func newFilter(key, cmp string, arg any) Filter {
	return Filter{
		Key: key,
		arg: arg,
		Cmp: cmp,
	}
}

func FilterEq(key string, arg any) Filter      { return newFilter(key, "=", arg) }
func FilterNotEq(key string, arg any) Filter   { return newFilter(key, "<>", arg) }
func FilterGte(key string, arg any) Filter     { return newFilter(key, ">=", arg) }
func FilterLt(key string, arg any) Filter      { return newFilter(key, "<", arg) }
func FilterLte(key string, arg any) Filter     { return newFilter(key, "<=", arg) }
func FilterIs(key string, arg any) Filter      { return newFilter(key, "is", arg) }
func FilterIsNot(key string, arg any) Filter   { return newFilter(key, "is not", arg) }
func FilterIn(key string, arg any) Filter      { return newFilter(key, "in", arg) }
func FilterLike(key string, arg any) Filter    { return newFilter(key, "like", arg) }
func FilterNotLike(key string, arg any) Filter { return newFilter(key, "not like", arg) }
func FilterContains(key string, arg any) Filter {
	return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
}

// FilterInSubquery compiles to `key in (subquery)`, binding args within the
// subquery. Prefer this over FilterIn with a large materialized list: it
// keeps the query text constant and lets sqlite plan a semi-join.
func FilterInSubquery(key, subquery string, args ...any) Filter {
	return newFilter(key, "in", subqueryArg{query: subquery, args: args})
}

type subqueryArg struct {
	query string
	args  []any
}

func (f Filter) Condition() string {
	if sub, ok := f.arg.(subqueryArg); ok {
		return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, sub.query)
	}

	rv := reflect.ValueOf(f.arg)
	kind := rv.Kind()

	// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
	if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
		if rv.Len() == 0 {
			// always false
			return "1 = 0"
		}

		placeholders := make([]string, rv.Len())
		for i := range placeholders {
			placeholders[i] = "?"
		}

		return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, strings.Join(placeholders, ", "))
	}

	return fmt.Sprintf("%s %s ?", f.Key, f.Cmp)
}

func (f Filter) Arg() []any {
	if sub, ok := f.arg.(subqueryArg); ok {
		return sub.args
	}

	rv := reflect.ValueOf(f.arg)
	kind := rv.Kind()
	if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
		if rv.Len() == 0 {
			return nil
		}

		out := make([]any, rv.Len())
		for i := range rv.Len() {
			out[i] = rv.Index(i).Interface()
		}
		return out
	}

	return []any{f.arg}
}
