# -*- coding: utf-8 -*-
"""
Un tour de dialogue avec l'API Claude.

Ce module ne connaît qu'une seule opération : `un_tour(projet_id)`. Il charge
la conversation depuis le disque, fait un appel, exécute les écritures de
fichiers demandées, ré-enregistre, et rend la main. Le worker l'appelle en
boucle tant qu'il lui reste du temps.

Ce découpage est ce qui permet de tourner sur un hébergement mutualisé : aucun
processus ne vit longtemps, et une interruption brutale ne coûte qu'un tour.
"""

import copy
import json

import anthropic

import config
import outils
import projets


# Les blocs qui acceptent un point de reprise du cache.
_BLOCS_CACHABLES = {"text", "tool_result", "document", "image"}

# Nombre de relances avant de considérer qu'un modèle qui s'arrête sans appeler
# terminer() a bel et bien fini.
MAX_RELANCES = 2


def _client():
    # La clé est résolue depuis ANTHROPIC_API_KEY (chargée par config.charger_env).
    return anthropic.Anthropic(max_retries=3)


def _poser_cache(bloc, duree=None):
    """Marque un bloc comme point de reprise du cache, si son type l'accepte."""
    if isinstance(bloc, dict) and bloc.get("type") in _BLOCS_CACHABLES:
        bloc["cache_control"] = (
            {"type": "ephemeral", "ttl": duree} if duree else {"type": "ephemeral"}
        )
        return True
    return False


def _messages_pour_requete(messages):
    """Copie la conversation en y ajoutant le point de reprise glissant.

    Le cache se fait par correspondance de préfixe : le système et le brief
    initial sont marqués une fois pour toutes à la création. Ici on ajoute un
    troisième point sur le dernier message, pour que le tour suivant relise en
    cache tout ce qui précède. Le marquage n'est pas enregistré sur disque :
    il est recalculé à chaque requête.
    """
    messages = copy.deepcopy(messages)
    for message in reversed(messages):
        contenu = message.get("content")
        if isinstance(contenu, list) and contenu:
            if _poser_cache(contenu[-1]):
                break
    return messages


def _texte(reponse):
    """Le texte visible de la réponse, hors blocs de réflexion."""
    morceaux = [b.text for b in reponse.content if b.type == "text" and b.text]
    return "\n".join(morceaux).strip()


def _usage(reponse):
    u = reponse.usage
    return {
        "input_tokens": getattr(u, "input_tokens", 0) or 0,
        "output_tokens": getattr(u, "output_tokens", 0) or 0,
        "cache_read_input_tokens": getattr(u, "cache_read_input_tokens", 0) or 0,
        "cache_creation_input_tokens": getattr(u, "cache_creation_input_tokens", 0) or 0,
    }


