package sqlite

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"

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

type TextRepository struct {
	db *DB
}

func NewTextRepository(db *DB) *TextRepository {
	return &TextRepository{db: db}
}

// Put кладёт текст записи, заменяя прежний того же вида.
//
// Замена, а не вставка: пара «запись и вид» уникальна, и повтор прерванного шага
// иначе завёл бы второй комплект строк — тогда вопрос «какой текст отдавать
// человеку» стал бы вопросом порядка записи, а не состояния.
//
// **Пустое не кладётся поверх непустого**, и это не осторожность, а защита
// архива. Повторный опрос той же операции — обычное дело: держатель захвата
// умер, сохранение рубежа отказало, человек вернул запись в работу. Провайдер
// при этом вправе ответить пустым потоком, отказом это не считается, и
// безусловная замена стирала бы сохранённую расшифровку живого человека без
// следа и без возврата. Та же защита стоит у сохранённого ответа провайдера, и
// разное правило у двух хранителей одного результата читалось бы как недосмотр.
//
// **Граница транзакции — весь метод.** Он читает состояние, которое сам же
// пишет, и идёт целиком по пишущему соединению: разорванный надвое, он завёл бы
// вторую строку на гонке двух шагов.
func (repo *TextRepository) Put(recordID, kind, contents string) (*entity.Text, error) {
	tx, err := repo.db.Writer().BeginTx(context.Background(), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to open a transaction for the text of record %s: %w", recordID, err)
	}
	defer func() { _ = tx.Rollback() }()

	var (
		id       string
		existing string
	)
	err = tx.QueryRowContext(context.Background(),
		"SELECT id, contents FROM texts WHERE record_id = ? AND kind = ?", recordID, kind,
	).Scan(&id, &existing)

	now := formatTime(clock.Now())

	switch {
	case err == nil:
		// Прежнее непустое содержимое пустым не заменяется: строка остаётся как
		// есть, и вызывающий получает её обратно.
		if contents == "" && existing != "" {
			return &entity.Text{Id: id, RecordID: recordID, Kind: kind, Contents: existing}, nil
		}
		if _, err := tx.ExecContext(context.Background(),
			"UPDATE texts SET contents = ?, updated_at = ? WHERE id = ?", contents, now, id,
		); err != nil {
			// Текст расшифровки наружу не выходит даже отказом: цепочка `%w` от
			// драйвера несёт значение поля.
			return nil, fmt.Errorf("failed to store text of kind %s for record %s", kind, recordID)
		}
	case errors.Is(err, sql.ErrNoRows):
		id = ident.New()
		if _, err := tx.ExecContext(context.Background(),
			`INSERT INTO texts (id, record_id, kind, contents, created_at, updated_at)
			 VALUES (?, ?, ?, ?, ?, ?)`,
			id, recordID, kind, contents, now, now,
		); err != nil {
			return nil, fmt.Errorf("failed to store text of kind %s for record %s", kind, recordID)
		}
	default:
		// Отказ базы «строкой нет» не является, и подменять его вставкой нельзя:
		// она упрётся в уникальный индекс, и наверх уедет жалоба на запись
		// вместо правды о недоступной базе.
		return nil, fmt.Errorf("failed to look up text of kind %s for record %s: %w", kind, recordID, err)
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("failed to commit the text of record %s: %w", recordID, err)
	}

	return &entity.Text{Id: id, RecordID: recordID, Kind: kind, Contents: contents}, nil
}

func (repo *TextRepository) GetByID(id string) (*entity.Text, error) {
	text := &entity.Text{Id: id}
	err := repo.db.Reader().QueryRowContext(context.Background(),
		"SELECT record_id, kind, contents FROM texts WHERE id = ?", id,
	).Scan(&text.RecordID, &text.Kind, &text.Contents)
	if err != nil {
		return nil, fmt.Errorf("failed to get text %s: %w", id, err)
	}
	return text, nil
}

type StructureRepository struct {
	db *DB
}

func NewStructureRepository(db *DB) *StructureRepository {
	return &StructureRepository{db: db}
}

// Put кладёт структуру реплик, заменяя прежнюю той же версии разбора. Довод тот
// же, что и у текста: повтор шага не должен заводить второй строки, а пустой
// перечень реплик поверх непустого не кладётся.
func (repo *StructureRepository) Put(recordID string, version int, replicas []entity.Replica) (*entity.Structure, error) {
	if replicas == nil {
		replicas = []entity.Replica{}
	}
	contents, err := json.Marshal(replicas)
	if err != nil {
		return nil, fmt.Errorf("failed to encode structure of record %s", recordID)
	}

	tx, err := repo.db.Writer().BeginTx(context.Background(), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to open a transaction for the structure of record %s: %w", recordID, err)
	}
	defer func() { _ = tx.Rollback() }()

	var (
		id       string
		existing string
	)
	err = tx.QueryRowContext(context.Background(),
		"SELECT id, contents FROM structures WHERE record_id = ? AND version = ?", recordID, version,
	).Scan(&id, &existing)

	now := formatTime(clock.Now())

	switch {
	case err == nil:
		if len(replicas) == 0 && len(existing) > len("[]") {
			stored, decodeErr := decodeReplicas(id, existing)
			if decodeErr != nil {
				return nil, decodeErr
			}
			return &entity.Structure{Id: id, RecordID: recordID, Version: version, Replicas: stored}, nil
		}
		if _, err := tx.ExecContext(context.Background(),
			"UPDATE structures SET contents = ?, updated_at = ? WHERE id = ?", string(contents), now, id,
		); err != nil {
			return nil, fmt.Errorf("failed to store structure of record %s", recordID)
		}
	case errors.Is(err, sql.ErrNoRows):
		id = ident.New()
		if _, err := tx.ExecContext(context.Background(),
			`INSERT INTO structures (id, record_id, version, contents, created_at, updated_at)
			 VALUES (?, ?, ?, ?, ?, ?)`,
			id, recordID, version, string(contents), now, now,
		); err != nil {
			return nil, fmt.Errorf("failed to store structure of record %s", recordID)
		}
	default:
		return nil, fmt.Errorf("failed to look up structure of record %s: %w", recordID, err)
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("failed to commit the structure of record %s: %w", recordID, err)
	}

	return &entity.Structure{Id: id, RecordID: recordID, Version: version, Replicas: replicas}, nil
}

func (repo *StructureRepository) GetByID(id string) (*entity.Structure, error) {
	structure := &entity.Structure{Id: id}
	var contents string
	err := repo.db.Reader().QueryRowContext(context.Background(),
		"SELECT record_id, version, contents FROM structures WHERE id = ?", id,
	).Scan(&structure.RecordID, &structure.Version, &contents)
	if err != nil {
		return nil, fmt.Errorf("failed to get structure %s: %w", id, err)
	}

	replicas, err := decodeReplicas(id, contents)
	if err != nil {
		return nil, err
	}
	structure.Replicas = replicas

	return structure, nil
}

// decodeReplicas разбирает сохранённые реплики. Текст расшифровки наружу
// отказом не выходит: сообщение несёт идентификатор строки, и только его.
func decodeReplicas(id, contents string) ([]entity.Replica, error) {
	if contents == "" {
		return nil, nil
	}
	var replicas []entity.Replica
	if err := json.Unmarshal([]byte(contents), &replicas); err != nil {
		return nil, fmt.Errorf("failed to decode structure %s", id)
	}
	return replicas, nil
}
