package sqlite

import (
	"context"
	"errors"
	"fmt"
	"io"

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

// payloadSuffix — окончание имени файла, под которым лежит сохранённый ответ
// провайдера. Имя задаёт сервис, как и у копий аудио.
const payloadSuffix = ".payload"

type RecognitionRepository struct {
	db    *DB
	store *Store
}

func NewRecognitionRepository(db *DB, store *Store) *RecognitionRepository {
	return &RecognitionRepository{db: db, store: store}
}

// Create заводит строку попытки **до** обращения к провайдеру.
//
// Порядок здесь несущий: окно между ответом провайдера и записью идентификатора
// операции — то место, где теряется оплаченное. Заведённая заранее строка даёт
// повторному шагу, чем проверить сделанное прежде, чем платить второй раз.
func (repo *RecognitionRepository) Create(r *entity.Recognition) error {
	started := clock.Now()
	if r.Id == "" {
		r.Id = ident.New()
	}

	now := formatTime(started)
	_, err := repo.db.Writer().ExecContext(context.Background(),
		`INSERT INTO recognitions
		    (id, record_id, provider, model, external_id, source_uri, started_at, created_at, updated_at)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		r.Id, r.RecordID, r.Provider, r.Model, r.ExternalID, r.SourceURI, now, now, now,
	)
	if err != nil {
		return fmt.Errorf("failed to create recognition attempt for record %s: %w", r.RecordID, err)
	}

	r.StartedAt = &started
	return nil
}

// Submitted сохраняет адрес аудио и идентификатор заведённой операции. По
// последнему повторный шаг узнаёт, что за эту запись уже заплачено, и второй раз
// наружу не платит.
func (repo *RecognitionRepository) Submitted(id, sourceURI, externalID string) error {
	_, err := repo.db.Writer().ExecContext(context.Background(),
		"UPDATE recognitions SET source_uri = ?, external_id = ?, updated_at = ? WHERE id = ?",
		sourceURI, externalID, formatTime(clock.Now()), id,
	)
	if err != nil {
		return fmt.Errorf("failed to store operation id of attempt %s: %w", id, err)
	}
	return nil
}

// Finish кладёт сохранённый ответ провайдера **третьим файлом в подкаталоге
// записи** и отмечает завершение попытки.
//
// Файлом, а не колонкой: шаг опроса читает эту строку раз в несколько секунд, и
// ответ на многочасовую запись, положенный колонкой, ехал бы в память при каждом
// опросе. Хранится он потому, что результат операции у провайдера не
// переспрашивается.
//
// Пустой ответ поверх сохранённого не кладётся — тем же доводом, что и у текста:
// повторный опрос вправе вернуть пустое, и безусловная замена стёрла бы
// сохранённое без возврата.
func (repo *RecognitionRepository) Finish(id string, raw []byte) error {
	attempt, err := repo.GetByID(id)
	if err != nil {
		return err
	}

	name := id + payloadSuffix
	if len(raw) > 0 {
		if _, err := repo.store.Put(attempt.RecordID, name, bytesReader(raw)); err != nil {
			// Путь к сохранённому ответу наружу не идёт: отказ называет попытку
			// её идентификатором.
			return errors.Join(fmt.Errorf("failed to store provider payload of attempt %s", id), err)
		}
	}

	finished := formatTime(clock.Now())
	if len(raw) > 0 {
		_, err = repo.db.Writer().ExecContext(context.Background(),
			"UPDATE recognitions SET payload_file = ?, finished_at = ?, updated_at = ? WHERE id = ?",
			name, finished, finished, id,
		)
	} else {
		_, err = repo.db.Writer().ExecContext(context.Background(),
			"UPDATE recognitions SET finished_at = ?, updated_at = ? WHERE id = ?",
			finished, finished, id,
		)
	}
	if err != nil {
		return fmt.Errorf("failed to store provider payload of attempt %s", id)
	}

	return nil
}

func (repo *RecognitionRepository) GetByID(id string) (*entity.Recognition, error) {
	attempt := &entity.Recognition{Id: id}
	var startedAt, finishedAt, payloadFile nullString
	err := repo.db.Reader().QueryRowContext(context.Background(),
		`SELECT record_id, provider, model, external_id, source_uri, payload_file, started_at, finished_at
		   FROM recognitions WHERE id = ?`, id,
	).Scan(
		&attempt.RecordID, &attempt.Provider, &attempt.Model,
		&attempt.ExternalID, &attempt.SourceURI, &payloadFile,
		&startedAt, &finishedAt,
	)
	if err != nil {
		return nil, fmt.Errorf("failed to get recognition attempt %s: %w", id, err)
	}

	attempt.StartedAt = timeOf(startedAt.NullString)
	attempt.FinishedAt = timeOf(finishedAt.NullString)

	return attempt, nil
}

// ReadRaw отдаёт сохранённый ответ провайдера. Зовётся только тогда, когда ответ
// нужен: шаг опроса читает строку попытки без него.
func (repo *RecognitionRepository) ReadRaw(id string) ([]byte, error) {
	attempt, err := repo.GetByID(id)
	if err != nil {
		return nil, err
	}

	var payloadFile string
	if err := repo.db.Reader().QueryRowContext(context.Background(),
		"SELECT payload_file FROM recognitions WHERE id = ?", id,
	).Scan(&payloadFile); err != nil {
		return nil, fmt.Errorf("failed to get recognition attempt %s: %w", id, err)
	}
	if payloadFile == "" {
		return nil, fmt.Errorf("recognition attempt %s has no stored payload", id)
	}

	file, err := repo.store.Open(attempt.RecordID, payloadFile)
	if err != nil {
		return nil, err
	}
	defer func() { _ = file.Close() }()

	raw, err := io.ReadAll(file)
	if err != nil {
		return nil, fmt.Errorf("failed to read stored payload of attempt %s", id)
	}

	return raw, nil
}
