package naming

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"git.vakhrushev.me/av/jellybit/internal/llm"
	"git.vakhrushev.me/av/jellybit/internal/logctx"
)

// systemPrompt инструктирует модель вытащить из вольного контекста короткое
// имя. Язык названия выбирается по происхождению контента.
const systemPrompt = `Ты извлекаешь из текста о торрент-раздаче данные для короткого названия.
Верни СТРОГО один JSON-объект, без markdown-ограждений и пояснений.

Схема:
{
  "type": "movie" | "series",
  "title": "название на нужном языке",
  "original_title": "оригинальное название или пустая строка",
  "year": число или 0,
  "director": "режиссёр или пустая строка",
  "season": число или null,
  "is_russian": true | false
}

Правила:
- Определи, фильм это или сериал ("type").
- "is_russian" = true, если это российский/советский фильм или сериал.
- "title": для российского контента — русское название; иначе — английское
  (оригинальное). Без года, страны и технических характеристик.
- "year" — год выпуска (0, если не ясен). "director" — режиссёр (пустая
  строка, если не указан). "season" — номер сезона для сериала (null для
  фильма или если сезон не указан).
- Если данных не хватает, заполняй что можешь; "title" должен быть непустым.`

// extractViaLLM делает до n.attempts попыток получить валидную структуру.
// Транспортные ошибки провайдера (сеть/429/5xx) гасятся внутри llm.Provider;
// здесь повторяем только переразбор невалидного ответа. ok=false, если за
// все попытки валидного результата не получено.
func (n *Namer) extractViaLLM(ctx context.Context, contextText, hint string) (Fields, bool) {
	contextText = strings.TrimSpace(contextText)
	if contextText == "" {
		return Fields{}, false
	}

	user := "Контекст раздачи:\n" + contextText
	if hint = strings.TrimSpace(hint); hint != "" {
		user += "\n\nИмя из magnet (подсказка, может быть мусором): " + hint
	}

	temp := 0.0
	msgs := []llm.Message{
		{Role: llm.RoleSystem, Content: systemPrompt},
		{Role: llm.RoleUser, Content: user},
	}

	log := logctx.FromOr(ctx, n.log)
	for attempt := 1; attempt <= n.attempts; attempt++ {
		resp, err := n.provider.Complete(ctx, llm.Request{
			Messages:    msgs,
			JSONMode:    true,
			Temperature: &temp,
		})
		if err != nil {
			// Транспортная ошибка/таймаут залогирована клиентом LLM (ext.*);
			// здесь — доменное решение «уходим в фолбек, приём не валим».
			log.Debug("naming llm failed, using fallback")
			return Fields{}, false
		}

		ex, perr := parseExtracted(resp.Content)
		if perr == nil {
			return ex, true
		}
		log.Warn("naming llm response unparsed", "attempt", attempt, "error", perr)
		msgs = append(msgs,
			llm.Message{Role: llm.RoleAssistant, Content: resp.Content},
			llm.Message{Role: llm.RoleUser, Content: "Ответ невалиден: " + perr.Error() +
				". Верни строго один JSON-объект по схеме, без пояснений."})
	}
	return Fields{}, false
}

// parseExtracted вытаскивает JSON-объект из ответа и валидирует минимум:
// непустой title. Тип по умолчанию — movie.
func parseExtracted(raw string) (Fields, error) {
	jsonStr, err := llm.ExtractJSONObject(raw)
	if err != nil {
		return Fields{}, err
	}
	var ex Fields
	if err := json.Unmarshal([]byte(jsonStr), &ex); err != nil {
		return Fields{}, fmt.Errorf("naming: unmarshal: %w", err)
	}
	if strings.TrimSpace(ex.Title) == "" {
		return Fields{}, fmt.Errorf("naming: empty title")
	}
	if ex.Type != typeMovie && ex.Type != typeSeries {
		ex.Type = typeMovie
	}
	return ex, nil
}
