package sqlite

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"

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

// workFile — рабочая копия файла на диске. Живёт во временном каталоге системы,
// а не в каталоге данных: последний смонтирован на сервере, и временному там не
// место.
type workFile struct {
	path string
}

func (w *workFile) Path() string { return w.path }

func (w *workFile) Size() (int64, error) {
	info, err := os.Stat(w.path)
	if err != nil {
		return 0, fmt.Errorf("failed to stat work file: %w", err)
	}
	return info.Size(), nil
}

// Close убирает копию. Отсутствие файла отказом не считается: шаг мог не дойти
// до его создания, и повторный Close тоже законен.
func (w *workFile) Close() error {
	if err := os.Remove(w.path); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to remove work file: %w", err)
	}
	return nil
}

// FileRepository — копии записей: строка в базе и содержимое в каталоге данных.
type FileRepository struct {
	db    *DB
	store *Store
}

func NewFileRepository(db *DB, store *Store) *FileRepository {
	return &FileRepository{db: db, store: store}
}

// newWorkFile заводит пустую копию во временном каталоге. Расширение сохраняется
// в имени: `ffprobe` и `ffmpeg` по нему выбирают разбор.
func newWorkFile(ext string) (*workFile, error) {
	f, err := os.CreateTemp("", "transcriber-*"+ext)
	if err != nil {
		return nil, fmt.Errorf("failed to create work file: %w", err)
	}
	path := f.Name()
	if err := f.Close(); err != nil {
		_ = os.Remove(path)
		return nil, fmt.Errorf("failed to close work file: %w", err)
	}
	return &workFile{path: path}, nil
}

func (repo *FileRepository) StageEmpty(ext string) (contract.WorkFile, error) {
	return newWorkFile(ext)
}

func (repo *FileRepository) Stage(ext string, content io.Reader) (contract.WorkFile, error) {
	work, err := newWorkFile(ext)
	if err != nil {
		return nil, err
	}

	if err := writeTo(work.path, content); err != nil {
		// Отказ уборки не подменяет отказ записи, но и не теряется.
		return nil, errors.Join(err, work.Close())
	}

	return work, nil
}

func (repo *FileRepository) Localize(fileID string) (contract.WorkFile, error) {
	file, err := repo.GetByID(fileID)
	if err != nil {
		return nil, err
	}

	work, err := newWorkFile(filepath.Ext(file.FileName))
	if err != nil {
		return nil, err
	}

	src, err := repo.store.Open(file.RecordID, file.FileName)
	if err != nil {
		return nil, errors.Join(err, work.Close())
	}
	defer func() { _ = src.Close() }()

	if err := writeTo(work.path, src); err != nil {
		return nil, errors.Join(err, work.Close())
	}

	return work, nil
}

// Create кладёт рабочую копию в каталог данных и заводит строку о файле.
//
// Порядок один: строка заводится **после** того, как содержимое лежит целиком
// под рабочим именем. Обратный порядок оставлял бы в базе строку, указывающую на
// файл, которого ещё нет или который короче принятого.
//
// Отсюда и уборка: содержимое легло, а строка не сохранилась — уложенный файл
// убирается, и следа от него не остаётся. Файл, переживший свою строку, —
// штатное состояние только у приведённой копии, которую заводит шаг конвейера; у
// принятой это мусор, на который не ссылается ничто и о котором узнать неоткуда.
//
// Владелец обязателен и лежит своей колонкой: пустой отвергает схема — колонка
// объявлена связью с учётной записью, и пустое значение ей не отвечает.
func (repo *FileRepository) Create(
	recordID, name string,
	work contract.WorkFile,
	meta contract.FileMeta,
	ownerID string,
) (*entity.File, error) {
	source, err := os.Open(work.Path())
	if err != nil {
		// Причина сохраняется, путь снимается: он ведёт к рабочей копии чужого
		// аудио, а отказ кончается в журнале.
		return nil, fmt.Errorf("failed to read work file: %w", causeOf(err))
	}

	size, putErr := repo.store.Put(recordID, name, source)
	closeErr := source.Close()
	if err := errors.Join(putErr, closeErr); err != nil {
		return nil, err
	}

	file := &entity.File{
		Id:         ident.New(),
		RecordID:   recordID,
		FileName:   name,
		Size:       size,
		Format:     meta.Format,
		DurationMs: meta.DurationMs,
		CreatedAt:  clock.Now(),
	}

	query, args := insertSQL("files", map[string]any{
		"id":          file.Id,
		"owner_id":    ownerID,
		"record_id":   file.RecordID,
		"file_name":   file.FileName,
		"size_bytes":  file.Size,
		"format":      file.Format,
		"duration_ms": file.DurationMs,
		"created_at":  formatTime(file.CreatedAt),
	})

	if _, err := repo.db.Writer().ExecContext(context.Background(), query, args...); err != nil {
		// Уложенное содержимое убирается: строки о нём не будет, и ссылаться на
		// него нечему. Имя файла в отказ не идёт — оно часть пути к чужому аудио.
		return nil, errors.Join(
			fmt.Errorf("failed to store the file row of record %s: %w", recordID, err),
			repo.store.Remove(recordID, name),
		)
	}

	return file, nil
}

func (repo *FileRepository) GetByID(id string) (*entity.File, error) {
	file := &entity.File{}
	var (
		createdAt  string
		recordID   string
		fileName   string
		size       int64
		format     string
		durationMs int64
	)

	err := repo.db.Reader().QueryRowContext(context.Background(),
		`SELECT record_id, file_name, size_bytes, format, duration_ms, created_at
		   FROM files WHERE id = ?`, id,
	).Scan(&recordID, &fileName, &size, &format, &durationMs, &createdAt)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, fmt.Errorf("file %s is not found", id)
		}
		return nil, fmt.Errorf("failed to get file %s: %w", id, err)
	}

	file.Id = id
	file.RecordID = recordID
	file.FileName = fileName
	file.Size = size
	file.Format = format
	file.DurationMs = durationMs
	file.CreatedAt = requiredTimeOf(createdAt)

	return file, nil
}

// Open отдаёт содержимое хранимой копии потоком с перемоткой: отдача по
// диапазону читает кусок, а не файл целиком.
func (repo *FileRepository) Open(fileID string) (io.ReadSeekCloser, error) {
	file, err := repo.GetByID(fileID)
	if err != nil {
		return nil, err
	}
	return repo.store.Open(file.RecordID, file.FileName)
}

// writeTo переливает содержимое в файл потоком. В память запись целиком не
// читается: расчётный потолок — шесть часов.
func writeTo(path string, content io.Reader) error {
	dst, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("failed to open work file: %w", err)
	}

	if _, err := io.Copy(dst, content); err != nil {
		_ = dst.Close()
		return fmt.Errorf("failed to write work file: %w", err)
	}

	if err := dst.Close(); err != nil {
		return fmt.Errorf("failed to close work file: %w", err)
	}

	return nil
}
