package sqlite

import (
	"context"
	"fmt"
	"strings"

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

// defaultListLimit — умолчание, к которому приводится непозитивный предел.
// Значение своё, а не занятое у транспорта: адаптер о транспорте не знает.
const defaultListLimit = 30

// List отдаёт страницу записей владельца, новыми сверху.
//
// **Страница берётся ключом, а не смещением.** Приём пишет в голову той же
// ленты, которую читает список, и человек, загрузивший запись и листающий свой
// архив, — штатный сценарий. Смещение сдвинуло бы окно на единицу: последний
// элемент первой страницы пришёл бы вторым разом первым элементом второй, а один
// элемент между ними не пришёл бы никогда — и оба раза молча.
//
// Ключ полный: пара «время заведения и идентификатор». Одного времени мало — у
// записей, принятых одним запросом, оно совпадает, и порядок между ними иначе не
// определён вовсе.
//
// Ни расшифровки, ни структуры реплик выборка не читает: обе лежат порознь от
// записи ровно затем, чтобы список их не тянул. Длительность и размер берутся
// колонками самой записи.
func (repo *AudioRecordRepository) List(q contract.RecordQuery) (*contract.RecordPage, error) {
	// Пустой владелец не совпадает ни с одной записью. Правило записано со
	// стороны спрашивающего: обязательность, которую держит одна лишь схема,
	// пустую строку пропустила бы.
	if q.OwnerID == "" {
		return &contract.RecordPage{Items: []*entity.AudioRecord{}}, nil
	}

	// Непозитивный предел приводится к умолчанию, а не роняет процесс: ниже
	// стоит обращение по индексу `q.Limit-1`, и нулевой предел дал бы индекс −1.
	// Сегодня отсекает его обработчик, но метод — часть интерфейса, и второй
	// вызывающий с забытым полем структуры получил бы панику, а восстановления у
	// воркеров нет вовсе.
	if q.Limit <= 0 {
		q.Limit = defaultListLimit
	}

	conditions := []string{"owner_id = ?"}
	args := []any{q.OwnerID}

	state, stateArgs, err := stateCondition(q.Filter)
	if err != nil {
		return nil, err
	}
	if state != "" {
		conditions = append(conditions, state)
		args = append(args, stateArgs...)
	}

	total, err := repo.countRecords(conditions, args)
	if err != nil {
		return nil, err
	}

	pageConditions := conditions
	pageArgs := args
	// Курсор режет ленту по паре: строго раньше по времени, а при равном времени
	// — строго меньше по идентификатору.
	if q.Cursor != nil {
		pageConditions = append(append([]string{}, conditions...),
			"(created_at < ? OR (created_at = ? AND id < ?))")
		cursorTime := formatTime(q.Cursor.CreatedAt)
		pageArgs = append(append([]any{}, args...),
			cursorTime, cursorTime, q.Cursor.ID)
	}

	row := &recordRow{}
	columns, targets := selectList(readRecordColumns(row), "")

	// Просим на одну больше предела: лишняя запись отвечает на вопрос «есть ли
	// следующая страница» без второго запроса и без вычислений по общему числу,
	// которое к этому моменту могло измениться.
	query := "SELECT " + columns + " FROM " + recordsTable +
		" WHERE " + strings.Join(pageConditions, " AND ") +
		" ORDER BY created_at DESC, id DESC LIMIT ?"
	pageArgs = append(pageArgs, q.Limit+1)

	rows, err := repo.db.Reader().QueryContext(context.Background(), query, pageArgs...)
	if err != nil {
		return nil, fmt.Errorf("failed to list audio records: %w", err)
	}
	defer func() { _ = rows.Close() }()

	items := []*entity.AudioRecord{}
	for rows.Next() {
		if err := rows.Scan(targets...); err != nil {
			return nil, fmt.Errorf("failed to read an audio record of the page: %w", err)
		}
		items = append(items, rowToAudioRecord(row))
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("failed to read the page of audio records: %w", err)
	}

	page := &contract.RecordPage{TotalItems: total}
	if len(items) > q.Limit {
		last := items[q.Limit-1]
		page.NextCursor = &contract.RecordCursor{
			CreatedAt: last.CreatedAt,
			ID:        last.Id,
		}
		items = items[:q.Limit]
	}

	ids := make([]string, 0, len(items))
	for _, item := range items {
		ids = append(ids, item.Id)
	}
	topics, err := repo.topicsOf(ids)
	if err != nil {
		return nil, err
	}
	for _, item := range items {
		item.TopicIDs = topics[item.Id]
	}

	page.Items = items
	return page, nil
}

// stateCondition переводит состояние отбора в условие запроса.
//
// Перечень рубежей сюда не переписывается: он приходит из дескриптора. Отбор
// списка — очередной его потребитель, и рубеж, добавленный конвейером, иначе
// молча поменял бы состав всех трёх состояний.
func stateCondition(filter *entity.ListFilter) (string, []any, error) {
	if filter == nil {
		return "", nil, nil
	}

	switch *filter {
	case entity.ListFilterHalted:
		return "halted_at IS NOT NULL", nil, nil
	case entity.ListFilterWorking:
		condition, args := stateIn(entity.WorkingStages())
		return "halted_at IS NULL AND " + condition, args, nil
	case entity.ListFilterDone:
		condition, args := stateIn(entity.TerminalStages())
		return "halted_at IS NULL AND " + condition, args, nil
	}

	// Ветвь отказа, а не молчаливое «без сужения»: значение, добавленное в
	// перечень состояний и забытое здесь, иначе вернуло бы человеку весь архив
	// под именем отбора — и заметить это было бы нечем.
	return "", nil, fmt.Errorf("%w: unknown list filter %q", contract.ErrBadRequest, *filter)
}

func stateIn(stages []entity.Stage) (string, []any) {
	names := entity.StageNames(stages)
	args := make([]any, 0, len(names))
	placeholders := make([]string, 0, len(names))
	for _, name := range names {
		args = append(args, name)
		placeholders = append(placeholders, "?")
	}
	return "state IN (" + strings.Join(placeholders, ", ") + ")", args
}

func (repo *AudioRecordRepository) countRecords(conditions []string, args []any) (int, error) {
	var total int
	query := "SELECT COUNT(*) FROM " + recordsTable + " WHERE " + strings.Join(conditions, " AND ")
	if err := repo.db.Reader().QueryRowContext(context.Background(), query, args...).Scan(&total); err != nil {
		return 0, fmt.Errorf("failed to count audio records: %w", err)
	}
	return total, nil
}

// topicsOf читает темы страницы **одним запросом**: страница в сотню записей
// иначе стоила бы сотни обращений к базе.
func (repo *AudioRecordRepository) topicsOf(recordIDs []string) (map[string][]string, error) {
	out := map[string][]string{}
	if len(recordIDs) == 0 {
		return out, nil
	}

	placeholders := make([]string, 0, len(recordIDs))
	args := make([]any, 0, len(recordIDs))
	for _, id := range recordIDs {
		placeholders = append(placeholders, "?")
		args = append(args, id)
	}

	query := "SELECT record_id, topic_id FROM record_topics WHERE record_id IN (" +
		strings.Join(placeholders, ", ") + ") ORDER BY topic_id"

	rows, err := repo.db.Reader().QueryContext(context.Background(), query, args...)
	if err != nil {
		return nil, fmt.Errorf("failed to read record topics: %w", err)
	}
	defer func() { _ = rows.Close() }()

	for rows.Next() {
		var recordID, topicID string
		if err := rows.Scan(&recordID, &topicID); err != nil {
			return nil, fmt.Errorf("failed to read a record topic: %w", err)
		}
		out[recordID] = append(out[recordID], topicID)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("failed to read record topics: %w", err)
	}

	return out, nil
}

// ResolveTopicNames разрешает темы названиями **одним запросом на страницу**, а
// не по запросу на запись.
//
// Названия, а не идентификаторы, потому что экран показывает названия: отдай мы
// ссылки, форму ответа переделывала бы задача языковой модели — ровно то, ради
// чего контракт согласуется один раз.
//
// Сужение владельцем стоит и здесь: словарь тем свой у каждого человека — пара
// «владелец и название» уникальна, — и разрешение без сужения отдало бы название
// чужой темы, как только темы начнёт писать языковая модель.
func (repo *AudioRecordRepository) ResolveTopicNames(ownerID string, ids []string) (map[string]string, error) {
	out := map[string]string{}
	if len(ids) == 0 || ownerID == "" {
		return out, nil
	}

	placeholders := make([]string, 0, len(ids))
	args := []any{ownerID}
	for _, id := range ids {
		placeholders = append(placeholders, "?")
		args = append(args, id)
	}

	query := "SELECT id, name FROM topics WHERE owner_id = ? AND id IN (" +
		strings.Join(placeholders, ", ") + ")"

	rows, err := repo.db.Reader().QueryContext(context.Background(), query, args...)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve topics: %w", err)
	}
	defer func() { _ = rows.Close() }()

	for rows.Next() {
		var id, name string
		if err := rows.Scan(&id, &name); err != nil {
			return nil, fmt.Errorf("failed to read a topic: %w", err)
		}
		out[id] = name
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("failed to resolve topics: %w", err)
	}

	return out, nil
}
