package main

import (
	"context"
	"flag"
	"fmt"
	"log/slog"
	"os"
	"strings"
	"time"

	"git.vakhrushev.me/av/jellybit/internal/config"
	"git.vakhrushev.me/av/jellybit/internal/layout"
	"git.vakhrushev.me/av/jellybit/internal/llm"
	"git.vakhrushev.me/av/jellybit/internal/metadata"
	"git.vakhrushev.me/av/jellybit/internal/qbt"
	"git.vakhrushev.me/av/jellybit/internal/recognize"
	"git.vakhrushev.me/av/jellybit/internal/worker"
)

// runRecognize — диагностика распознавания: берёт торрент из qBittorrent по
// infohash, прогоняет конвейер (LLM + метабазы) и печатает план раскладки.
// Только чтение: ни записи в БД, ни хардлинков.
func runRecognize(args []string) error {
	fs := flag.NewFlagSet("recognize", flag.ContinueOnError)
	configPath := fs.String("config", config.DefaultPath, "путь к config.toml")
	dryRun := fs.Bool("dry-run", true, "только показать план, без изменений (единственный режим)")
	contextStr := fs.String("context", "", "доп. текстовый контекст для распознавания")
	if err := fs.Parse(args); err != nil {
		return err
	}
	infohash := strings.ToLower(strings.TrimSpace(fs.Arg(0)))
	// flag останавливается на первом позиционном — допарсим флаги, стоящие
	// после <infohash> (напр. `recognize <infohash> --dry-run`).
	if fs.NArg() > 1 {
		if err := fs.Parse(fs.Args()[1:]); err != nil {
			return err
		}
	}
	if infohash == "" {
		return fmt.Errorf("usage: jellybit recognize <infohash> [--dry-run] [--context ...]")
	}
	if !*dryRun {
		return fmt.Errorf("recognize runs only in --dry-run mode; layout is applied via review")
	}

	cfg, err := config.Load(*configPath)
	if err != nil {
		return err
	}
	if cfg.LLM.Type == "" || cfg.LLM.BaseURL == "" {
		return fmt.Errorf("[llm] is not configured in config — recognition is unavailable")
	}
	// Внутренние логи (ретраи/ошибки провайдеров) — в stderr, чтобы не мешать
	// плану в stdout.
	logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	// qBittorrent: ищем торрент и его файлы.
	qb, err := qbt.New(qbt.Config{
		URL:      cfg.QBittorrent.URL,
		Username: cfg.QBittorrent.Username,
		Password: cfg.QBittorrent.Password,
	}, logger)
	if err != nil {
		return err
	}
	torrents, err := qb.Torrents(ctx, "")
	if err != nil {
		return fmt.Errorf("qbittorrent: %w", err)
	}
	t, ok := findTorrent(torrents, infohash)
	if !ok {
		return fmt.Errorf("torrent with infohash %s not found in qBittorrent", infohash)
	}
	files, err := qb.Files(ctx, t.Hash)
	if err != nil {
		return fmt.Errorf("qbittorrent files: %w", err)
	}

	// Провайдеры метаданных + LLM + распознаватель.
	providers, err := metadataProviders(cfg, logger)
	if err != nil {
		return err
	}
	provider, err := llm.New(llm.Config{
		Type: cfg.LLM.Type, BaseURL: cfg.LLM.BaseURL, APIKey: cfg.LLM.APIKey,
		Model: cfg.LLM.Model, Proxy: cfg.LLM.Proxy, Timeout: cfg.LLM.Timeout.Std(),
	}, logger)
	if err != nil {
		return err
	}
	rec := recognize.New(provider, providers, recognize.Config{
		MaxRetries:    cfg.LLM.MaxRetries,
		AutoThreshold: cfg.Recognition.AutoConfidenceThreshold,
		Language:      cfg.ContentLanguage(),
	}, logger)

	in := recognize.Input{Name: t.Name, Context: *contextStr}
	for _, f := range files {
		in.Files = append(in.Files, recognize.File{Path: f.Name, Size: f.Size})
	}

	start := time.Now()
	res, err := rec.Recognize(ctx, in)
	if err != nil {
		return fmt.Errorf("recognize: %w", err)
	}

	// Раскладчик для превью (BuildLinks ничего не пишет; логгер не нужен).
	lay, err := layout.New(layout.Config{MoviesDir: cfg.Paths.Movies, SeriesDir: cfg.Paths.Series}, nil)
	if err != nil {
		return err
	}
	printDryRun(t, files, res, lay, providerNames(providers), time.Since(start))
	return nil
}

