"""
Codeur - Proposition de codes CIM-10/CCAM avec justifications.

Ce module implémente le Codeur qui propose des codes DP, DR, DAS et CCAM
en utilisant:
- Les faits cliniques extraits
- Le moteur RAG pour la recherche de codes
- Un LLM pour la génération de raisonnements et la sélection de codes

Pour le POC, une approche basée sur des règles simples est utilisée
avec génération de raisonnements structurés.
"""

import hashlib
import logging
from datetime import datetime
from typing import Dict, List, Optional, Tuple

from pipeline_mco_pmsi.models.clinical import ClinicalFact, Evidence
from pipeline_mco_pmsi.models.coding import Code, CodeCandidate, CodingProposal
from pipeline_mco_pmsi.models.metadata import ModelVersion, StayMetadata
from pipeline_mco_pmsi.rag.rag_engine import RAGEngine

logger = logging.getLogger(__name__)


class Codeur:
    """
    Codeur - Propose des codes CIM-10/CCAM avec justifications.

    Responsabilités:
    - Proposer le Diagnostic Principal (DP) avec justification
    - Proposer le Diagnostic Relié (DR) si applicable
    - Proposer les Diagnostics Associés Significatifs (DAS)
    - Proposer les codes d'actes CCAM
    - Calculer les scores de confiance basés sur la qualité des preuves
    - Générer des raisonnements pour chaque code
    - Appliquer l'approche conservative (pas de codes pour faits niés/suspectés)

    Exigences: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 16.2
    """

    def __init__(
        self,
        rag_engine: RAGEngine,
        model_name: str = "mock-llm",
        model_version: str = "1.0.0",
        prompt_version: str = "1.0.0",
        conservative_mode: bool = True,
        llm_client=None,
    ):
        """
        Initialise le Codeur.

        Args:
            rag_engine: Moteur RAG pour la recherche de codes
            model_name: Nom du modèle LLM (pour le POC: "mock-llm")
            model_version: Version du modèle
            prompt_version: Version du prompt
            conservative_mode: Mode conservateur (rejeter faits niés/suspectés)
            llm_client: Client LLM (OllamaClient) pour génération assistée par IA (optionnel)
        """
        self.rag_engine = rag_engine
        self.model_name = model_name
        self.model_version_str = model_version
        self.prompt_version = prompt_version
        self.conservative_mode = conservative_mode
        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"Codeur initialisé avec modèle={model_name}, "
            f"version={model_version}, conservative_mode={conservative_mode}, "
            f"llm_enabled={llm_client is not None}"
        )

    def propose_codes(
        self,
        facts: List[ClinicalFact],
        stay_metadata: StayMetadata,
        cim10_version: str = "2026",
        ccam_version: str = "2025",
    ) -> CodingProposal:
        """
        Propose les codes DP, DR, DAS et CCAM pour un séjour.

        Pipeline:
        1. Filtrer les faits selon l'approche conservative
        2. Rechercher les codes candidats via RAG
        3. Sélectionner le DP (diagnostic principal)
        4. Sélectionner le DR (diagnostic relié) si applicable
        5. Sélectionner les DAS (diagnostics associés significatifs)
        6. Sélectionner les actes CCAM
        7. Calculer les scores de confiance
        8. Générer les raisonnements

        Args:
            facts: Liste de faits cliniques extraits
            stay_metadata: Métadonnées du séjour
            cim10_version: Version du référentiel CIM-10
            ccam_version: Version du référentiel CCAM

        Returns:
            CodingProposal avec codes proposés et justifications

        Exigences: 8.1, 8.2, 8.3, 8.4, 1.1, 1.2, 2.4, 2.5, 2.6
        """
        logger.info(f"Proposition de codes pour séjour {stay_metadata.stay_id}")

        # 1. Filtrer les faits selon l'approche conservative
        filtered_facts = self._filter_facts_conservative(facts)
        logger.info(
            f"Faits filtrés: {len(filtered_facts)}/{len(facts)} "
            f"(mode conservateur: {self.conservative_mode})"
        )

        # 2. Rechercher les codes candidats pour chaque fait
        fact_candidates = self._search_candidates_for_facts(
            filtered_facts, cim10_version, ccam_version
        )

        # 3. Sélectionner le DP (Diagnostic Principal)
        dp = self._select_dp(filtered_facts, fact_candidates, cim10_version)

        # 4. Sélectionner le DR (Diagnostic Relié) si applicable
        dr = self._select_dr(filtered_facts, fact_candidates, dp, cim10_version)

        # 5. Sélectionner les DAS (Diagnostics Associés Significatifs)
        das = self._select_das(filtered_facts, fact_candidates, dp, dr, cim10_version)

        # 6. Sélectionner les actes CCAM
        ccam = self._select_ccam(filtered_facts, fact_candidates, ccam_version)

        # 7. Générer le raisonnement global
        reasoning = self._generate_global_reasoning(
            dp, dr, das, ccam, filtered_facts, stay_metadata
        )

        # 8. Créer la proposition
        model_version = ModelVersion(
            model_name=self.model_name,
            model_tag=self.model_version_str,
            model_digest=self.model_digest,
            quantization=None,
        )

        proposal = CodingProposal(
            stay_id=stay_metadata.stay_id,
            dp=dp,
            dr=dr,
            das=das,
            ccam=ccam,
            reasoning=reasoning,
            model_version=model_version,
            prompt_version=self.prompt_version,
        )

        logger.info(
            f"Proposition générée: DP={dp.code if dp else None}, "
            f"DR={dr.code if dr else None}, "
            f"DAS={len(das)}, CCAM={len(ccam)}"
        )

        return proposal

    def _filter_facts_conservative(
        self, facts: List[ClinicalFact]
    ) -> List[ClinicalFact]:
        """
        Filtre les faits selon l'approche conservative.

        En mode conservateur:
        - Rejeter les faits niés (qualifier.certainty == "nié")
        - Rejeter les faits suspectés sans confirmation
        - Rejeter les antécédents pour le DP

        Args:
            facts: Liste de faits cliniques

        Returns:
            Liste de faits filtrés

        Exigences: 2.4, 2.5, 2.6
        """
        if not self.conservative_mode:
            return facts

        filtered = []
        for fact in facts:
            # Rejeter les faits niés (Exigence 2.4)
            if fact.qualifier.certainty == "nié":
                logger.debug(
                    f"Fait rejeté (nié): {fact.text} "
                    f"(markers: {fact.qualifier.markers})"
                )
                continue

            # Rejeter les faits suspectés pour le DP (Exigence 2.5)
            # Note: Les faits suspectés peuvent être utilisés pour DAS/DR
            # mais pas pour DP. Le filtrage spécifique au DP est fait
            # dans _select_dp()

            # Rejeter les antécédents pour le DP (Exigence 2.6)
            # Note: Les antécédents peuvent être utilisés pour DAS
            # mais pas pour DP. Le filtrage spécifique au DP est fait
            # dans _select_dp()

            filtered.append(fact)

        return filtered

    def _search_candidates_for_facts(
        self,
        facts: List[ClinicalFact],
        cim10_version: str,
        ccam_version: str,
    ) -> Dict[str, List[CodeCandidate]]:
        """
        Recherche les codes candidats pour chaque fait via RAG.

        Args:
            facts: Liste de faits cliniques
            cim10_version: Version du référentiel CIM-10
            ccam_version: Version du référentiel CCAM

        Returns:
            Dictionnaire {fact_id: [candidats]}

        Exigences: 7.1, 7.2, 7.3
        """
        fact_candidates = {}

        for fact in facts:
            # Déterminer le type de recherche selon le type de fait
            if fact.type in ["diagnostic", "complication"]:
                # Recherche CIM-10
                try:
                    candidates = self.rag_engine.search_icd10(
                        query=fact.text, top_k=5, version=cim10_version
                    )
                    fact_candidates[fact.fact_id] = candidates
                    logger.debug(
                        f"Trouvé {len(candidates)} candidats CIM-10 pour: {fact.text}"
                    )
                except Exception as e:
                    logger.error(f"Erreur recherche CIM-10 pour {fact.text}: {e}")
                    fact_candidates[fact.fact_id] = []

            elif fact.type == "acte":
                # Recherche CCAM
                try:
                    candidates = self.rag_engine.search_ccam(
                        query=fact.text, top_k=5, version=ccam_version
                    )
                    fact_candidates[fact.fact_id] = candidates
                    logger.debug(
                        f"Trouvé {len(candidates)} candidats CCAM pour: {fact.text}"
                    )
                except Exception as e:
                    logger.error(f"Erreur recherche CCAM pour {fact.text}: {e}")
                    fact_candidates[fact.fact_id] = []

            else:
                # Autres types de faits (examen, traitement, antécédent)
                # ne génèrent pas de codes directement
                fact_candidates[fact.fact_id] = []

        return fact_candidates

    def _select_dp(
        self,
        facts: List[ClinicalFact],
        fact_candidates: Dict[str, List[CodeCandidate]],
        cim10_version: str,
    ) -> Optional[Code]:
        """
        Sélectionne le Diagnostic Principal (DP).

        Règles de sélection:
        - Le DP doit être un diagnostic affirmé (pas nié, pas suspecté)
        - Le DP doit être actuel (pas un antécédent)
        - Le DP doit avoir au moins une preuve
        - Prioriser les diagnostics avec confiance élevée
        - Prioriser les complications sur les diagnostics simples

        Args:
            facts: Liste de faits cliniques
            fact_candidates: Candidats par fait
            cim10_version: Version du référentiel CIM-10

        Returns:
            Code DP ou None si aucun DP trouvé

        Exigences: 8.1, 2.5, 2.6, 1.1
        """
        logger.debug("Sélection du Diagnostic Principal (DP)")

        # Filtrer les faits éligibles pour le DP
        eligible_facts = []
        for fact in facts:
            # Doit être un diagnostic ou une complication
            if fact.type not in ["diagnostic", "complication"]:
                continue

            # Doit être affirmé (Exigence 2.5)
            if fact.qualifier.certainty != "affirmé":
                logger.debug(
                    f"Fait non éligible pour DP (non affirmé): {fact.text} "
                    f"(certainty={fact.qualifier.certainty})"
                )
                continue

            # Doit être actuel (Exigence 2.6)
            if fact.temporality != "actuel":
                logger.debug(
                    f"Fait non éligible pour DP (non actuel): {fact.text} "
                    f"(temporality={fact.temporality})"
                )
                continue

            # Doit avoir des candidats
            if fact.fact_id not in fact_candidates or not fact_candidates[fact.fact_id]:
                logger.debug(f"Fait sans candidats pour DP: {fact.text}")
                continue

            eligible_facts.append(fact)

        if not eligible_facts:
            logger.warning("Aucun fait éligible pour le DP")
            return None

        # Prioriser les complications sur les diagnostics simples
        complications = [f for f in eligible_facts if f.type == "complication"]
        if complications:
            eligible_facts = complications
            logger.debug(f"Priorisation des complications: {len(complications)} faits")

        # Sélectionner le fait avec la confiance la plus élevée
        best_fact = max(eligible_facts, key=lambda f: f.confidence)
        logger.debug(
            f"Meilleur fait pour DP: {best_fact.text} (confidence={best_fact.confidence})"
        )

        # Sélectionner le meilleur candidat pour ce fait
        candidates = fact_candidates[best_fact.fact_id]
        best_candidate = candidates[0]  # Déjà trié par score

        # Créer le code DP
        dp_code = self._create_code(
            candidate=best_candidate,
            fact=best_fact,
            code_type="dp",
            referentiel_version=cim10_version,
        )

        logger.info(f"DP sélectionné: {dp_code.code} - {dp_code.label}")
        return dp_code

    def _select_dr(
        self,
        facts: List[ClinicalFact],
        fact_candidates: Dict[str, List[CodeCandidate]],
        dp: Optional[Code],
        cim10_version: str,
    ) -> Optional[Code]:
        """
        Sélectionne le Diagnostic Relié (DR) si applicable.

        Le DR est le diagnostic qui a motivé l'hospitalisation mais qui n'est
        pas le diagnostic principal (ex: traumatisme initial pour une complication).

        Args:
            facts: Liste de faits cliniques
            fact_candidates: Candidats par fait
            dp: Diagnostic Principal déjà sélectionné
            cim10_version: Version du référentiel CIM-10

        Returns:
            Code DR ou None si non applicable

        Exigences: 8.2
        """
        logger.debug("Sélection du Diagnostic Relié (DR)")

        # Pour le POC, on ne propose pas de DR systématiquement
        # Le DR est complexe à déterminer et nécessite une analyse
        # de la relation causale entre diagnostics

        # Règle simple: si le DP est une complication, chercher le diagnostic initial
        if dp and dp.type == "dp":
            # Chercher un diagnostic non-complication qui pourrait être le DR
            for fact in facts:
                if fact.type != "diagnostic":
                    continue

                if fact.qualifier.certainty != "affirmé":
                    continue

                if fact.temporality != "actuel":
                    continue

                if fact.fact_id not in fact_candidates or not fact_candidates[fact.fact_id]:
                    continue

                # Vérifier que ce n'est pas le même code que le DP
                candidates = fact_candidates[fact.fact_id]
                if candidates and candidates[0].code != dp.code:
                    # Créer le code DR
                    dr_code = self._create_code(
                        candidate=candidates[0],
                        fact=fact,
                        code_type="dr",
                        referentiel_version=cim10_version,
                    )
                    logger.info(f"DR sélectionné: {dr_code.code} - {dr_code.label}")
                    return dr_code

        logger.debug("Aucun DR applicable")
        return None

    def _select_das(
        self,
        facts: List[ClinicalFact],
        fact_candidates: Dict[str, List[CodeCandidate]],
        dp: Optional[Code],
        dr: Optional[Code],
        cim10_version: str,
    ) -> List[Code]:
        """
        Sélectionne les Diagnostics Associés Significatifs (DAS).

        Les DAS sont les diagnostics qui ont eu un impact sur la prise en charge
        mais qui ne sont ni le DP ni le DR.

        Args:
            facts: Liste de faits cliniques
            fact_candidates: Candidats par fait
            dp: Diagnostic Principal
            dr: Diagnostic Relié
            cim10_version: Version du référentiel CIM-10

        Returns:
            Liste de codes DAS

        Exigences: 8.3
        """
        logger.debug("Sélection des Diagnostics Associés Significatifs (DAS)")

        das_codes = []
        used_codes = set()

        # Ajouter les codes déjà utilisés
        if dp:
            used_codes.add(dp.code)
        if dr:
            used_codes.add(dr.code)

        # Parcourir les faits pour trouver les DAS
        for fact in facts:
            # Doit être un diagnostic ou une complication
            if fact.type not in ["diagnostic", "complication"]:
                continue

            # Doit être affirmé (les DAS peuvent inclure des suspicions confirmées)
            if fact.qualifier.certainty == "nié":
                continue

            # Les antécédents peuvent être des DAS s'ils ont impacté la prise en charge
            # (ex: diabète, insuffisance rénale chronique)

            # Doit avoir des candidats
            if fact.fact_id not in fact_candidates or not fact_candidates[fact.fact_id]:
                continue

            # Sélectionner le meilleur candidat
            candidates = fact_candidates[fact.fact_id]
            best_candidate = candidates[0]

            # Vérifier que le code n'est pas déjà utilisé
            if best_candidate.code in used_codes:
                continue

            # Créer le code DAS
            das_code = self._create_code(
                candidate=best_candidate,
                fact=fact,
                code_type="das",
                referentiel_version=cim10_version,
            )

            das_codes.append(das_code)
            used_codes.add(das_code.code)

            logger.debug(f"DAS ajouté: {das_code.code} - {das_code.label}")

        logger.info(f"{len(das_codes)} DAS sélectionnés")
        return das_codes

    def _select_ccam(
        self,
        facts: List[ClinicalFact],
        fact_candidates: Dict[str, List[CodeCandidate]],
        ccam_version: str,
    ) -> List[Code]:
        """
        Sélectionne les codes d'actes CCAM.

        Args:
            facts: Liste de faits cliniques
            fact_candidates: Candidats par fait
            ccam_version: Version du référentiel CCAM

        Returns:
            Liste de codes CCAM

        Exigences: 8.4, 1.2
        """
        logger.debug("Sélection des actes CCAM")

        ccam_codes = []
        used_codes = set()

        # Parcourir les faits de type "acte"
        for fact in facts:
            if fact.type != "acte":
                continue

            # Doit être affirmé
            if fact.qualifier.certainty != "affirmé":
                continue

            # Doit avoir des candidats
            if fact.fact_id not in fact_candidates or not fact_candidates[fact.fact_id]:
                logger.debug(f"Acte sans candidats CCAM: {fact.text}")
                continue

            # Sélectionner le meilleur candidat
            candidates = fact_candidates[fact.fact_id]
            best_candidate = candidates[0]

            # Vérifier que le code n'est pas déjà utilisé
            if best_candidate.code in used_codes:
                continue

            # Créer le code CCAM
            ccam_code = self._create_code(
                candidate=best_candidate,
                fact=fact,
                code_type="ccam",
                referentiel_version=ccam_version,
            )

            ccam_codes.append(ccam_code)
            used_codes.add(ccam_code.code)

            logger.debug(f"CCAM ajouté: {ccam_code.code} - {ccam_code.label}")

        logger.info(f"{len(ccam_codes)} actes CCAM sélectionnés")
        return ccam_codes

    def _create_code(
        self,
        candidate: CodeCandidate,
        fact: ClinicalFact,
        code_type: str,
        referentiel_version: str,
    ) -> Code:
        """
        Crée un objet Code à partir d'un candidat et d'un fait.

        Args:
            candidate: Candidat de code sélectionné
            fact: Fait clinique source
            code_type: Type de code (dp, dr, das, ccam)
            referentiel_version: Version du référentiel

        Returns:
            Code avec preuves, confiance et raisonnement

        Exigences: 1.1, 1.2, 8.5, 8.6
        """
        # Utiliser la preuve du fait comme preuve du code (Exigence 1.1, 1.2)
        evidence = [fact.evidence]

        # Calculer le score de confiance (Exigence 8.5)
        confidence = self.assign_confidence(candidate, fact)

        # Générer le raisonnement (Exigence 8.6)
        reasoning = self._generate_code_reasoning(candidate, fact, code_type)

        code = Code(
            code=candidate.code,
            label=candidate.label,
            type=code_type,
            evidence=evidence,
            confidence=confidence,
            reasoning=reasoning,
            referentiel_version=referentiel_version,
        )

        return code

    def assign_confidence(
        self, candidate: CodeCandidate, fact: ClinicalFact
    ) -> float:
        """
        Calcule le score de confiance pour un code.

        Le score de confiance est basé sur:
        - Le score de similarité du candidat RAG
        - La confiance dans l'extraction du fait
        - La confiance dans le qualificateur
        - La qualité de la preuve

        Args:
            candidate: Candidat de code
            fact: Fait clinique source

        Returns:
            Score de confiance [0.0, 1.0]

        Exigences: 8.5
        """
        # Combiner les scores avec des poids
        # - 40% score de similarité RAG
        # - 30% confiance du fait
        # - 30% confiance du qualificateur

        confidence = (
            0.4 * candidate.similarity_score
            + 0.3 * fact.confidence
            + 0.3 * fact.qualifier.confidence
        )

        # Pénaliser les faits suspectés
        if fact.qualifier.certainty == "suspecté":
            confidence *= 0.7

        # Pénaliser les antécédents (moins pertinents pour l'épisode actuel)
        if fact.temporality == "antecedent":
            confidence *= 0.8

        # Assurer que le score est dans [0.0, 1.0]
        confidence = max(0.0, min(1.0, confidence))

        return confidence

    def _generate_code_reasoning(
        self, candidate: CodeCandidate, fact: ClinicalFact, code_type: str
    ) -> str:
        """
        Génère le raisonnement pour la sélection d'un code.

        Args:
            candidate: Candidat de code sélectionné
            fact: Fait clinique source
            code_type: Type de code

        Returns:
            Raisonnement textuel

        Exigences: 8.6
        """
        reasoning_parts = []

        # Type de code
        code_type_labels = {
            "dp": "Diagnostic Principal",
            "dr": "Diagnostic Relié",
            "das": "Diagnostic Associé Significatif",
            "ccam": "Acte médical",
        }
        reasoning_parts.append(
            f"Code proposé comme {code_type_labels.get(code_type, code_type)}."
        )

        # Fait source
        reasoning_parts.append(
            f"Basé sur le fait clinique: \"{fact.text}\" "
            f"(type: {fact.type}, certitude: {fact.qualifier.certainty}, "
            f"temporalité: {fact.temporality})."
        )

        # Similarité RAG
        reasoning_parts.append(
            f"Score de similarité RAG: {candidate.similarity_score:.2f}."
        )

        # Preuve
        reasoning_parts.append(
            f"Preuve textuelle: \"{fact.evidence.text}\" "
            f"(document: {fact.evidence.document_id})."
        )

        # Qualificateurs
        if fact.qualifier.markers:
            reasoning_parts.append(
                f"Marqueurs linguistiques détectés: {', '.join(fact.qualifier.markers)}."
            )

        # Justification spécifique au type
        if code_type == "dp":
            reasoning_parts.append(
                "Sélectionné comme DP car il s'agit du diagnostic principal "
                "ayant motivé la prise en charge."
            )
        elif code_type == "dr":
            reasoning_parts.append(
                "Sélectionné comme DR car il s'agit du diagnostic initial "
                "ayant conduit au DP."
            )
        elif code_type == "das":
            reasoning_parts.append(
                "Sélectionné comme DAS car il a eu un impact significatif "
                "sur la prise en charge."
            )
        elif code_type == "ccam":
            reasoning_parts.append(
                "Acte médical réalisé pendant le séjour."
            )

        return " ".join(reasoning_parts)

    def _generate_global_reasoning(
        self,
        dp: Optional[Code],
        dr: Optional[Code],
        das: List[Code],
        ccam: List[Code],
        facts: List[ClinicalFact],
        stay_metadata: StayMetadata,
    ) -> str:
        """
        Génère le raisonnement global pour la proposition de codage.

        Args:
            dp: Diagnostic Principal
            dr: Diagnostic Relié
            das: Diagnostics Associés Significatifs
            ccam: Actes CCAM
            facts: Faits cliniques
            stay_metadata: Métadonnées du séjour

        Returns:
            Raisonnement global

        Exigences: 8.6
        """
        reasoning_parts = []

        # Contexte du séjour
        reasoning_parts.append(
            f"Analyse du séjour {stay_metadata.stay_id} "
            f"(spécialité: {stay_metadata.specialty}, "
            f"durée: {(stay_metadata.discharge_date - stay_metadata.admission_date).days} jours)."
        )

        # Nombre de faits analysés
        reasoning_parts.append(
            f"{len(facts)} faits cliniques ont été extraits et analysés."
        )

        # Résumé des codes proposés
        if dp:
            reasoning_parts.append(
                f"Diagnostic Principal (DP): {dp.code} - {dp.label} "
                f"(confiance: {dp.confidence:.2f})."
            )
        else:
            reasoning_parts.append(
                "Aucun Diagnostic Principal (DP) n'a pu être déterminé avec certitude."
            )

        if dr:
            reasoning_parts.append(
                f"Diagnostic Relié (DR): {dr.code} - {dr.label} "
                f"(confiance: {dr.confidence:.2f})."
            )

        if das:
            reasoning_parts.append(
                f"{len(das)} Diagnostic(s) Associé(s) Significatif(s) (DAS) proposé(s)."
            )

        if ccam:
            reasoning_parts.append(
                f"{len(ccam)} acte(s) CCAM proposé(s)."
            )

        # Approche conservative
        reasoning_parts.append(
            "Approche conservative appliquée: les diagnostics niés ou suspectés "
            "sans confirmation n'ont pas été codés comme DP. "
            "Les antécédents n'ont pas été proposés comme DP."
        )

        # Traçabilité
        reasoning_parts.append(
            "Tous les codes proposés sont justifiés par des preuves textuelles "
            "extraites des documents cliniques."
        )

        return " ".join(reasoning_parts)
