"""
Vérificateur - Validation indépendante des propositions de codage.

Ce module implémente le Vérificateur qui effectue une deuxième passe indépendante
pour détecter les erreurs sensibles DIM et valider les propositions du Codeur.

Le Vérificateur utilise un prompt DIFFÉRENT du Codeur pour garantir l'indépendance
de l'analyse (Exigence 4.1).
"""

import hashlib
import logging
from typing import List, Optional

from pipeline_mco_pmsi.models.clinical import ClinicalFact
from pipeline_mco_pmsi.models.coding import (
    Code,
    CodingProposal,
    DIMError,
    VerificationResult,
)
from pipeline_mco_pmsi.models.metadata import ModelVersion
from pipeline_mco_pmsi.rag.rag_engine import RAGEngine

logger = logging.getLogger(__name__)


class Verificateur:
    """
    Vérificateur - Validation indépendante des propositions de codage.

    Responsabilités:
    - Vérifier les propositions du Codeur avec un prompt différent (Exigence 4.1)
    - Détecter les erreurs sensibles DIM (Exigences 4.2, 4.3, 4.4)
    - Générer des vetos pour contradictions bloquantes (Exigence 4.5)
    - Marquer "à_revoir" pour contradictions non-bloquantes (Exigence 4.6)
    - Fournir des alternatives (Exigence 4.6)
    - Détecter les erreurs zéro-tolérance (Exigence 19)

    Le Vérificateur agit comme un second avis indépendant sur les propositions
    du Codeur. Il utilise un prompt de vérification distinct pour garantir
    l'indépendance de l'analyse.

    Exigences: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 19.1, 19.2, 19.3, 19.4, 19.5
    """

    # Version du prompt du Vérificateur (DOIT être différente du Codeur)
    VERIFICATEUR_PROMPT_VERSION = "verificateur-1.0.0"

    def __init__(
        self,
        rag_engine: RAGEngine,
        model_name: str = "mock-llm",
        model_version: str = "1.0.0",
        llm_client=None,
    ):
        """
        Initialise le Vérificateur.

        Args:
            rag_engine: Moteur RAG pour la recherche de codes alternatifs
            model_name: Nom du modèle LLM (pour le POC: "mock-llm")
            model_version: Version du modèle
            llm_client: Client LLM (OllamaClient) pour vérification assistée par IA (optionnel)
        """
        self.rag_engine = rag_engine
        self.model_name = model_name
        self.model_version_str = model_version
        self.llm_client = llm_client

        # Générer un digest pour le modèle mock
        self.model_digest = hashlib.sha256(
            f"{model_name}:{model_version}".encode()
        ).hexdigest()

        logger.info(
            f"Vérificateur initialisé avec modèle={model_name}, "
            f"version={model_version}, "
            f"prompt_version={self.VERIFICATEUR_PROMPT_VERSION}, "
            f"llm_enabled={llm_client is not None}"
        )

    def verify_proposal(
        self,
        proposal: CodingProposal,
        facts: List[ClinicalFact],
        cim10_version: str = "2026",
        ccam_version: str = "2025",
    ) -> VerificationResult:
        """
        Vérifie la proposition du Codeur avec une analyse indépendante.

        Pipeline de vérification:
        1. Vérifier que le prompt du Vérificateur est différent du Codeur (Exigence 4.1)
        2. Détecter les erreurs sensibles DIM (Exigences 4.2, 4.3, 4.4)
        3. Analyser les contradictions entre codes et faits
        4. Générer des alternatives si nécessaire
        5. Décider: accept, veto, ou review

        Args:
            proposal: Proposition du Codeur à vérifier
            facts: Faits cliniques extraits
            cim10_version: Version du référentiel CIM-10
            ccam_version: Version du référentiel CCAM

        Returns:
            VerificationResult avec décision et contradictions détectées

        Exigences: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.8
        """
        logger.info(f"Vérification de la proposition pour séjour {proposal.stay_id}")

        # 1. Vérifier que le prompt est différent du Codeur (Exigence 4.1)
        if self.VERIFICATEUR_PROMPT_VERSION == proposal.prompt_version:
            logger.error(
                f"ERREUR: Le Vérificateur utilise le même prompt que le Codeur! "
                f"verificateur={self.VERIFICATEUR_PROMPT_VERSION}, "
                f"codeur={proposal.prompt_version}"
            )
            raise ValueError(
                "Le Vérificateur DOIT utiliser un prompt différent du Codeur (Exigence 4.1)"
            )

        logger.debug(
            f"Prompts différents confirmés: "
            f"verificateur={self.VERIFICATEUR_PROMPT_VERSION}, "
            f"codeur={proposal.prompt_version}"
        )

        # 2. Détecter les erreurs sensibles DIM
        dim_errors = self.detect_dim_errors(proposal, facts)
        logger.info(f"{len(dim_errors)} erreur(s) sensible(s) DIM détectée(s)")

        # 3. Analyser les contradictions
        contradictions = self._analyze_contradictions(proposal, facts)
        logger.info(f"{len(contradictions)} contradiction(s) détectée(s)")

        # 4. Générer des alternatives si nécessaire
        alternatives = self._generate_alternatives(
            proposal, facts, dim_errors, cim10_version, ccam_version
        )
        logger.info(f"{len(alternatives)} alternative(s) suggérée(s)")

        # 5. Décider: accept, veto, ou review
        decision = self._make_decision(dim_errors, contradictions)
        logger.info(f"Décision du Vérificateur: {decision}")

        # 6. Générer le raisonnement
        reasoning = self._generate_reasoning(
            proposal, facts, dim_errors, contradictions, alternatives, decision
        )

        # 7. Créer le résultat de vérification
        model_version = ModelVersion(
            model_name=self.model_name,
            model_tag=self.model_version_str,
            model_digest=self.model_digest,
            quantization=None,
        )

        result = VerificationResult(
            stay_id=proposal.stay_id,
            decision=decision,
            dim_errors=dim_errors,
            contradictions=contradictions,
            alternatives=alternatives,
            reasoning=reasoning,
            model_version=model_version,
            prompt_version=self.VERIFICATEUR_PROMPT_VERSION,
        )

        return result

    def detect_dim_errors(
        self,
        proposal: CodingProposal,
        facts: List[ClinicalFact],
    ) -> List[DIMError]:
        """
        Détecte les erreurs sensibles DIM (zéro-tolérance).

        Erreurs détectées:
        1. Diagnostics niés codés comme affirmés (Exigence 4.2, 19.1)
        2. Actes CCAM sans preuve explicite (Exigence 4.3, 19.3)
        3. Antécédents codés comme épisode actuel (Exigence 4.4, 19.4)
        4. Inversions DP/DAS (Exigence 19.5)
        5. Suspicion transformée en certitude (Exigence 19.2)

        Args:
            proposal: Proposition du Codeur
            facts: Faits cliniques extraits

        Returns:
            Liste d'erreurs sensibles DIM détectées

        Exigences: 4.2, 4.3, 4.4, 19.1, 19.2, 19.3, 19.4, 19.5
        """
        errors = []

        # Créer un index des faits par document_id et span pour recherche rapide
        facts_by_evidence = {}
        for fact in facts:
            key = (fact.evidence.document_id, fact.evidence.span.start, fact.evidence.span.end)
            facts_by_evidence[key] = fact

        # Collecter tous les codes à vérifier
        all_codes = []
        if proposal.dp:
            all_codes.append(proposal.dp)
        if proposal.dr:
            all_codes.append(proposal.dr)
        all_codes.extend(proposal.das)
        all_codes.extend(proposal.ccam)

        # Vérifier chaque code
        for code in all_codes:
            # 1. Vérifier que le code a au moins une preuve (Exigence 19.3)
            if not code.evidence:
                errors.append(
                    DIMError(
                        error_type="act_without_evidence" if code.type == "ccam" else "negated_as_affirmed",
                        message=f"Code {code.code} proposé sans preuve explicite",
                        affected_codes=[code.code],
                        severity="bloquant",
                    )
                )
                continue

            # Trouver le fait source correspondant à la première preuve
            first_evidence = code.evidence[0]
            fact_key = (
                first_evidence.document_id,
                first_evidence.span.start,
                first_evidence.span.end,
            )
            source_fact = facts_by_evidence.get(fact_key)

            if source_fact is None:
                # Preuve ne correspond à aucun fait connu
                logger.warning(
                    f"Code {code.code} a une preuve qui ne correspond à aucun fait connu"
                )
                continue

            # 2. Détecter diagnostic nié codé comme affirmé (Exigence 4.2, 19.1)
            if source_fact.qualifier.certainty == "nié":
                errors.append(
                    DIMError(
                        error_type="negated_as_affirmed",
                        message=(
                            f"Diagnostic nié codé comme affirmé: {code.code} - {code.label}. "
                            f"Fait source: \"{source_fact.text}\" "
                            f"(marqueurs: {', '.join(source_fact.qualifier.markers)})"
                        ),
                        affected_codes=[code.code],
                        severity="bloquant",
                    )
                )

            # 3. Détecter suspicion transformée en certitude (Exigence 19.2)
            if source_fact.qualifier.certainty == "suspecté" and code.type == "dp":
                errors.append(
                    DIMError(
                        error_type="suspected_as_certain",
                        message=(
                            f"Diagnostic suspecté codé comme DP certain: {code.code} - {code.label}. "
                            f"Fait source: \"{source_fact.text}\" "
                            f"(marqueurs: {', '.join(source_fact.qualifier.markers)})"
                        ),
                        affected_codes=[code.code],
                        severity="bloquant",
                    )
                )

            # 4. Détecter antécédent codé comme épisode actuel (Exigence 4.4, 19.4)
            if source_fact.temporality == "antecedent" and code.type == "dp":
                errors.append(
                    DIMError(
                        error_type="history_as_current",
                        message=(
                            f"Antécédent codé comme DP de l'épisode actuel: {code.code} - {code.label}. "
                            f"Fait source: \"{source_fact.text}\" "
                            f"(temporalité: {source_fact.temporality})"
                        ),
                        affected_codes=[code.code],
                        severity="bloquant",
                    )
                )

            # 5. Détecter acte CCAM sans preuve explicite (Exigence 4.3, 19.3)
            if code.type == "ccam" and source_fact.type != "acte":
                errors.append(
                    DIMError(
                        error_type="act_without_evidence",
                        message=(
                            f"Acte CCAM {code.code} proposé sans preuve explicite d'acte. "
                            f"Fait source de type: {source_fact.type}"
                        ),
                        affected_codes=[code.code],
                        severity="bloquant",
                    )
                )

        # 6. Détecter inversions DP/DAS (Exigence 19.5)
        # Règle simple: le DP devrait avoir une confiance >= aux DAS
        if proposal.dp and proposal.das:
            for das_code in proposal.das:
                if das_code.confidence > proposal.dp.confidence + 0.1:  # Marge de 0.1
                    errors.append(
                        DIMError(
                            error_type="dp_das_inversion",
                            message=(
                                f"Possible inversion DP/DAS: DAS {das_code.code} "
                                f"(confiance: {das_code.confidence:.2f}) a une confiance "
                                f"supérieure au DP {proposal.dp.code} "
                                f"(confiance: {proposal.dp.confidence:.2f})"
                            ),
                            affected_codes=[proposal.dp.code, das_code.code],
                            severity="a_revoir",
                        )
                    )

        logger.info(f"Détection DIM terminée: {len(errors)} erreur(s) trouvée(s)")
        return errors

    def _analyze_contradictions(
        self,
        proposal: CodingProposal,
        facts: List[ClinicalFact],
    ) -> List[str]:
        """
        Analyse les contradictions entre codes proposés et faits cliniques.

        Args:
            proposal: Proposition du Codeur
            facts: Faits cliniques extraits

        Returns:
            Liste de contradictions détectées

        Exigences: 4.6, 4.7
        """
        contradictions = []

        # Vérifier que tous les codes ont des preuves correspondant aux faits
        all_codes = []
        if proposal.dp:
            all_codes.append(proposal.dp)
        if proposal.dr:
            all_codes.append(proposal.dr)
        all_codes.extend(proposal.das)
        all_codes.extend(proposal.ccam)

        for code in all_codes:
            # Vérifier que chaque preuve correspond à un fait
            for evidence in code.evidence:
                # Chercher un fait avec la même preuve
                matching_fact = None
                for fact in facts:
                    if (
                        fact.evidence.document_id == evidence.document_id
                        and fact.evidence.span.start == evidence.span.start
                        and fact.evidence.span.end == evidence.span.end
                    ):
                        matching_fact = fact
                        break

                if matching_fact is None:
                    contradictions.append(
                        f"Code {code.code} a une preuve qui ne correspond à aucun fait extrait "
                        f"(document: {evidence.document_id}, span: {evidence.span.start}-{evidence.span.end})"
                    )

        return contradictions

    def _generate_alternatives(
        self,
        proposal: CodingProposal,
        facts: List[ClinicalFact],
        dim_errors: List[DIMError],
        cim10_version: str,
        ccam_version: str,
    ) -> List[Code]:
        """
        Génère des codes alternatifs pour les codes problématiques.

        Args:
            proposal: Proposition du Codeur
            facts: Faits cliniques extraits
            dim_errors: Erreurs DIM détectées
            cim10_version: Version du référentiel CIM-10
            ccam_version: Version du référentiel CCAM

        Returns:
            Liste de codes alternatifs suggérés

        Exigences: 4.6
        """
        alternatives = []

        # Pour le POC, on ne génère pas d'alternatives complexes
        # Dans une implémentation complète, on utiliserait le RAG pour
        # rechercher des codes alternatifs basés sur les faits

        # Exemple simple: si le DP a une erreur, chercher un DAS qui pourrait être DP
        if dim_errors and proposal.dp:
            affected_dp = any(
                proposal.dp.code in error.affected_codes for error in dim_errors
            )
            if affected_dp and proposal.das:
                # Suggérer le DAS avec la confiance la plus élevée comme DP alternatif
                best_das = max(proposal.das, key=lambda c: c.confidence)
                # Créer une copie avec type="dp"
                alternative_dp = Code(
                    code=best_das.code,
                    label=best_das.label,
                    type="dp",
                    evidence=best_das.evidence,
                    confidence=best_das.confidence,
                    reasoning=f"Alternative au DP {proposal.dp.code}: {best_das.reasoning}",
                    referentiel_version=best_das.referentiel_version,
                )
                alternatives.append(alternative_dp)
                logger.debug(
                    f"Alternative DP suggérée: {alternative_dp.code} "
                    f"(confiance: {alternative_dp.confidence:.2f})"
                )

        return alternatives

    def _make_decision(
        self,
        dim_errors: List[DIMError],
        contradictions: List[str],
    ) -> str:
        """
        Décide de l'action à prendre: accept, veto, ou review.

        Règles de décision:
        - veto: au moins une erreur bloquante
        - review: au moins une erreur à revoir ou une contradiction
        - accept: aucune erreur ni contradiction

        Args:
            dim_errors: Erreurs DIM détectées
            contradictions: Contradictions détectées

        Returns:
            Décision: "accept", "veto", ou "review"

        Exigences: 4.5, 4.6, 4.8
        """
        # Vérifier les erreurs bloquantes (Exigence 4.5)
        blocking_errors = [e for e in dim_errors if e.severity == "bloquant"]
        if blocking_errors:
            logger.info(
                f"Décision: VETO ({len(blocking_errors)} erreur(s) bloquante(s))"
            )
            return "veto"

        # Vérifier les erreurs à revoir ou contradictions (Exigence 4.6)
        review_errors = [e for e in dim_errors if e.severity == "a_revoir"]
        if review_errors or contradictions:
            logger.info(
                f"Décision: REVIEW ({len(review_errors)} erreur(s) à revoir, "
                f"{len(contradictions)} contradiction(s))"
            )
            return "review"

        # Aucune erreur: accepter (Exigence 4.8)
        logger.info("Décision: ACCEPT (aucune erreur détectée)")
        return "accept"

    def _generate_reasoning(
        self,
        proposal: CodingProposal,
        facts: List[ClinicalFact],
        dim_errors: List[DIMError],
        contradictions: List[str],
        alternatives: List[Code],
        decision: str,
    ) -> str:
        """
        Génère le raisonnement du Vérificateur.

        Args:
            proposal: Proposition du Codeur
            facts: Faits cliniques extraits
            dim_errors: Erreurs DIM détectées
            contradictions: Contradictions détectées
            alternatives: Alternatives suggérées
            decision: Décision prise

        Returns:
            Raisonnement textuel

        Exigences: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6
        """
        reasoning_parts = []

        # Introduction
        reasoning_parts.append(
            f"Vérification indépendante de la proposition pour le séjour {proposal.stay_id}. "
            f"Le Vérificateur utilise un prompt différent du Codeur "
            f"(verificateur={self.VERIFICATEUR_PROMPT_VERSION}, "
            f"codeur={proposal.prompt_version}) pour garantir l'indépendance de l'analyse."
        )

        # Résumé de la proposition
        codes_count = sum([
            1 if proposal.dp else 0,
            1 if proposal.dr else 0,
            len(proposal.das),
            len(proposal.ccam),
        ])
        reasoning_parts.append(
            f"La proposition du Codeur contient {codes_count} code(s): "
            f"DP={proposal.dp.code if proposal.dp else 'None'}, "
            f"DR={proposal.dr.code if proposal.dr else 'None'}, "
            f"{len(proposal.das)} DAS, {len(proposal.ccam)} CCAM."
        )

        # Erreurs DIM détectées
        if dim_errors:
            reasoning_parts.append(
                f"{len(dim_errors)} erreur(s) sensible(s) DIM détectée(s):"
            )
            for error in dim_errors:
                reasoning_parts.append(
                    f"  - [{error.severity.upper()}] {error.error_type}: {error.message}"
                )
        else:
            reasoning_parts.append("Aucune erreur sensible DIM détectée.")

        # Contradictions
        if contradictions:
            reasoning_parts.append(
                f"{len(contradictions)} contradiction(s) détectée(s):"
            )
            for contradiction in contradictions:
                reasoning_parts.append(f"  - {contradiction}")
        else:
            reasoning_parts.append("Aucune contradiction détectée.")

        # Alternatives
        if alternatives:
            reasoning_parts.append(
                f"{len(alternatives)} alternative(s) suggérée(s):"
            )
            for alt in alternatives:
                reasoning_parts.append(
                    f"  - {alt.code} ({alt.type}): {alt.label} "
                    f"(confiance: {alt.confidence:.2f})"
                )

        # Décision
        decision_labels = {
            "accept": "ACCEPTÉE - La proposition est validée sans réserve",
            "veto": "REJETÉE (VETO) - Contradictions bloquantes détectées, arbitrage TIM requis",
            "review": "À REVOIR - Contradictions non-bloquantes détectées, vérification TIM recommandée",
        }
        reasoning_parts.append(
            f"Décision du Vérificateur: {decision_labels.get(decision, decision)}."
        )

        # Conclusion
        if decision == "accept":
            reasoning_parts.append(
                "La proposition du Codeur est conforme aux exigences de qualité "
                "et peut être validée."
            )
        elif decision == "veto":
            reasoning_parts.append(
                "La proposition contient des erreurs critiques qui nécessitent "
                "un arbitrage TIM avant validation."
            )
        else:  # review
            reasoning_parts.append(
                "La proposition nécessite une vérification TIM pour résoudre "
                "les contradictions non-bloquantes."
            )

        return " ".join(reasoning_parts)
