package sqlite

import (
	"database/sql"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	sqlitedriver "modernc.org/sqlite"
	sqlitelib "modernc.org/sqlite/lib"
)

// timeLayout — единственный вид времени в схеме: RFC 3339, UTC, суффикс `Z`,
// секундная точность.
//
// Ширина такой записи постоянная, поэтому лексикографический порядок `TEXT`
// совпадает с хронологией, и отбор по колонке времени работает без разбора
// значения. Своего типа времени у SQLite нет: колонка хранит то, что в неё
// положили, — колонка, заполненная то одним видом, то другим, обратила бы
// условие срока протухания захвата в постоянную истину или ложь молча, и запись
// не выдавалась бы ни одному воркеру никогда.
const timeLayout = "2006-01-02T15:04:05Z"

// formatTime приводит метку времени к виду колонки.
func formatTime(v time.Time) string {
	return v.UTC().Format(timeLayout)
}

// timeValue кладёт время в колонку, допускающую пустое значение. Нулевое время
// и отсутствующее — одно и то же: «времени нет».
func timeValue(v *time.Time) any {
	if v == nil || v.IsZero() {
		return nil
	}
	return formatTime(*v)
}

// timeOf читает колонку времени. Нечитаемое значение отдаётся нулевым: колонка
// пишется только нами, и разбор здесь — сторож, а не ветвь поведения.
func timeOf(v sql.NullString) *time.Time {
	if !v.Valid || v.String == "" {
		return nil
	}
	parsed, err := time.Parse(timeLayout, v.String)
	if err != nil {
		return nil
	}
	parsed = parsed.UTC()
	return &parsed
}

// requiredTimeOf читает обязательную колонку времени.
func requiredTimeOf(v string) time.Time {
	parsed, err := time.Parse(timeLayout, v)
	if err != nil {
		return time.Time{}
	}
	return parsed.UTC()
}

// stringValue кладёт необязательную строку: пустая и отсутствующая — одно и то
// же.
func stringValue(v *string) any {
	if v == nil || *v == "" {
		return nil
	}
	return *v
}

// stringOf читает необязательную строку.
func stringOf(v sql.NullString) *string {
	if !v.Valid || v.String == "" {
		return nil
	}
	out := v.String
	return &out
}

// numberOf читает необязательное число.
//
// Указатель здесь не выражает «неизвестно»: обе величины записи ставит приём и
// ставит всегда, а колонки объявлены обязательными. Форма осталась указателем
// потому, что её несёт домен, а ответ приложению обязан различать поле и его
// отсутствие.
func numberOf(v int64) *int64 {
	out := v
	return &out
}

// numberValue кладёт необязательное число нулём: колонка обязательна.
func numberValue(v *int64) int64 {
	if v == nil {
		return 0
	}
	return *v
}

// insertSQL собирает вставку из карты «колонка → значение».
//
// Именованными параметрами, а не позиционным списком: у аудиозаписи поля одного
// типа идут длинным непрерывным рядом, и позиционный сдвиг на одно поле
// скомпилировался бы молча, положив идентификатор файла в колонку текста. По
// имени такого сдвига не существует вовсе.
//
// Порядок колонок берётся сортировкой, а не порядком обхода карты: обход карты
// в Go случаен, и текст запроса менялся бы от прогона к прогону — отладка по
// журналу читала бы каждый раз новый запрос.
func insertSQL(table string, values map[string]any) (string, []any) {
	names := sortedNames(values)

	placeholders := make([]string, 0, len(names))
	args := make([]any, 0, len(names))
	for _, name := range names {
		placeholders = append(placeholders, ":"+name)
		args = append(args, sql.Named(name, values[name]))
	}

	query := fmt.Sprintf(
		"INSERT INTO %s (%s) VALUES (%s)",
		table,
		strings.Join(names, ", "),
		strings.Join(placeholders, ", "),
	)

	return query, args
}

// updateSQL собирает правку из карты «колонка → значение» и условия.
func updateSQL(table string, values map[string]any, where string, whereArgs []any) (string, []any) {
	names := sortedNames(values)

	assignments := make([]string, 0, len(names))
	args := make([]any, 0, len(names)+len(whereArgs))
	for _, name := range names {
		assignments = append(assignments, name+" = :"+name)
		args = append(args, sql.Named(name, values[name]))
	}
	args = append(args, whereArgs...)

	query := fmt.Sprintf(
		"UPDATE %s SET %s WHERE %s",
		table,
		strings.Join(assignments, ", "),
		where,
	)

	return query, args
}

func sortedNames(values map[string]any) []string {
	names := make([]string, 0, len(values))
	for name := range values {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

// selectList собирает перечень колонок для выборки из той же карты, по которой
// потом идёт чтение. Один источник у обеих половин: колонка, забытая в перечне,
// не имеет места назначения, и наоборот — расхождению взяться неоткуда.
func selectList(targets map[string]any, prefix string) (string, []any) {
	names := sortedNames(targets)

	columns := make([]string, 0, len(names))
	scan := make([]any, 0, len(names))
	for _, name := range names {
		columns = append(columns, prefix+name)
		scan = append(scan, targets[name])
	}

	return strings.Join(columns, ", "), scan
}

// isUniqueViolation говорит, отказала ли запись по уникальному индексу.
//
// Судится **код** отказа, а не его текст: текст у драйвера свой на каждую
// версию, а узнавание ошибки по тексту запрещено правилом проекта. Какая именно
// колонка не сошлась, код не называет — и это не мешает: заведение учётной
// записи различает два отказа повторным поиском по ключу, а не разбором текста.
func isUniqueViolation(err error) bool {
	var sqliteErr *sqlitedriver.Error
	if !errors.As(err, &sqliteErr) {
		return false
	}
	return sqliteErr.Code() == sqlitelib.SQLITE_CONSTRAINT_UNIQUE ||
		sqliteErr.Code() == sqlitelib.SQLITE_CONSTRAINT_PRIMARYKEY
}
