package contract

import (
	"io"
	"time"

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

// WorkFile — рабочая копия файла на диске: её просят шаги, отдающие файл
// внешней программе, потому что `ffmpeg` и `ffprobe` принимают имя аргументом.
//
// Заводится копия одним способом — репозиторием файлов, — и убирает её за собой
// Close. Каждый шаг, заводящий копию сам, повторял бы и обязанность прибрать, а
// забытая копия это шестичасовая запись во временном каталоге, о которой не
// узнает никто.
type WorkFile interface {
	// Path — имя копии на диске, годное для внешней программы.
	Path() string
	// Size — длина копии в байтах на момент вызова.
	Size() (int64, error)
	// Close убирает копию. Зовётся на любом исходе, включая отказ.
	Close() error
}

// FileMeta — что известно о копии сверх её содержимого.
type FileMeta struct {
	// Format — расширение без точки, в нижнем регистре.
	Format string
	// DurationMs — длительность, если её удалось прочитать.
	DurationMs int64
}

type FileRepository interface {
	// Stage принимает содержимое потоком в рабочую копию с заданным
	// расширением: по нему внешняя программа выбирает разбор. В память запись
	// целиком не читается — расчётный потолок шесть часов.
	Stage(ext string, content io.Reader) (WorkFile, error)
	// StageEmpty заводит пустую рабочую копию с заданным расширением — под
	// результат внешней программы, которая пишет по имени.
	StageEmpty(ext string) (WorkFile, error)
	// Localize выдаёт рабочую копию хранимого файла.
	Localize(fileID string) (WorkFile, error)
	// Create кладёт рабочую копию в каталог данных под именем name и заводит
	// строку о файле. Имя задаёт сервис, и имя, данное отправителем, в него не
	// попадает: от него взято только расширение.
	//
	// recordID — запись, которой копия принадлежит: копии одной записи лежат её
	// подкаталогом, и имя этого подкаталога и есть идентификатор записи. Приём
	// знает его раньше, чем кладёт файл, потому что назначает сам.
	//
	// ownerID — владелец записи, которой файл принадлежит, и он обязателен:
	// колонка владельца пустого значения не принимает, пустой отвергается
	// схемой. Владелец лежит своей колонкой, а не выводится через запись: файл
	// переживает свою запись — шаг заводит его до сохранения, и потерянный
	// захват оставляет файл с владельцем и без ссылки.
	Create(recordID, name string, work WorkFile, meta FileMeta, ownerID string) (*entity.File, error)
	GetByID(id string) (*entity.File, error)
	// Open отдаёт содержимое хранимого файла потоком с перемоткой: отдача по
	// диапазону читает запрошенный кусок, а не файл целиком.
	Open(fileID string) (io.ReadSeekCloser, error)
}

// AcquiredRecord — то, что отдаёт захват: идентификатор записи и признак
// **этого** захвата.
//
// Перечня колонок здесь нет намеренно. Захват, возвращавший колонки поимённо,
// требовал править их в четырёх местах сразу, и забытая колонка приезжала
// нулевой, а первое же сохранение писало этот ноль поверх значения. Колонки шаг
// читает обычным чтением.
type AcquiredRecord struct {
	ID string
	// Holder — значение, уникальное для каждого захвата. Запись результата
	// условна по нему, а не по занятости записи: захват, перевыданный другому по
	// протуханию срока или после снятия остановки человеком, обязан обратить
	// запись первого в отказ.
	Holder string
}

// RecordCursor — положение в ленте записей, заданное **полным** ключом
// сортировки. Одного времени мало: у записей, принятых одним запросом, оно
// совпадает, и порядок между ними иначе не определён.
//
// Время лежит здесь значением времени, а не строкой: вид, каким оно уходит в
// запрос, принадлежит хранилищу — сравнение там побайтово, и вид, собранный
// транспортом, разошёлся бы с колонкой молча, обратив условие в постоянную ложь.
type RecordCursor struct {
	CreatedAt time.Time
	ID        string
}

// RecordQuery — что спрашивают у ленты записей.
type RecordQuery struct {
	// OwnerID обязателен: пустой не совпадает ни с одной записью.
	OwnerID string
	// Filter — состояние записи. Пустой значит «все».
	Filter *entity.ListFilter
	// Cursor — положение, с которого продолжать. Пустой значит «сначала».
	Cursor *RecordCursor
	Limit  int
}

// RecordPage — страница ленты. Ключ следующей страницы пуст, когда страница
// последняя.
type RecordPage struct {
	Items      []*entity.AudioRecord
	NextCursor *RecordCursor
	TotalItems int
}

type AudioRecordRepository interface {
	Create(record *entity.AudioRecord) error
	// List отдаёт страницу записей владельца, новыми сверху, не читая ни
	// расшифровки, ни структуры реплик.
	List(q RecordQuery) (*RecordPage, error)
	// ResolveTopicNames разрешает темы названиями одним запросом на страницу и
	// сужает их владельцем: словарь тем свой у каждого человека.
	ResolveTopicNames(ownerID string, ids []string) (map[string]string, error)
	// Save сохраняет запись, захват которой держит holder. Захват, доставшийся
	// за время работы другому, даёт LostAcquisitionError и запись не проводит.
	// Пустой holder снимает эту условность и в конвейере не употребляется: все
	// его шаги получают признак захвата от FindAndAcquire.
	Save(record *entity.AudioRecord, holder string) error
	// GetByID отдаёт запись, только если её владелец — ownerID. Чужая запись,
	// ничья и несуществующая дают одну и ту же ошибку: по разнице ответов иначе
	// перебирается список заведённых записей.
	//
	// Владелец здесь обязателен, и пустой ownerID не совпадает ни с чем —
	// включая записи без владельца. Правило записано со стороны спрашивающего:
	// обязательность, которую держит одна лишь подпись метода, пустую строку
	// пропускает.
	GetByID(id, ownerID string) (*entity.AudioRecord, error)
	// Get отдаёт запись без сужения владельцем: им пользуется конвейер, чья
	// выборка владельцем не сужается.
	Get(id string) (*entity.AudioRecord, error)
	// FindAndAcquire забирает пригодную к работе запись одним неделимым шагом и
	// увеличивает число её отказов. Отбор идёт по рабочим рубежам, паузе, сроку
	// протухания захвата и отсутствию признака остановки; срок протухания
	// приезжает с рубежом и пишется в саму запись.
	//
	// Работы нет — JobNotFoundError.
	FindAndAcquire(stages []entity.Stage) (*AcquiredRecord, error)
}

// TextRepository — тексты записи. Пара «запись и вид» уникальна: повтор
// прерванного шага не заводит второй строки.
type TextRepository interface {
	Put(recordID, kind, contents string) (*entity.Text, error)
	GetByID(id string) (*entity.Text, error)
}

// StructureRepository — структура реплик записи. Пара «запись и версия разбора»
// уникальна по той же причине.
type StructureRepository interface {
	Put(recordID string, version int, replicas []entity.Replica) (*entity.Structure, error)
	GetByID(id string) (*entity.Structure, error)
}

// RecognitionRepository — попытка распознавания у внешнего провайдера.
type RecognitionRepository interface {
	// Create заводит строку попытки **до** обращения к провайдеру: окно между
	// его ответом и записью идентификатора — то место, где теряется оплаченное.
	Create(recognition *entity.Recognition) error
	// Submitted сохраняет адрес аудио и идентификатор заведённой операции. По
	// последнему повторный шаг узнаёт, что за эту запись уже заплачено.
	Submitted(id, sourceURI, externalID string) error
	// Finish отмечает завершение операции и кладёт сохранённый ответ отдельным
	// файлом в подкаталоге записи.
	Finish(id string, raw []byte) error
	GetByID(id string) (*entity.Recognition, error)
	// ReadRaw отдаёт сохранённый ответ провайдера. Зовётся только тогда, когда
	// ответ нужен: шаг опроса читает строку попытки без него.
	ReadRaw(id string) ([]byte, error)
}

// RecordEventRepository — журнал событий записи.
type RecordEventRepository interface {
	Append(event *entity.RecordEvent) error
}

// Identity — то, чем доверенный источник называет пришедшего.
//
// Логин — ключ учётной записи, остальное берётся только при её заведении.
type Identity struct {
	Login string
	Name  string
	Email string
}

// UserAccount — учётная запись сервиса, какой её видит транспорт: ключ и имя,
// пригодное к показу. Логина у провайдера и адреса почты здесь нет: оба
// принадлежат человеку, а не сервису, и наружу не выходят.
type UserAccount struct {
	ID   string
	Name string
}

// UserRepository — учётные записи.
//
// Дом правила «найти по логину, а не найдя — завести» один, и он в хранилище, а
// не в транспорте: второй способ представиться возьмёт этот же метод.
type UserRepository interface {
	// EnsureUser находит учётную запись по логину у провайдера, а не найдя —
	// заводит её. Второе значение истинно только у заведённой: заведение —
	// событие, и владелец обязан видеть его строкой журнала.
	EnsureUser(identity Identity) (account *UserAccount, created bool, err error)
}
