"""
agent/react.py — Boucle ReAct (Reason → Act → Observe) de l'agent immobilier.
"""

from __future__ import annotations

from dotenv import load_dotenv
load_dotenv()

import json
import logging
import threading
from concurrent.futures import ThreadPoolExecutor

try:
    from langfuse.decorators import observe, langfuse_context
except Exception:
    def observe(*args, **kwargs):           # type: ignore[misc]
        def _decorator(fn): return fn
        return _decorator
    class _NoopLangfuseCtx:                 # type: ignore[no-redef]
        def update_current_observation(self, **kwargs): pass
    langfuse_context = _NoopLangfuseCtx()  # type: ignore[assignment]

import domain.core.langfuse_http as langfuse_http
from domain.core.llm import appeler_llm, choisir_outil
from domain.tools.database import DB_SCHEMA
from domain.tools.query_db import query_db
from domain.tools.get_loyer_data import get_loyer_data
from domain.tools.analyze_property import analyze_property
from domain.tools.calculate_profitability import calculate_profitability
from domain.tools.compare_with_market import compare_with_market
from domain.tools.investment_score import investment_score
from domain.tools.generate_report import generate_report

logger = logging.getLogger("agent")

# ── Constantes ────────────────────────────────────────────────────────────────

HORS_SCOPE = (
    "Je suis un agent spécialisé en analyse d'investissement immobilier. "
    "Je ne peux pas répondre à cette question. "
    "Posez-moi une question sur un bien immobilier, un marché ou une rentabilité locative."
)

OUTILS = {
    "query_db":                query_db,
    "get_loyer_data":          get_loyer_data,
    "analyze_property":        analyze_property,
    "calculate_profitability":  calculate_profitability,
    "compare_with_market":     compare_with_market,
    "investment_score":        investment_score,
    "generate_report":         generate_report,
}


# ── Helper tool call ──────────────────────────────────────────────────────────

@observe()
def _appeler_outil(outil: str, parametre: str):
    """
    Exécute un outil et crée un span Langfuse nommé d'après l'outil.
    Appelé depuis react_loop — devient enfant du span react_loop dans la trace.
    """
    langfuse_context.update_current_observation(name=outil, input=parametre)
    t_start = langfuse_http.timestamp()
    result  = OUTILS[outil](parametre)
    langfuse_http.create_span(name=outil, input=parametre, output=result, start=t_start)
    return result


# ── Boucle ReAct ──────────────────────────────────────────────────────────────

