package pocketbase

import (
	"fmt"

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

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

// GuardOwnerDeletion отвергает удаление учётной записи, у которой остались
// аудиозаписи, их файлы либо темы её словаря.
//
// Колонка владельца — связь с выключенным каскадным удалением, и одного этого
// мало: при выключенном каскаде хранилище не удаляет ссылающуюся запись, а
// **вынимает** идентификатор из поля связи и сохраняет её без проверок. Задачи
// остались бы на месте, но стали бы ничьими, а ничья задача не достаётся по API
// никому — архив человека исчез бы молча и восстановлению не подлежал:
// прежнего владельца не остаётся нигде.
//
// Цена запрета названа прямо: владелец панели упирается в отказ, а способа
// удалить записи в сервисе пока нет вовсе — его приносит отдельная задача. До
// неё удаление учётной записи с записями невозможно, и это осознанный тупик.
//
// Слой стоит на удалении записи, а не на запросе к панели: панель ходит правами
// суперпользователя, и правило коллекции её не судит. Удаление при этом не
// только панельное — умолчание библиотеки разрешает вошедшему удалить свою
// учётную запись запросом, так что страж закрывает и публичную поверхность.
//
// Считаются **все** коллекции с владельцем, и перечень их живёт одним списком
// ниже. Файл переживает свою запись: шаг конвейера заводит его до сохранения, и
// потерянный захват оставляет файл с владельцем и без ссылки. Учётная запись, у
// которой остались одни такие файлы, без этого счёта удалялась бы штатно, а
// аудио становилось бы ничьим.
func GuardOwnerDeletion(app core.App) {
	app.OnRecordDelete(migrations.UsersCollection).BindFunc(func(e *core.RecordEvent) error {
		count, err := countOwned(e.App, e.Record.Id)
		if err != nil {
			return err
		}

		if count > 0 {
			// Отказ отдаётся ошибкой роутера, а не обычной: библиотека пропускает
			// наружу только `*router.ApiError`, а всякую другую подменяет своим
			// сообщением — «убедитесь, что запись не участвует в обязательной
			// связи». Подсказка эта не просто бесполезная, а **ведущая**:
			// единственная обязательная связь у задачи — файл, и владелец панели,
			// поверив ей, пойдёт удалять задачи и файлы руками. То есть сделает
			// ровно то необратимое, ради предотвращения чего страж и заведён.
			//
			// Число в отказе — не содержимое записей, а их счёт: он говорит
			// владельцу панели, почему удаление не прошло, и не выносит наружу
			// ничего о самих записях.
			return router.NewBadRequestError(fmt.Sprintf(
				"у учётной записи остались записи (%d): сервис — архив, и удаление сделало бы их ничьими",
				count,
			), nil)
		}

		return e.Next()
	})
}

// ownedCollections — коллекции с колонкой владельца. Перечень живёт здесь одним
// списком, и разойтись с шагом схемы ему нельзя: пропущенная коллекция
// пропускает удаление вперёд, а наружу приезжает не наш отказ с причиной, а
// подсказка библиотеки про обязательную связь — та самая, по которой владелец
// панели пойдёт удалять записи руками.
//
// Так уже случилось однажды: `topics` завелась третьей и в списке не появилась.
var ownedCollections = []string{
	migrations.RecordsCollection,
	migrations.FilesCollection,
	migrations.TopicsCollection,
}

// countOwned считает всё, что принадлежит учётной записи, — по всем коллекциям
// с колонкой владельца.
func countOwned(app core.App, ownerID string) (int64, error) {
	var total int64

	for _, collection := range ownedCollections {
		count, err := app.CountRecords(collection, dbx.HashExp{"owner": ownerID})
		if err != nil {
			return 0, fmt.Errorf("failed to count owned records in %s: %w", collection, err)
		}
		total += count
	}

	return total, nil
}
