package worker

import (
	"context"
	"slices"
	"strings"
	"time"

	"git.vakhrushev.me/av/jellybit/internal/qbt"
	"git.vakhrushev.me/av/jellybit/internal/store"
)

// discover усыновляет новые раздачи: для каждого торрента с нашей категорией
// ИЛИ тегом, чьих хешей ещё нет в БД, заводит задачу downloading. Дальше
// её ведёт обычный reconcile. Вызывается под w.mu.
//
// Корректность при гонке с Ingest (другая горутина): и adopt, и приём идут
// через store.CreateDownloadIfNoActive — атомарный check-then-insert в одной
// write-транзакции; при столкновении второй участник получает существующую
// активную задачу и просто пропускает.
func (w *Worker) discover(ctx context.Context, torrents []qbt.Torrent) {
	for _, t := range torrents {
		if w.tracked(t) {
			w.adopt(ctx, t)
		}
	}
}

// tracked — относится ли торрент к jellybit (категория или тег из конфига).
func (w *Worker) tracked(t qbt.Torrent) bool {
	if w.cfg.Category != "" && t.Category == w.cfg.Category {
		return true
	}
	return hasTag(t.Tags, w.cfg.Tag)
}

// adopt заводит задачу под торрент, если его ещё не видели.
func (w *Worker) adopt(ctx context.Context, t qbt.Torrent) {
	hashes := torrentHashes(t)
	if len(hashes) == 0 {
		return // нечем идентифицировать (напр. ещё metaDL без хэша)
	}
	exists, err := w.store.ExistsByInfohash(ctx, hashes...)
	if err != nil {
		w.log.Warn("discover exists check failed", "capability", capIngest, "infohash", hashes[0], "error", err)
		return
	}
	if exists {
		return // уже усыновлён ранее (или обработан) — не трогаем
	}

	d := &store.Download{
		SourceType:  store.SourceMagnet,
		SourceRef:   magnetURN(hashes[0]),
		DisplayName: t.Name, // усыновление: приёма/rename нет, берём имя торрента из qBittorrent
		State:       store.StateDownloading,
	}
	existing, err := w.store.CreateDownloadIfNoActive(ctx, d, hashes, nil)
	if err != nil {
		w.log.Error("discover adopt failed", "capability", capIngest, "infohash", hashes[0], "error", err)
		return
	}
	if existing != nil {
		return // гонка с Ingest/другим тиком: задача уже заведена — всё ок
	}
	// Базис сортировки — время добавления в источник; у усыновлённого оно уже
	// известно (created_at задачи было бы моментом усыновления, не добавления).
	if t.AddedOn > 0 {
		if err := w.store.SetSourceAddedAt(ctx, d.ID, time.Unix(t.AddedOn, 0)); err != nil {
			w.log.Warn("adopt set source_added_at failed",
				"capability", capIngest, "download_id", d.ID, "error", err)
		}
	}
	w.log.Info("discover adopted torrent",
		"capability", capIngest, "download_id", d.ID, "infohash", hashes[0], "name", t.Name,
		"category", t.Category, "tags", t.Tags)
}

// hasTag сообщает, есть ли tag среди списка тегов qBit (через запятую).
func hasTag(tags, tag string) bool {
	if tag == "" {
		return false
	}
	for _, x := range strings.Split(tags, ",") {
		if strings.TrimSpace(x) == tag {
			return true
		}
	}
	return false
}

// torrentHashes — все непустые хеши торрента (нижний регистр, без дублей,
// v1-приоритетный порядок: v1 раньше v2). Единственный сборщик хешей
// торрента для записи в БД: t.Hash берётся только когда qBittorrent не
// отдал infohash_v1/v2 (старые версии API); у v2-only раздачи t.Hash — это
// УСЕЧЁННЫЙ до 40 hex v2-хеш, хранить его нельзя (по длине он неотличим от
// v1 и порождает битые btih-magnet при retry).
func torrentHashes(t qbt.Torrent) []string {
	cands := []string{t.InfohashV1, t.InfohashV2}
	if t.InfohashV1 == "" && t.InfohashV2 == "" {
		cands = append(cands, t.Hash)
	}
	var out []string
	for _, h := range cands {
		h = store.NormalizeHash(h)
		if h != "" && !slices.Contains(out, h) {
			out = append(out, h)
		}
	}
	return out
}

// torrentIndexHashes — хеши, под которыми раздачу индексируют lookup-мапы
// (byHash/live) и поиск по infohash. Зеркалит выбор torrentHashes: t.Hash берём
// только когда qBittorrent не отдал infohash_v1/v2 (старый API). У v2-only
// раздачи t.Hash — усечённый до 40 hex v2-хеш; индексировать его нельзя — по длине
// он неотличим от v1 и мог бы ложно сматчить задачу с таким же v1-хешем (NIT-11),
// хотя для этого нужна невозможная на практике 160-битная коллизия. В отличие от
// torrentHashes хеши не нормализует и не дедуплицирует — ключ приводит вызывающий.
func torrentIndexHashes(t qbt.Torrent) []string {
	if t.InfohashV1 == "" && t.InfohashV2 == "" {
		if t.Hash == "" {
			return nil
		}
		return []string{t.Hash}
	}
	out := make([]string, 0, 2)
	if t.InfohashV1 != "" {
		out = append(out, t.InfohashV1)
	}
	if t.InfohashV2 != "" {
		out = append(out, t.InfohashV2)
	}
	return out
}

// magnetURN — синтетический источник усыновлённой раздачи по её хешу:
// btih для v1, btmh (multihash sha256, префикс 1220) для v2. Хеш обязан
// быть полноразмерным (torrentHashes усечённые не отдаёт).
func magnetURN(h string) string {
	if store.HashKind(h) == store.HashV2 {
		return "magnet:?xt=urn:btmh:1220" + h
	}
	return "magnet:?xt=urn:btih:" + h
}