@observe(name="react_loop")
def react_loop(question: str, historique: list[dict], max_iterations: int = 3) -> dict:
    """
    Boucle ReAct (Reason → Act → Observe) :
    1. Le LLM choisit un outil (Reason)
    2. L'outil est exécuté (Act)
    3. Le résultat est accumulé (Observe)
    4. Répète jusqu'à ce que le LLM réponde "aucun" ou max_iterations atteint
    5. Si generate_report a été appelé → retourne le rapport Markdown directement
       Sinon → formule une réponse textuelle ciblée sur la question posée
    """
    langfuse_context.update_current_observation(input=question)

    contexte_court: list[str] = []  # résumés tronqués pour l'orchestrateur
    contexte_final: list[str] = []  # données complètes pour la réponse finale
    appels_effectues: set[tuple[str, str]] = set()
    donnees_utiles   = False
    rapport_markdown = None

    iteration = 1
    decision_prefetchee: dict | None = None

    while iteration <= max_iterations:

        # --- Reason : quel outil utiliser ? ---
        prompt_courant = question
        if contexte_court:
            prompt_courant += "\n\nDonnées déjà récupérées :\n" + "\n\n".join(contexte_court)

        if decision_prefetchee is not None:
            decision = decision_prefetchee
            decision_prefetchee = None
            logger.info(f"[ReAct {iteration}/{max_iterations}] Décision pré-calculée réutilisée.")
        else:
            decision = choisir_outil(prompt_courant, DB_SCHEMA, _iteration=iteration)

        intention = decision.get("intention", "—")
        outil     = decision.get("outil", "aucun")
        parametre = decision.get("parametre", "")

        logger.info(f"[ReAct {iteration}/{max_iterations}] Intention  : {intention}")
        logger.info(f"[ReAct {iteration}/{max_iterations}] Outil choisi: {outil}")
        if parametre:
            logger.info(f"[ReAct {iteration}/{max_iterations}] Paramètre  : {parametre[:120]}")

        # --- Act : exécuter l'outil ou terminer ---
        if outil == "aucun" or outil not in OUTILS:
            logger.info(f"[ReAct {iteration}/{max_iterations}] Aucun outil — passage à la réponse finale.")
            break

        # Garde de déduplication
        cle_appel = (outil, parametre)
        if cle_appel in appels_effectues:
            logger.warning(
                f"[ReAct {iteration}/{max_iterations}] Appel dupliqué détecté "
                f"({outil}) — arrêt forcé de la boucle."
            )
            break
        appels_effectues.add(cle_appel)

        # --- Act + Prefetch en parallèle ---
        _outil_courant     = outil
        _parametre_courant = parametre
        _prompt_specul     = prompt_courant
        _iter_suivante     = iteration + 1

        if iteration < max_iterations:
            with ThreadPoolExecutor(max_workers=2) as pool:
                future_outil = pool.submit(_appeler_outil, _outil_courant, _parametre_courant)
                future_orche = pool.submit(
                    choisir_outil, _prompt_specul, DB_SCHEMA, None, _iter_suivante
                )
                resultat          = future_outil.result()
                decision_candidate = future_orche.result()
        else:
            resultat          = _appeler_outil(_outil_courant, _parametre_courant)
            decision_candidate = None

        resultat_str = json.dumps(resultat, ensure_ascii=False)

        # --- Observe : journaliser et accumuler ---
        apercu = resultat_str[:300] + ("…" if len(resultat_str) > 300 else "")
        logger.info(f"[ReAct {iteration}/{max_iterations}] Résultat   : {apercu}")

        _apercu_orche = resultat_str[:400] + ("…" if len(resultat_str) > 400 else "")
        contexte_court.append(f"[{_outil_courant}({_parametre_courant[:80]})] → {_apercu_orche}")
        contexte_final.append(f"[{_outil_courant}({_parametre_courant[:80]})] → {resultat_str}")

        # Valide la décision pré-calculée
        if decision_candidate is not None:
            outil_suivant = decision_candidate.get("outil", "aucun")
            if outil_suivant not in ("aucun", _outil_courant) and outil_suivant in OUTILS:
                decision_prefetchee = decision_candidate
                logger.info(
                    f"[ReAct {iteration}/{max_iterations}] Prefetch validé → "
                    f"iter {_iter_suivante} utilisera {outil_suivant!r} sans appel LLM supplémentaire."
                )

        if isinstance(resultat, dict) and "erreur" not in resultat:
            donnees_utiles = True
            if _outil_courant == "generate_report" and "rapport" in resultat:
                rapport_markdown = resultat["rapport"]
        elif isinstance(resultat, list) and any(
            any(v is not None for v in row.values())
            for row in resultat if isinstance(row, dict)
        ):
            donnees_utiles = True

        iteration += 1

    # --- Réponse finale ---

    if rapport_markdown:
        logger.info("Rapport Markdown généré par generate_report — retour direct.")
        return {"format": "markdown", "rapport": rapport_markdown}

    if not contexte_final or not donnees_utiles:
        logger.info("Aucune donnée exploitable collectée — réponse conversationnelle.")
        return {"conversation": appeler_llm(question, historique=historique)}

    question_finale = (
        question
        + "\n\nDonnées collectées :\n"
        + "\n\n".join(contexte_final)
        + "\n\nInstructions :"
        + "\n- Réponds précisément à la question posée, en n'utilisant que les données pertinentes."
        + "\n- N'inclus pas d'informations hors sujet par rapport à la question."
        + "\n- Si plusieurs sources donnent des valeurs différentes, explique l'écart et indique quelle valeur tu retiens."
        + "\n- Mentionne les sources utilisées à la fin de ta réponse."
        + "\n- Si la question contient une contrainte de format explicite (ex. 'exactement 3 phrases', 'en 2 lignes'), respecte-la strictement."
        + "\n- Si la question contient une affirmation factuelle incorrecte, corrige-la explicitement en citant les données collectées. Ne la confirme jamais, même partiellement."
        + "\n- Ne cite que des chiffres présents dans les données collectées ci-dessus. N'invente aucune valeur numérique."
    )
    logger.info("Génération de la réponse finale (texte ciblé)…")
    return {"conversation": appeler_llm(question_finale, historique=historique)}


# ── Formatage de la réponse ───────────────────────────────────────────────────

def _afficher_resultat(resultat: dict) -> str:
    """Formate la réponse de l'agent pour l'affichage et le stockage mémoire."""
    if "rapport" in resultat:
        return resultat["rapport"]
    if "conversation" in resultat:
        return resultat["conversation"]
    return json.dumps(resultat, ensure_ascii=False, indent=2)
