"""Use case : chat conversationnel LoreMind avec Structural Context.

Construit un system prompt riche à partir de 4 contextes possibles
(Lore, Page focalisée, Campagne, entité narrative focalisée) puis délègue
au port `LLMChatProvider` pour le streaming token par token.

Ne charge PAS le contenu détaillé des pages — l'IA doit savoir ce qui
existe, pas être noyée sous le texte. Pattern "Structural Context", plus
simple que le RAG sémantique tant que les univers restent de taille humaine.

Combinaisons supportées :
  - lore seul                              → chat Lore (page-edit / page-create)
  - lore + page_context                    → chat Lore focalisé page
  - campaign (+lore si liée) + optional narrative_entity → chat Campagne
"""
from typing import AsyncIterator

from app.domain.models import (
    ArcSummary,
    CampaignStructuralContext,
    ChatMessage,
    ChapterSummary,
    LoreStructuralContext,
    NarrativeEntityContext,
    PageContext,
    PageSummary,
)
from app.domain.ports import LLMChatProvider


# Température moyenne : chat conversationnel créatif mais cohérent.
# Plus élevée que le one-shot (0.4) car on veut de la variété d'idées,
# mais sans partir en délire halluciné (1.0+).
_DEFAULT_TEMPERATURE = 0.7


_BASE_SYSTEM = """Tu es un assistant d'écriture pour un Maître de Jeu de JDR.
Tu dialogues avec le MJ pour l'aider à enrichir son univers et ses campagnes.

Règles de ton :
- Réponds en français, ton chaleureux et créatif.
- Sois concis : listes à puces courtes plutôt que longs paragraphes.
- Propose des idées qui s'intègrent dans le contexte existant ci-dessous.

Règles de cohérence (IMPORTANT) :
- Tu PEUX et DOIS inventer des éléments originaux (personnages, lieux, objets, intrigues, créatures, scènes) — c'est ton rôle d'assistant créatif.
- Tu ne peux PAS faire référence à un élément du MJ (du Lore, des arcs, chapitres ou scènes) comme s'il existait déjà, SAUF s'il apparaît EXACTEMENT (même orthographe) dans l'une des sections de contexte ci-dessous.
- Si l'utilisateur mentionne un nom que tu ne vois pas dans le contexte, ne fais surtout pas semblant de le connaître : dis clairement "Je ne vois pas [nom] dans le contexte actuel, veux-tu qu'on le crée ?" plutôt que d'inventer des détails à son sujet.
- Évite les précisions inventées qu'on ne peut pas vérifier : dates exactes, chiffres de population, hiérarchies politiques complexes, généalogies détaillées. Préfère des formulations ouvertes que le MJ validera ("il y a longtemps", "de nombreux", "la haute noblesse")."""


