package migrations

import (
	"context"
	"database/sql"
	"fmt"
	"strconv"

	"git.vakhrushev.me/av/transcriber/internal/entity"
)

// up202608220002 заводит схему сервиса целиком.
//
// Шаг один, и он начальный: прежние шаги встроенного хранилища удалены вместе с
// ним — разовое снятие инварианта «применённая миграция не переписывается»
// решением владельца от 2026-08-22. Причина названа прямо: стадия проекта —
// стройка, на сервере данных нет, сервис остановлен, а новая база ведёт учёт
// применённого своей таблицей, которой отметки прежнего каталога не годятся
// вовсе. Снятие кончается этим шагом: уехав на сервер, он подпадает под
// инвариант как всякий прежний.
//
// Порядок заведения задан связями: сперва учётные записи, потом всё, что на них
// ссылается, и только потом обратные ссылки записи на её приложения.
//
// **Времени умолчанием схема не ставит.** Вид времени один на все колонки —
// `TEXT` в RFC 3339, UTC, секундная точность, — и ставит его приложение единой
// точкой. `CURRENT_TIMESTAMP` писал бы свой вид, отличный от объявленного, а
// вставка, забывшая проставить время, при умолчании прошла бы молча.
//
// **Перечни значений держит код, а не схема.** Прежде рубеж, причина остановки
// и вид текста были закрыты схемой, потому что панель владельца правила запись
// руками и вправе была завести значение, которого сервис не знает. Панели нет,
// правка идёт только нашим кодом, и `CHECK` остался бы ценой — новое значение
// стоило бы нового шага схемы — без покупателя.
func up202608220002(ctx context.Context, tx *sql.Tx) error {
	for _, statement := range initStatements() {
		if _, err := tx.ExecContext(ctx, statement); err != nil {
			return fmt.Errorf("failed to apply initial schema: %w", err)
		}
	}
	return nil
}

// down202608220002 сносит схему целиком. Порядок обратный порядку заведения:
// приложения ссылаются на запись, запись — на учётную запись.
func down202608220002(ctx context.Context, tx *sql.Tx) error {
	tables := []string{
		"record_events",
		"recognitions",
		"structures",
		"texts",
		"record_topics",
		"audio_records",
		"topics",
		"files",
		"users",
	}
	for _, table := range tables {
		if _, err := tx.ExecContext(ctx, "DROP TABLE IF EXISTS "+table); err != nil {
			return fmt.Errorf("failed to drop %s: %w", table, err)
		}
	}
	return nil
}

