package pocketbase

import (
	"time"

	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tools/types"

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

// Отображение аудиозаписи в запись коллекции и обратно живёт одним местом.
//
// Мест стало **два** вместо прежних четырёх: захват больше не перечисляет
// колонки поимённо, а возвращает идентификатор и признак своего захвата.
// Инвариант проекта о колонках очереди этим съёживается и перестаёт расти с
// моделью — иначе каждая новая колонка записи попадала бы под него.

// applyOwnedByPipeline кладёт в запись только те поля, которыми распоряжается
// конвейер. Поля, которые он не меняет никогда — владелец, вход, заголовок,
// краткое описание, темы и адресат ответа, — не трогаются вовсе.
//
// Разрез нужен потому, что шаг держит запись снимком с момента захвата и до
// своего сохранения, а это часы. Всё, что владелец правил в панели за это время,
// безусловная запись снимка стёрла бы молча: ни строки в журнале, ни отказа в
// панели — владелец видел бы успешное сохранение и был бы уверен, что правка на
// месте.
func applyOwnedByPipeline(record *core.Record, r *entity.AudioRecord) {
	record.Set("state", r.State)
	record.Set("state_entered_at", dateOrEmpty(&r.StateEnteredAt))
	record.Set("halted_at", dateOrEmpty(r.HaltedAt))
	record.Set("halt_reason", derefString(r.HaltReason))
	record.Set("error_text", derefString(r.ErrorText))
	record.Set("acquisition_id", derefString(r.AcquisitionID))
	record.Set("acquire_expires_at", dateOrEmpty(r.AcquireExpiresAt))
	record.Set("delay_time", dateOrEmpty(r.DelayTime))
	record.Set("attempts", r.Attempts)
	record.Set("original_file", derefString(r.OriginalFileID))
	record.Set("normalized_file", derefString(r.NormalizedFileID))
	record.Set("transcript_text", derefString(r.TranscriptTextID))
	record.Set("literary_text", derefString(r.LiteraryTextID))
	record.Set("structure", derefString(r.StructureID))
	record.Set("recognition", derefString(r.RecognitionID))
}

// applyToRecord кладёт запись целиком — это заведение, и спорить за поля здесь
// не с кем.
func applyToRecord(record *core.Record, r *entity.AudioRecord) {
	applyOwnedByPipeline(record, r)
	// Владелец кладётся только здесь, при заведении. В applyOwnedByPipeline его
	// нет намеренно: конвейер владельца не назначает и не меняет, а снимок шага,
	// записанный поверх, стёр бы его молча.
	record.Set("owner", r.OwnerID)
	record.Set("source", r.Source)
	record.Set("title", derefString(r.Title))
	record.Set("brief", derefString(r.Brief))
	// Имя файла отправителя, длительность и размер кладёт приём и только он: это
	// снимок принятого, и конвейер его не пересчитывает. В applyOwnedByPipeline их
	// нет намеренно — снимок шага, записанный поверх, стёр бы их молча.
	record.Set("original_filename", derefString(r.OriginalFilename))
	record.Set("duration_ms", numberOrZero(r.DurationMs))
	record.Set("size_bytes", numberOrZero(r.SizeBytes))
	// Темы кладутся при заведении пустыми и конвейером не трогаются: считает их
	// языковая модель отдельной задачей. Пишутся здесь ради симметрии с чтением —
	// колонка, которую читают и не пишут, ничем не отличима от забытой.
	record.Set("topics", r.TopicIDs)
}

func recordToAudioRecord(record *core.Record) *entity.AudioRecord {
	return &entity.AudioRecord{
		Id:               record.Id,
		OwnerID:          record.GetString("owner"),
		Source:           record.GetString("source"),
		Title:            nilIfEmpty(record.GetString("title")),
		Brief:            nilIfEmpty(record.GetString("brief")),
		State:            record.GetString("state"),
		StateEnteredAt:   record.GetDateTime("state_entered_at").Time(),
		HaltedAt:         timeOrNil(record.GetDateTime("halted_at")),
		HaltReason:       nilIfEmpty(record.GetString("halt_reason")),
		ErrorText:        nilIfEmpty(record.GetString("error_text")),
		AcquisitionID:    nilIfEmpty(record.GetString("acquisition_id")),
		AcquireExpiresAt: timeOrNil(record.GetDateTime("acquire_expires_at")),
		DelayTime:        timeOrNil(record.GetDateTime("delay_time")),
		Attempts:         record.GetInt("attempts"),
		OriginalFileID:   nilIfEmpty(record.GetString("original_file")),
		NormalizedFileID: nilIfEmpty(record.GetString("normalized_file")),
		TranscriptTextID: nilIfEmpty(record.GetString("transcript_text")),
		LiteraryTextID:   nilIfEmpty(record.GetString("literary_text")),
		StructureID:      nilIfEmpty(record.GetString("structure")),
		RecognitionID:    nilIfEmpty(record.GetString("recognition")),
		OriginalFilename: nilIfEmpty(record.GetString("original_filename")),
		// Имя колонки стоит литералом рядом с `.Get…`, а не уезжает в аргумент
		// помощника: сверка колонок в `internal/archrules` ищет именно эту форму, а
		// инвариант о колонках компилятор не проверяет.
		DurationMs: numberValue(record.GetInt("duration_ms")),
		SizeBytes:  numberValue(record.GetInt("size_bytes")),
		TopicIDs:   record.GetStringSlice("topics"),
		CreatedAt:  record.GetDateTime("created").Time(),
		UpdatedAt:  record.GetDateTime("updated").Time(),
	}
}

func derefString(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}

// dateOrEmpty отдаёт пустое значение вместо нулевой даты: пустая колонка даты в
// хранилище это пустая строка, и она же значит «времени нет».
func dateOrEmpty(v *time.Time) any {
	if v == nil || v.IsZero() {
		return ""
	}
	date, err := types.ParseDateTime(*v)
	if err != nil {
		return ""
	}
	return date
}

// numberOrZero отдаёт ноль вместо отсутствующего числа.
//
// «Неизвестно» числовая колонка хранилища не выражает вовсе: пустое значение она
// не держит и кладёт нулём. Отличимость потребовала бы четвёртой колонки-признака
// либо текстового типа у чисел, и платить за это нечем — обе величины ставит
// приём и ставит всегда. Решение владельца 2026-08-15.
func numberOrZero(v *int64) int64 {
	if v == nil {
		return 0
	}
	return *v
}

// numberValue читает колонку числом. Ноль здесь означает ноль — см. numberOrZero.
func numberValue(value int) *int64 {
	v := int64(value)
	return &v
}

func nilIfEmpty(v string) *string {
	if v == "" {
		return nil
	}
	return &v
}

func timeOrNil(v types.DateTime) *time.Time {
	if v.IsZero() {
		return nil
	}
	t := v.Time()
	return &t
}