def un_tour(projet_id):
    """Fait avancer le projet d'un tour.

    Renvoie ("continuer"|"termine"|"echec"|"vide", détail). Le worker s'arrête
    sur autre chose que "continuer".
    """
    conversation = projets.lire_conversation(projet_id)
    if not conversation:
        return "echec", "Conversation absente : le projet n'a pas été préparé."

    messages = conversation.get("messages") or []
    if not messages:
        return "echec", "Conversation vide."

    projet = projets.lire(projet_id)
    if projet and projet["tours"] >= config.MAX_TOURS:
        return "echec", (
            "Plafond de %d tours atteint sans que le modèle appelle terminer(). "
            "Génération abandonnée." % config.MAX_TOURS
        )

    client = _client()
    with client.messages.stream(
        model=config.MODELE,
        max_tokens=config.MAX_JETONS,
        system=conversation["systeme"],
        messages=_messages_pour_requete(messages),
        tools=outils.OUTILS,
        thinking={"type": "adaptive"},
        output_config={"effort": config.EFFORT},
    ) as flux:
        reponse = flux.get_final_message()

    projets.compter_tour(projet_id, _usage(reponse))
    projets.compter_echec(projet_id, remise_a_zero=True)

    # Le contenu de l'assistant est ré-enregistré tel quel : les blocs de
    # réflexion doivent revenir au modèle inchangés au tour suivant.
    contenu_assistant = reponse.to_dict()["content"]
    messages.append({"role": "assistant", "content": contenu_assistant})

    commentaire = _texte(reponse)
    if commentaire:
        projets.journaliser(projet_id, "message", commentaire[:2000])

    appels = [b for b in reponse.content if b.type == "tool_use"]

    # --- Refus du modèle ----------------------------------------------------
    if reponse.stop_reason == "refusal":
        details = getattr(reponse, "stop_details", None)
        motif = getattr(details, "explanation", None) or "motif non précisé"
        conversation["messages"] = messages
        projets.ecrire_conversation(projet_id, conversation)
        return "echec", "Le modèle a décliné la demande (%s)." % motif

    # --- Réponse tronquée ---------------------------------------------------
    if reponse.stop_reason == "max_tokens":
        if appels:
            # Un appel d'outil tronqué n'est pas exécutable, mais l'API exige
            # un résultat pour chaque appel : on répond en erreur.
            messages.append({
                "role": "user",
                "content": [
                    {
                        "type": "tool_result",
                        "tool_use_id": appel.id,
                        "content": (
                            "Réponse tronquée : ce fichier dépasse ce qui tient en "
                            "un tour. Écris-le en plusieurs fichiers plus petits, "
                            "ou écris d'abord une partie puis complète."
                        ),
                        "is_error": True,
                    }
                    for appel in appels
                ],
            })
        else:
            messages.append({
                "role": "user",
                "content": [{"type": "text", "text": "Réponse tronquée, poursuis."}],
            })
        conversation["messages"] = messages
        projets.ecrire_conversation(projet_id, conversation)
        projets.journaliser(projet_id, "tronque", "réponse coupée au plafond de jetons")
        return "continuer", "réponse tronquée, relance"

    # --- Le modèle s'arrête sans appeler terminer() -------------------------
    if not appels:
        relances = conversation.get("relances", 0) + 1
        conversation["relances"] = relances
        if relances > MAX_RELANCES:
            conversation["messages"] = messages
            projets.ecrire_conversation(projet_id, conversation)
            return "termine", commentaire or "Le modèle s'est arrêté."
        messages.append({
            "role": "user",
            "content": [{
                "type": "text",
                "text": (
                    "Si le travail demandé est terminé, appelle l'outil terminer "
                    "avec ton résumé de livraison. Sinon, poursuis l'écriture des "
                    "fichiers."
                ),
            }],
        })
        conversation["messages"] = messages
        projets.ecrire_conversation(projet_id, conversation)
        return "continuer", "relance (%d/%d)" % (relances, MAX_RELANCES)

    # --- Exécution des outils ----------------------------------------------
    conversation["relances"] = 0
    resultats = []
    fini = None
    racine_site = projets.dossier_site(projet_id)

    for appel in appels:
        texte, erreur = outils.executer(racine_site, appel.name, appel.input)
        resultats.append({
            "type": "tool_result",
            "tool_use_id": appel.id,
            "content": texte,
            **({"is_error": True} if erreur else {}),
        })
        if appel.name == "ecrire_fichier":
            projets.journaliser(projet_id, "erreur_outil" if erreur else "fichier", texte[:500])
        elif appel.name == "terminer" and not erreur:
            fini = (appel.input or {}).get("resume") or commentaire
            projets.journaliser(projet_id, "termine", (fini or "")[:2000])
        elif erreur:
            projets.journaliser(projet_id, "erreur_outil", texte[:500])

    messages.append({"role": "user", "content": resultats})
    conversation["messages"] = messages
    projets.ecrire_conversation(projet_id, conversation)

    if fini is not None:
        return "termine", fini
    return "continuer", "%d appel(s) d'outil" % len(appels)


def relancer(projet_id, consigne):
    """Ajoute une consigne à une conversation terminée et la remet en file.

    C'est tout le mécanisme des itérations et des annexes : « refais la section
    chiffres », « crée la page Exposants », « convertis en Elementor » sont des
    messages de plus dans le même dialogue, qui a déjà tout le contexte.
    """
    conversation = projets.lire_conversation(projet_id)
    if not conversation:
        raise ValueError("Projet sans conversation.")
    conversation["messages"].append({
        "role": "user",
        "content": [{"type": "text", "text": consigne}],
    })
    conversation["relances"] = 0
    projets.ecrire_conversation(projet_id, conversation)
    projets.journaliser(projet_id, "consigne", consigne[:2000])
    projets.changer_etat(projet_id, projets.EN_ATTENTE)