// initStatements — шаг по одному оператору на элемент.
//
// Россыпью, а не одной строкой с разделителями: тело триггера само несёт точку с
// запятой, и разбиение общей строки резало бы его пополам.
func initStatements() []string {
	return []string{
		// Учётная запись. Ключ — логин у провайдера: его приносит заголовок
		// доверенного источника, и по нему запись находится при каждом
		// обращении. Адрес почты необязателен и ключом не служит — он меняется,
		// и первое обращение с чужим адресом досталось бы чужой записи.
		`CREATE TABLE users (
			id              TEXT    NOT NULL PRIMARY KEY,
			provider_login  TEXT    NOT NULL,
			name            TEXT    NOT NULL DEFAULT '',
			email           TEXT    NOT NULL DEFAULT '',
			created_at      TEXT    NOT NULL,
			updated_at      TEXT    NOT NULL
		)`,
		`CREATE UNIQUE INDEX idx_users_provider_login ON users (provider_login)`,
		// Уникальность почты частичная: пустая почта законна и не спорит с
		// другой пустой. Индекс нужен затем, чтобы занятый адрес отвергался
		// схемой — по этому отказу заведение переходит на ветвь «запись без
		// почты», а не отдаёт чужую учётную запись.
		`CREATE UNIQUE INDEX idx_users_email ON users (email) WHERE email <> ''`,

		// Копия записи на диске. Владелец лежит своей колонкой, а не выводится
		// через запись: файл переживает свою запись — шаг заводит его до
		// сохранения, — и заведённый до неё остаётся с владельцем и без ссылки.
		//
		// Ссылки на аудиозапись внешним ключом нет намеренно, и `record_id`
		// здесь — имя подкаталога, где копия лежит. Приём заводит файл **до**
		// самой записи, и обязательная связь отвергала бы первую же принятую
		// запись.
		`CREATE TABLE files (
			id          TEXT    NOT NULL PRIMARY KEY,
			owner_id    TEXT    NOT NULL REFERENCES users (id),
			record_id   TEXT    NOT NULL,
			file_name   TEXT    NOT NULL,
			size_bytes  INTEGER NOT NULL,
			format      TEXT    NOT NULL DEFAULT '',
			duration_ms INTEGER NOT NULL DEFAULT 0,
			created_at  TEXT    NOT NULL
		)`,
		`CREATE INDEX idx_files_owner ON files (owner_id)`,

		// Словарь тем. Своя таблица, а не набор строк в записи: перечень тем
		// человека нужен целиком перед каждым обращением к модели, а собрать его
		// из наборов строк можно только перебором всех его записей.
		`CREATE TABLE topics (
			id         TEXT NOT NULL PRIMARY KEY,
			owner_id   TEXT NOT NULL REFERENCES users (id),
			name       TEXT NOT NULL,
			created_at TEXT NOT NULL,
			updated_at TEXT NOT NULL
		)`,
		`CREATE UNIQUE INDEX idx_topics_owner_name ON topics (owner_id, name)`,

		// Аудиозапись — центральная сущность. Поля очереди соседствуют с
		// доменом, но не с содержимым: расшифровка лежит строкой `texts`, и
		// чтение очереди её не тянет.
		//
		// Колонка владельца обязательна и объявлена внешним ключом: ничьей
		// записи не бывает, и держит это схема, а не проверка вызывающего.
		// Пустое значение внешнему ключу не отвечает — идентификаторы у учётных
		// записей непустые, — поэтому ничью запись отвергает та же связь.
		//
		// `duration_ms` и `size_bytes` обязательны и различать «неизвестно» и
		// «ноль» не обязаны: обе величины ставит приём и ставит всегда — запись,
		// метаданные которой прочитать не удалось, отвергается отказом и не
		// заводится вовсе. Решение владельца 2026-08-15.
		`CREATE TABLE audio_records (
			id                 TEXT    NOT NULL PRIMARY KEY,
			owner_id           TEXT    NOT NULL REFERENCES users (id),
			title              TEXT,
			brief              TEXT,
			original_filename  TEXT,
			duration_ms        INTEGER NOT NULL,
			size_bytes         INTEGER NOT NULL,
			state              TEXT    NOT NULL,
			state_entered_at   TEXT    NOT NULL,
			halted_at          TEXT,
			halt_reason        TEXT,
			error_text         TEXT,
			acquisition_id     TEXT,
			acquire_expires_at TEXT,
			delay_time         TEXT,
			attempts           INTEGER NOT NULL DEFAULT 0,
			original_file_id   TEXT             REFERENCES files (id),
			normalized_file_id TEXT             REFERENCES files (id),
			transcript_text_id TEXT,
			literary_text_id   TEXT,
			structure_id       TEXT,
			recognition_id     TEXT,
			created_at         TEXT    NOT NULL,
			updated_at         TEXT    NOT NULL
		)`,
		// Отбор захвата идёт по рубежу, признаку остановки и порядку ленты.
		// Индекс заводится здесь, а не потом: применённый шаг схемы не
		// переписывается, и добавление индекса стоило бы отдельного шага.
		`CREATE INDEX idx_audio_records_acquire
			ON audio_records (state, halted_at, created_at, id)`,
		// Страница списка сужается владельцем и режется полным ключом
		// сортировки — парой «время заведения и ключ записи».
		`CREATE INDEX idx_audio_records_owner_page
			ON audio_records (owner_id, created_at, id)`,

		// Темы записи. Отдельной таблицей связи, а не колонкой-перечнем: у
		// набора строк в колонке нет ни связи, ни потолка.
		`CREATE TABLE record_topics (
			record_id TEXT NOT NULL REFERENCES audio_records (id),
			topic_id  TEXT NOT NULL REFERENCES topics (id),
			PRIMARY KEY (record_id, topic_id)
		)`,
		`CREATE INDEX idx_record_topics_topic ON record_topics (topic_id)`,
		// Потолок числа тем держит схема: без него часовой разговор даёт два
		// десятка тем, и словарь распухает за неделю. Число берётся у домена —
		// то же самое, которое сервис объявляет приложению.
		`CREATE TRIGGER trg_record_topics_limit
			BEFORE INSERT ON record_topics
			BEGIN
				SELECT RAISE(ABORT, 'record has too many topics')
				WHERE (
					SELECT COUNT(*) FROM record_topics WHERE record_id = NEW.record_id
				) >= ` + strconv.Itoa(entity.MaxTopicsPerRecord) + `;
			END`,

		// Тексты записи. Пара «запись и вид» уникальна: повтор прерванного шага
		// иначе завёл бы второй комплект строк, и вопрос «какой текст отдавать
		// человеку» стал бы вопросом порядка записи, а не состояния.
		`CREATE TABLE texts (
			id         TEXT NOT NULL PRIMARY KEY,
			record_id  TEXT NOT NULL REFERENCES audio_records (id),
			kind       TEXT NOT NULL,
			contents   TEXT NOT NULL DEFAULT '',
			created_at TEXT NOT NULL,
			updated_at TEXT NOT NULL
		)`,
		`CREATE UNIQUE INDEX idx_texts_record_kind ON texts (record_id, kind)`,

		// Структура реплик. Номер версии нужен потому, что разбор сохранённого
		// ответа изменится раньше, чем архив пересчитают.
		`CREATE TABLE structures (
			id         TEXT    NOT NULL PRIMARY KEY,
			record_id  TEXT    NOT NULL REFERENCES audio_records (id),
			version    INTEGER NOT NULL,
			contents   TEXT    NOT NULL DEFAULT '[]',
			created_at TEXT    NOT NULL,
			updated_at TEXT    NOT NULL
		)`,
		`CREATE UNIQUE INDEX idx_structures_record_version ON structures (record_id, version)`,

		// Попытка распознавания у внешнего провайдера.
		//
		// Сохранённый ответ лежит **третьим файлом в подкаталоге записи**, а
		// здесь стоит только его имя: шаг опроса читает эту строку раз в
		// несколько секунд, и ответ на многочасовую запись, положенный колонкой,
		// ехал бы в память при каждом опросе.
		`CREATE TABLE recognitions (
			id           TEXT NOT NULL PRIMARY KEY,
			record_id    TEXT NOT NULL REFERENCES audio_records (id),
			provider     TEXT NOT NULL,
			model        TEXT NOT NULL DEFAULT '',
			external_id  TEXT NOT NULL DEFAULT '',
			source_uri   TEXT NOT NULL DEFAULT '',
			payload_file TEXT NOT NULL DEFAULT '',
			started_at   TEXT,
			finished_at  TEXT,
			created_at   TEXT NOT NULL,
			updated_at   TEXT NOT NULL
		)`,
		`CREATE INDEX idx_recognitions_record ON recognitions (record_id)`,

		// Журнал событий записи. Колонка текста отказа зовётся `outcome_text`, а
		// не `error_text`: последнее имя названо поимённо инвариантом проекта о
		// секрете, и две колонки с этим именем сделали бы инвариант
		// двусмысленным.
		`CREATE TABLE record_events (
			id           TEXT    NOT NULL PRIMARY KEY,
			record_id    TEXT    NOT NULL REFERENCES audio_records (id),
			origin       TEXT    NOT NULL,
			step         TEXT    NOT NULL DEFAULT '',
			outcome      TEXT    NOT NULL,
			outcome_text TEXT    NOT NULL DEFAULT '',
			duration_ms  INTEGER NOT NULL DEFAULT 0,
			created_at   TEXT    NOT NULL
		)`,
		`CREATE INDEX idx_record_events_record ON record_events (record_id)`,
	}
}
