package pocketbase

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

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/core"

	"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
	"git.vakhrushev.me/av/transcriber/internal/entity"
)

type TextRepository struct {
	app core.App
}

func NewTextRepository(app core.App) *TextRepository {
	return &TextRepository{app: app}
}

// Put кладёт текст записи, заменяя прежний того же вида.
//
// Замена, а не вставка: пара «запись и вид» уникальна, и повтор прерванного шага
// иначе завёл бы второй комплект строк — тогда вопрос «какой текст отдавать
// человеку» стал бы вопросом порядка записи, а не состояния.
//
// **Пустое не кладётся поверх непустого**, и это не осторожность, а защита
// архива. Повторный опрос той же операции — обычное дело: держатель захвата
// умер, сохранение рубежа отказало, человек снял остановку в панели. Провайдер
// при этом вправе ответить пустым потоком, отказом это не считается, и
// безусловная замена стирала бы сохранённую расшифровку живого человека без
// следа и без возврата. Та же защита стоит у сырого ответа провайдера
// (`RecognitionRepository.Finish`), и разное правило у двух хранителей одного
// результата читалось бы как недосмотр.
func (repo *TextRepository) Put(recordID, kind, contents string) (*entity.Text, error) {
	collection, err := findCollection(repo.app, migrations.TextsCollection)
	if err != nil {
		return nil, err
	}

	record, err := repo.app.FindFirstRecordByFilter(
		migrations.TextsCollection,
		"record = {:record} && kind = {:kind}",
		dbx.Params{"record": recordID, "kind": kind},
	)
	switch {
	case err == nil:
		// Строка есть — заменяем содержимое.
	case errors.Is(err, sql.ErrNoRows):
		record = core.NewRecord(collection)
		record.Set("record", recordID)
		record.Set("kind", kind)
	default:
		// Отказ хранилища «строкой нет» не является, и подменять его вставкой
		// нельзя: она упрётся в уникальный индекс, и наверх уедет жалоба на
		// запись вместо правды о недоступной базе.
		return nil, fmt.Errorf("failed to look up text of kind %s for record %s: %w", kind, recordID, err)
	}
	// Прежнее непустое содержимое пустым не заменяется: строка остаётся как
	// есть, и вызывающий получает её обратно.
	if contents == "" && record.GetString("contents") != "" {
		return textFromRecord(record), nil
	}
	record.Set("contents", contents)

	if err := repo.app.Save(record); err != nil {
		// Текст расшифровки наружу не выходит даже отказом: цепочка `%w` от
		// хранилища несёт значение поля.
		return nil, fmt.Errorf("failed to store text of kind %s for record %s", kind, recordID)
	}

	return textFromRecord(record), nil
}

func (repo *TextRepository) GetByID(id string) (*entity.Text, error) {
	record, err := repo.app.FindRecordById(migrations.TextsCollection, id)
	if err != nil {
		return nil, fmt.Errorf("failed to get text %s: %w", id, err)
	}
	return textFromRecord(record), nil
}

func textFromRecord(record *core.Record) *entity.Text {
	return &entity.Text{
		Id:       record.Id,
		RecordID: record.GetString("record"),
		Kind:     record.GetString("kind"),
		Contents: record.GetString("contents"),
	}
}

type StructureRepository struct {
	app core.App
}

func NewStructureRepository(app core.App) *StructureRepository {
	return &StructureRepository{app: app}
}

// Put кладёт структуру реплик, заменяя прежнюю той же версии разбора. Довод тот
// же, что и у текста: повтор шага не должен заводить второй строки.
func (repo *StructureRepository) Put(recordID string, version int, replicas []entity.Replica) (*entity.Structure, error) {
	collection, err := findCollection(repo.app, migrations.StructuresCollection)
	if err != nil {
		return nil, err
	}

	contents, err := json.Marshal(replicas)
	if err != nil {
		return nil, fmt.Errorf("failed to encode structure of record %s", recordID)
	}

	record, err := repo.app.FindFirstRecordByFilter(
		migrations.StructuresCollection,
		"record = {:record} && version = {:version}",
		dbx.Params{"record": recordID, "version": version},
	)
	switch {
	case err == nil:
		// Строка есть — заменяем содержимое. Пустой перечень реплик поверх
		// непустого не кладётся по тому же доводу, что и у текста: повторный
		// опрос с пустым ответом провайдера стирал бы разбор живой записи.
		if len(replicas) == 0 && len(record.GetString("contents")) > len("[]") {
			return repo.GetByID(record.Id)
		}
	case errors.Is(err, sql.ErrNoRows):
		record = core.NewRecord(collection)
		record.Set("record", recordID)
		record.Set("version", version)
	default:
		return nil, fmt.Errorf("failed to look up structure of record %s: %w", recordID, err)
	}
	record.Set("contents", string(contents))

	if err := repo.app.Save(record); err != nil {
		return nil, fmt.Errorf("failed to store structure of record %s", recordID)
	}

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

func (repo *StructureRepository) GetByID(id string) (*entity.Structure, error) {
	record, err := repo.app.FindRecordById(migrations.StructuresCollection, id)
	if err != nil {
		return nil, fmt.Errorf("failed to get structure %s: %w", id, err)
	}

	var replicas []entity.Replica
	raw := record.GetString("contents")
	if raw != "" {
		if err := json.Unmarshal([]byte(raw), &replicas); err != nil {
			return nil, fmt.Errorf("failed to decode structure %s", id)
		}
	}

	return &entity.Structure{
		Id:       record.Id,
		RecordID: record.GetString("record"),
		Version:  record.GetInt("version"),
		Replicas: replicas,
	}, nil
}