class ChatUseCase:
    """Orchestre un tour de conversation avec le LLM + contextes structurels."""

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

    async def stream(
        self,
        messages: list[ChatMessage],
        *,
        lore_context: LoreStructuralContext | None = None,
        page_context: PageContext | None = None,
        campaign_context: CampaignStructuralContext | None = None,
        narrative_entity: NarrativeEntityContext | None = None,
    ) -> AsyncIterator[str]:
        """Streame les tokens de la réponse assistant pour le dernier message user.

        Les 4 contextes sont tous optionnels, mais au moins l'un des deux
        "niveaux haut" (lore_context ou campaign_context) doit être fourni
        pour que le prompt ait du sens. Le controller (main.py) applique
        cette règle à la frontière HTTP.
        """
        system_prompt = self._build_system_prompt(
            lore_context, page_context, campaign_context, narrative_entity
        )
        async for token in self._llm.stream_chat(
            messages,
            system_prompt=system_prompt,
            temperature=_DEFAULT_TEMPERATURE,
        ):
            yield token

    # --- Construction du system prompt --------------------------------------

    def _build_system_prompt(
        self,
        lore: LoreStructuralContext | None,
        page: PageContext | None,
        campaign: CampaignStructuralContext | None,
        narrative: NarrativeEntityContext | None,
    ) -> str:
        sections = [_BASE_SYSTEM]
        if lore is not None:
            sections.append(self._format_lore(lore))
        if campaign is not None:
            sections.append(self._format_campaign(campaign, lore_present=lore is not None))
        if page is not None:
            sections.append(self._format_page(page))
        if narrative is not None:
            sections.append(self._format_narrative_entity(narrative))
        return "\n\n".join(sections)

    # --- Blocs Lore ---------------------------------------------------------

    @staticmethod
    def _format_lore(ctx: LoreStructuralContext) -> str:
        desc = f"\nDescription : {ctx.lore_description}" if ctx.lore_description else ""
        folders_block = ChatUseCase._format_folders(ctx.folders)
        tags_line = ", ".join(ctx.tags) if ctx.tags else "(aucun)"
        return (
            "--- UNIVERS (Lore) ---\n"
            f"Nom : {ctx.lore_name}{desc}\n\n"
            f"Organisation :\n{folders_block}\n\n"
            f"Tags déjà utilisés : {tags_line}"
        )

    @staticmethod
    def _format_folders(folders: dict[str, list[PageSummary]]) -> str:
        """Rend chaque page avec son contenu exploitable par le LLM.

        Depuis b9 : affiche en plus des champs values/tags/pages liées sous
        forme d'une fiche indentée par page, et seulement si l'info existe
        (prompt compact quand une page est vierge).
        """
        if not folders:
            return "(Lore vide pour l'instant)"
        lines: list[str] = []
        for folder_name, pages in folders.items():
            lines.append(f"- {folder_name} (dossier)")
            if not pages:
                lines.append("    (vide)")
                continue
            for ps in pages:
                lines.append(f"    - {ps.title} [template: {ps.template_name}]")
                for field_name, value in ps.values.items():
                    lines.append(f"        · {field_name} : {value}")
                if ps.tags:
                    lines.append(f"        · tags : {', '.join(ps.tags)}")
                if ps.related_page_titles:
                    lines.append(
                        "        · liée à : " + ", ".join(ps.related_page_titles)
                    )
        return "\n".join(lines)

    @staticmethod
    def _format_page(pc: PageContext) -> str:
        """Bloc "PAGE EN COURS" — oriente l'IA vers la page précise éditée."""
        if pc.template_fields:
            fields_block = "\n".join(
                f'- "{f}" : {pc.values.get(f) or "(vide)"}'
                for f in pc.template_fields
            )
        else:
            fields_block = "(aucun champ défini dans ce template)"
        return (
            "--- PAGE EN COURS D'ÉDITION ---\n"
            f"Titre : {pc.title}\n"
            f"Template : {pc.template_name}\n"
            f"Champs et valeurs actuelles :\n{fields_block}\n\n"
            "IMPORTANT : concentre-toi EXCLUSIVEMENT sur cette page. "
            "Si l'utilisateur te demande de proposer des idées, elles doivent "
            "concerner UNIQUEMENT les champs listés ci-dessus. Ne déborde pas "
            "vers d'autres pages ou d'autres templates du Lore, même si ça te "
            "semblerait pertinent."
        )

    # --- Blocs Campagne -----------------------------------------------------

    @staticmethod
    def _format_campaign(ctx: CampaignStructuralContext, *, lore_present: bool) -> str:
        desc = f"\nDescription : {ctx.campaign_description}" if ctx.campaign_description else ""
        arcs_block = ChatUseCase._format_arcs(ctx.arcs)
        lore_note = (
            "\n(Cette campagne est liée à l'univers ci-dessus : tu peux t'appuyer dessus.)"
            if lore_present
            else "\n(Cette campagne n'est associée à aucun univers — tu peux proposer des éléments d'ambiance libres.)"
        )
        return (
            "--- CAMPAGNE COURANTE ---\n"
            f"Nom : {ctx.campaign_name}{desc}{lore_note}\n\n"
            "Structure narrative (les flèches → indiquent des transitions de scène "
            "déclenchées par un choix des joueurs) :\n"
            f"{arcs_block}"
        )

    @staticmethod
    def _format_arcs(arcs: list[ArcSummary]) -> str:
        if not arcs:
            return "(Aucun arc créé pour l'instant.)"
        lines: list[str] = []
        for arc in arcs:
            lines.append(f"- {arc.name} (arc){ChatUseCase._illustration_hint(arc.illustration_count)}")
            if arc.description:
                lines.append(f"  Synopsis : {arc.description}")
            if not arc.chapters:
                lines.append("    (aucun chapitre)")
                continue
            for chapter in arc.chapters:
                lines.extend(ChatUseCase._format_chapter_block(chapter))
        return "\n".join(lines)

    @staticmethod
    def _format_chapter_block(chapter: ChapterSummary) -> list[str]:
        hint = ChatUseCase._illustration_hint(chapter.illustration_count)
        block = [f"    - {chapter.name} (chapitre){hint}"]
        if chapter.description:
            block.append(f"      Synopsis : {chapter.description}")
        if not chapter.scenes:
            block.append("        (aucune scène)")
        else:
            for scene in chapter.scenes:
                sc_hint = ChatUseCase._illustration_hint(scene.illustration_count)
                block.append(f"        - {scene.name} (scène){sc_hint}")
                if scene.description:
                    block.append(f"          Description : {scene.description}")
                for br in scene.branches:
                    cond = f" (si : {br.condition})" if br.condition else ""
                    block.append(
                        f'          → "{br.label}" vers {br.target_scene_name}{cond}'
                    )
        return block

    @staticmethod
    def _illustration_hint(count: int) -> str:
        """Rend " [N illustrations]" si count > 0, sinon chaine vide.

        Informe l'IA que l'entite a deja un support visuel. Permet de prioriser
        les suggestions ecrites qui collent a l'existant visuel plutot que de
        diverger.
        """
        if count <= 0:
            return ""
        noun = "illustration" if count == 1 else "illustrations"
        return f" [{count} {noun}]"

    @staticmethod
    def _format_narrative_entity(ne: NarrativeEntityContext) -> str:
        """Bloc équivalent à _format_page mais pour Arc/Chapter/Scene."""
        type_label = {"arc": "ARC", "chapter": "CHAPITRE", "scene": "SCÈNE"}.get(
            ne.entity_type.lower(), ne.entity_type.upper()
        )
        if ne.fields:
            fields_block = "\n".join(
                f'- "{key}" : {value or "(vide)"}'
                for key, value in ne.fields.items()
            )
        else:
            fields_block = "(aucun champ renseigné)"
        return (
            f"--- {type_label} EN COURS D'ÉDITION ---\n"
            f"Titre : {ne.title}\n"
            f"Champs et valeurs actuelles :\n{fields_block}\n\n"
            "IMPORTANT : concentre-toi EXCLUSIVEMENT sur cette entité narrative. "
            "Tes suggestions doivent enrichir UNIQUEMENT les champs listés ci-dessus. "
            "Ne déborde pas vers d'autres arcs, chapitres ou scènes de la campagne, "
            "même si ça te semblerait pertinent."
        )