func findTorrent(torrents []qbt.Torrent, infohash string) (qbt.Torrent, bool) {
	for _, t := range torrents {
		for _, h := range []string{t.Hash, t.InfohashV1, t.InfohashV2} {
			if h != "" && strings.EqualFold(h, infohash) {
				return t, true
			}
		}
	}
	return qbt.Torrent{}, false
}

func printDryRun(t qbt.Torrent, files []qbt.File, res recognize.Result, lay *layout.Layouter, providers []string, took time.Duration) {
	p := res.Plan
	fmt.Printf("════ Торрент ════\n")
	fmt.Printf("name      : %s\n", t.Name)
	fmt.Printf("infohash  : %s\n", t.Hash)
	fmt.Printf("save_path : %s\n", t.SavePath)
	fmt.Printf("файлов    : %d   state: %s\n\n", len(files), t.State)

	fmt.Printf("════ Распознавание ════\n")
	fmt.Printf("провайдеры базы: %v\n", providers)
	fmt.Printf("заняло         : %s, попыток LLM: %d\n", took.Truncate(time.Millisecond), res.Attempts)
	fmt.Printf("тип            : %s\n", p.Type)
	fmt.Printf("название       : %s", p.Title)
	if p.OriginalTitle != "" {
		fmt.Printf("  (ориг: %s)", p.OriginalTitle)
	}
	fmt.Printf("\nгод            : %d\n", p.Year)
	fmt.Printf("self-confidence: %.2f\n", p.Confidence)
	if p.Notes != "" {
		fmt.Printf("notes          : %s\n", p.Notes)
	}

	fmt.Printf("\n──── Матч в базе ────\n")
	if m := res.Match; m != nil {
		fmt.Printf("provider=%s id=%s title=%q year=%d\n", m.Provider, m.ProviderID, m.Title, m.Year)
		if len(m.SeasonEpisodeCounts) > 0 {
			fmt.Printf("серий по сезонам в базе: %v\n", m.SeasonEpisodeCounts)
		}
	} else {
		fmt.Printf("единичного сильного матча нет\n")
	}
	if len(res.Candidates) > 0 {
		fmt.Printf("кандидаты для ручного выбора (%d):\n", len(res.Candidates))
		for _, c := range res.Candidates {
			tagP, tagID := recognize.CandidateTag(c)
			fmt.Printf("  · %s/%s  %q (%d)  [тег: %s-%s]\n", c.Provider, c.ID, c.Title, c.Year, tagP, tagID)
		}
	}

	fmt.Printf("\n──── Решение ────\n")
	if res.Decision.Auto {
		fmt.Printf("АВТО-раскладка (review не нужен)\n")
	} else {
		fmt.Printf("REVIEW — причины:\n")
		for _, reason := range res.Decision.Reasons {
			fmt.Printf("  · %s\n", reason)
		}
	}

	fmt.Printf("\n──── Превью раскладки (хардлинки НЕ создаются) ────\n")
	tag := ""
	if res.Match != nil {
		tag = worker.ProviderTag(res.Match.Provider, res.Match.ProviderID)
	}
	links, err := lay.BuildLinks(worker.ToLayoutPlan(p, t.SavePath, tag))
	if err != nil {
		fmt.Printf("план не построился: %v\n", err)
		return
	}
	for _, l := range links {
		fmt.Printf("  [%s] %s\n", l.Kind, l.Dst)
	}
	fmt.Printf("\nИтого ссылок: %d (это создалось бы при Apply)\n", len(links))
}

func providerNames(providers []metadata.Provider) []string {
	out := make([]string, len(providers))
	for i, p := range providers {
		out[i] = p.Name()
	}
	return out
}
