"""Use case : génération d'une page LoreMind à partir d'un contexte métier.

Couche APPLICATION — au-dessus du domaine, en-dessous de l'infra web.
Orchestre le flux : contexte → prompt → appel LLM → parsing JSON → résultat.

Ne dépend que des abstractions du domaine (port `LLMProvider`). C'est ce qui
permet de tester ce use case avec un FakeLLMProvider, sans Ollama qui tourne.
"""
import json

from app.domain.models import PageGenerationContext, PageGenerationResult
from app.domain.ports import LLMProvider, LLMProviderError


# Température basse : remplissage de champs = tâche factuelle, peu créative.
# Une valeur trop haute (par défaut Ollama = 0.8) encourage l'IA à broder
# et à inventer des références à des PNJ/lieux/événements inexistants.
_DEFAULT_TEMPERATURE = 0.4


_SYSTEM_INSTRUCTIONS = """Tu es un assistant d'écriture pour un Maître de Jeu de JDR.
Tu vas générer le contenu d'une page appartenant à un univers fictionnel.

Règles impératives de ta réponse :
- Tu réponds UNIQUEMENT par un objet JSON valide.
- Les clés du JSON correspondent EXACTEMENT aux noms de champs demandés.
- Les valeurs sont des chaînes de texte en français, riches et évocatrices.
- Aucun markdown, aucune explication, aucun commentaire autour du JSON.

Règles de cohérence (IMPORTANT) :
- Tu PEUX inventer des détails originaux pour CETTE page : apparence, traits de caractère, anecdotes, histoire personnelle.
- Tu ne dois PAS faire référence à d'autres personnages, lieux, organisations ou événements comme s'ils existaient déjà dans l'univers, sauf si le contexte ci-dessous les mentionne explicitement.
- Si un champ appelle une précision externe (date, nom d'un roi, ville voisine, guerre passée), reste volontairement vague : "il y a de nombreuses années", "un bourg voisin", "une époque troublée". Le MJ préfère combler lui-même les blancs plutôt que trouver des faits inventés contradictoires avec son univers."""


class GeneratePageUseCase:
    """Orchestre la génération d'une page LoreMind via un LLM."""

    def __init__(self, llm: LLMProvider) -> None:
        self._llm = llm

    async def execute(
        self,
        context: PageGenerationContext,
    ) -> PageGenerationResult:
        prompt = self._build_prompt(context)
        raw = await self._llm.generate(
            prompt,
            output_format="json",
            temperature=_DEFAULT_TEMPERATURE,
        )
        values = self._parse_values(raw, context.template_fields)
        return PageGenerationResult(values=values)

    def _build_prompt(self, context: PageGenerationContext) -> str:
        fields_block = "\n".join(f'- "{field}"' for field in context.template_fields)
        lore_desc_line = (
            f"\nDescription de l'univers : {context.lore_description}"
            if context.lore_description
            else ""
        )

        return (
            f"{_SYSTEM_INSTRUCTIONS}\n\n"
            f"Univers : {context.lore_name}"
            f"{lore_desc_line}\n"
            f"Catégorie (dossier) : {context.folder_name}\n"
            f"Gabarit : {context.template_name}\n"
            f"Titre de la page à créer : {context.page_title}\n\n"
            f"Champs à remplir (clés JSON attendues) :\n"
            f"{fields_block}\n\n"
            f"Génère maintenant le JSON."
        )

    def _parse_values(
        self,
        raw: str,
        expected_fields: list[str],
    ) -> dict[str, str]:
        try:
            parsed = json.loads(raw)
        except json.JSONDecodeError as exc:
            raise LLMProviderError(
                f"Réponse du LLM non parseable en JSON : {exc}"
            ) from exc

        if not isinstance(parsed, dict):
            raise LLMProviderError(
                f"Le LLM a renvoyé un {type(parsed).__name__}, pas un objet JSON."
            )

        # Filtrage défensif : on ne garde que les champs demandés, cast en str,
        # jamais None. Les champs absents de la réponse deviennent des chaînes vides
        # (l'utilisateur les complètera manuellement dans page-edit).
        return {
            field: str(parsed.get(field, "")).strip()
            for field in expected_fields
        }
