"""
Pipeline principal - Orchestration de tous les composants du système de codage MCO PMSI.

Ce module implémente la classe Pipeline qui orchestre séquentiellement tous les
composants du système pour traiter un séjour complet, de l'ingestion des documents
jusqu'à l'export de l'audit.

Exigences: 1.1, 4.1, 5.1, 14.1, 14.6, 15.4, 15.5, 15.6
"""

import logging
import time
import uuid
from typing import Dict, List, Optional

from sqlalchemy.orm import Session

from .audit.audit_logger import AuditLogger
from .coders.codeur import Codeur
from .extractors.clinical_facts_extractor import ClinicalFactsExtractor
from .models.clinical import ClinicalDocument, ClinicalFact, StructuredStay
from .models.coding import CodingProposal, VerificationResult
from .models.metadata import ModelVersion, ReferentielVersion, StayMetadata, VersionInfo
from .models.validation import GroupageResult, Question, ValidationIssue
from .processors.document_processor import DocumentProcessor
from .processors.pii_protector import PIIProtector
from .rag.rag_engine import RAGEngine
from .rules.rules_manager import RulesManager
from .validators.groupage_validator import GroupageValidator
from .validators.pmsi_validator import PMSIValidator
from .validators.question_generator import QuestionGenerator
from .verifiers.verificateur import Verificateur

logger = logging.getLogger(__name__)


class PipelineResult:
    """Résultat complet du traitement d'un séjour par le pipeline."""

    def __init__(
        self,
        stay_id: str,
        structured_stay: StructuredStay,
        coding_proposal: Optional[CodingProposal],
        verification_result: Optional[VerificationResult],
        validation_issues: List[ValidationIssue],
        questions: List[Question],
        groupage_result: Optional[GroupageResult],
        versions: VersionInfo,
        success: bool,
        error_message: Optional[str] = None,
    ):
        """
        Initialise le résultat du pipeline.

        Args:
            stay_id: Identifiant du séjour
            structured_stay: Séjour structuré avec documents et faits
            coding_proposal: Proposition de codage (None si échec)
            verification_result: Résultat de vérification (None si échec)
            validation_issues: Problèmes de validation détectés
            questions: Questions générées pour le TIM
            groupage_result: Résultat du groupage (None si échec)
            versions: Informations de version complètes
            success: True si le traitement a réussi
            error_message: Message d'erreur si échec
        """
        self.stay_id = stay_id
        self.structured_stay = structured_stay
        self.coding_proposal = coding_proposal
        self.verification_result = verification_result
        self.validation_issues = validation_issues
        self.questions = questions
        self.groupage_result = groupage_result
        self.versions = versions
        self.success = success
        self.error_message = error_message

    def has_blocking_issues(self) -> bool:
        """Vérifie si des problèmes bloquants sont présents."""
        return any(issue.severity == "bloquant" for issue in self.validation_issues)

    def can_auto_validate(self) -> bool:
        """Vérifie si la validation automatique est possible."""
        return (
            self.success
            and not self.has_blocking_issues()
            and self.verification_result is not None
            and self.verification_result.decision == "accept"
        )


class Pipeline:
    """
    Pipeline principal orchestrant tous les composants du système.

    Le pipeline traite un séjour en 8 étapes séquentielles:
    1. DocumentProcessor: Segmentation et structuration des documents
    2. PIIProtector: Filtrage des données identifiantes
    3. ClinicalFactsExtractor: Extraction de faits structurés
    4. Codeur: Proposition de codes avec RAG
    5. Vérificateur: Validation indépendante
    6. GroupageValidator: Validation tarifaire
    7. PMSIValidator + QuestionGenerator: Validation PMSI et questions
    8. AuditLogger: Enregistrement complet

    Exigences: 1.1, 4.1, 5.1
    """

    def __init__(
        self,
        db_session: Session,
        rag_engine: RAGEngine,
        model_name: str = "mock-llm",
        model_version: str = "1.0.0",
        codeur_prompt_version: str = "codeur-1.0.0",
        verificateur_prompt_version: str = "verificateur-1.0.0",
        groupage_version: str = "2026",
        rules_version: str = "1.0.0",
        rules_manager: Optional[RulesManager] = None,
        conservative_mode: bool = True,
        max_retries: int = 3,
        retry_delay: float = 1.0,
        timeout: Optional[float] = None,
        partial_results_mode: bool = False,
    ):
        """
        Initialise le pipeline avec tous ses composants.

        Args:
            db_session: Session SQLAlchemy pour persistance
            rag_engine: Moteur RAG pour recherche de codes
            model_name: Nom du modèle LLM
            model_version: Version du modèle LLM
            codeur_prompt_version: Version du prompt du Codeur
            verificateur_prompt_version: Version du prompt du Vérificateur
            groupage_version: Version de la fonction de groupage
            rules_version: Version des règles de codage
            rules_manager: Gestionnaire de règles configurables (optionnel)
            conservative_mode: Mode conservateur (rejeter faits niés/suspectés)
            max_retries: Nombre maximum de tentatives en cas d'erreur
            retry_delay: Délai initial entre les tentatives (secondes)
            timeout: Timeout maximum pour le traitement (secondes)
            partial_results_mode: Activer le mode résultats partiels
        """
        self.db_session = db_session
        self.rag_engine = rag_engine

        # Versions
        self.model_name = model_name
        self.model_version = model_version
        self.codeur_prompt_version = codeur_prompt_version
        self.verificateur_prompt_version = verificateur_prompt_version
        self.groupage_version = groupage_version
        self.rules_version = rules_version

        # Configuration de gestion des erreurs
        self.max_retries = max_retries
        self.retry_delay = retry_delay
        self.timeout = timeout
        self.partial_results_mode = partial_results_mode

        # Initialiser le client LLM Ollama
        from pipeline_mco_pmsi.llm.ollama_client import OllamaClient
        try:
            ollama_client = OllamaClient(
                model="mistral-large-3:675b-cloud",
                base_url="http://localhost:11434",
                timeout=300
            )
            # Vérifier la disponibilité d'Ollama
            if ollama_client.check_availability():
                logger.info("✅ Ollama disponible - LLM activé")
                self.ollama_client = ollama_client
            else:
                logger.warning("⚠️  Ollama non disponible - Mode règles uniquement")
                self.ollama_client = None
        except Exception as e:
            logger.warning(f"⚠️  Erreur initialisation Ollama: {e} - Mode règles uniquement")
            self.ollama_client = None

        # Initialiser les composants
        self.document_processor = DocumentProcessor()
        self.pii_protector = PIIProtector()
        self.clinical_facts_extractor = ClinicalFactsExtractor(llm_client=self.ollama_client)
        self.codeur = Codeur(
            rag_engine=rag_engine,
            model_name=model_name,
            model_version=model_version,
            prompt_version=codeur_prompt_version,
            conservative_mode=conservative_mode,
            llm_client=self.ollama_client,
        )
        self.verificateur = Verificateur(
            rag_engine=rag_engine,
            model_name=model_name,
            model_version=model_version,
            llm_client=self.ollama_client,
        )
        self.groupage_validator = GroupageValidator(groupage_version=groupage_version)
        self.pmsi_validator = PMSIValidator(
            rag_engine=rag_engine,
            rules_manager=rules_manager,
        )
        self.question_generator = QuestionGenerator()
        self.audit_logger = AuditLogger(
            db_session=db_session, pii_protector=self.pii_protector
        )

        logger.info(
            f"Pipeline initialisé: model={model_name}, "
            f"codeur_prompt={codeur_prompt_version}, "
            f"verificateur_prompt={verificateur_prompt_version}, "
            f"groupage={groupage_version}, "
            f"rules_manager={'activé' if rules_manager else 'désactivé'}, "
            f"max_retries={max_retries}, "
            f"timeout={timeout}s, "
            f"partial_results={partial_results_mode}"
        )

    def process_stay(
        self,
        documents: List[ClinicalDocument],
        stay_metadata: StayMetadata,
        cim10_version: str = "2026",
        ccam_version: str = "2025",
    ) -> PipelineResult:
        """
        Traite un séjour complet à travers le pipeline.

        Pipeline séquentiel:
        1. DocumentProcessor: Segmentation et structuration
        2. PIIProtector: Filtrage DIP
        3. ClinicalFactsExtractor: Extraction de faits
        4. Détection de contradictions multi-documents
        5. Codeur: Proposition de codes (avec RAG)
        6. Vérificateur: Validation indépendante
        7. GroupageValidator: Validation tarifaire
        8. PMSIValidator + QuestionGenerator: Validation PMSI
        9. AuditLogger: Enregistrement complet

        Args:
            documents: Liste de documents cliniques
            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:
            PipelineResult avec tous les résultats du traitement

        Exigences: 1.1, 4.1, 5.1, 14.1, 14.6, 15.4, 15.5, 15.6
        """
        logger.info(f"Début du traitement du séjour {stay_metadata.stay_id}")
        logger.info(f"Nombre de documents: {len(documents)}")

        # Données partielles pour gestion d'erreur
        partial_data: Dict = {"documents": documents}
        start_time = time.time()

        try:
            # Construire les informations de version
            versions = self._build_version_info(cim10_version, ccam_version)
            partial_data["versions"] = versions

            # Vérifier le timeout
            if self.timeout and (time.time() - start_time) > self.timeout:
                raise TimeoutError(f"Timeout dépassé avant le début du traitement")

            # Étape 1: DocumentProcessor - Segmentation et structuration
            logger.info("Étape 1/9: Segmentation des documents")
            structured_stay = self._execute_with_retry(
                "Segmentation des documents",
                self.document_processor.process_documents,
                documents,
                stay_metadata,
            )
            partial_data["structured_stay"] = structured_stay

            # Enregistrer les documents dans l'audit
            self.audit_logger.record_documents(
                stay_metadata.stay_id, documents, versions
            )

            # Étape 2: PIIProtector - Filtrage des DIP (appliqué aux logs)
            logger.info("Étape 2/9: Filtrage des DIP")
            # Le filtrage est appliqué automatiquement par l'audit_logger lors des exports

            # Vérifier le timeout
            if self.timeout and (time.time() - start_time) > self.timeout:
                raise TimeoutError(f"Timeout dépassé après segmentation")

            # Étape 3: ClinicalFactsExtractor - Extraction de faits structurés
            logger.info("Étape 3/9: Extraction des faits cliniques")
            facts = self._execute_with_retry(
                "Extraction des faits cliniques",
                self.clinical_facts_extractor.extract_facts,
                structured_stay,
            )
            structured_stay.facts = facts
            partial_data["structured_stay"] = structured_stay
            logger.info(f"{len(facts)} faits cliniques extraits")

            # Enregistrer les faits dans l'audit
            self.audit_logger.record_facts(stay_metadata.stay_id, facts, versions)

            # Étape 4: Détection de contradictions multi-documents
            logger.info("Étape 4/9: Détection de contradictions multi-documents")
            contradiction_issues = self._detect_multi_document_contradictions(
                facts, documents
            )
            if contradiction_issues:
                logger.warning(
                    f"{len(contradiction_issues)} contradictions détectées entre documents"
                )

            # Vérifier le timeout
            if self.timeout and (time.time() - start_time) > self.timeout:
                raise TimeoutError(f"Timeout dépassé après extraction des faits")

            # Étape 5: Codeur - Proposition de codes avec RAG
            logger.info("Étape 5/9: Proposition de codes (Codeur)")
            coding_proposal = self._execute_with_retry(
                "Proposition de codes",
                self.codeur.propose_codes,
                facts,
                stay_metadata,
                cim10_version,
                ccam_version,
            )
            partial_data["coding_proposal"] = coding_proposal
            logger.info(
                f"Codes proposés: DP={coding_proposal.dp.code if coding_proposal.dp else None}, "
                f"DR={coding_proposal.dr.code if coding_proposal.dr else None}, "
                f"{len(coding_proposal.das)} DAS, {len(coding_proposal.ccam)} CCAM"
            )

            # Enregistrer les codes proposés dans l'audit
            self.audit_logger.record_codes_with_justifications(
                stay_metadata.stay_id, coding_proposal, versions
            )

            # Vérifier le timeout
            if self.timeout and (time.time() - start_time) > self.timeout:
                raise TimeoutError(f"Timeout dépassé après proposition de codes")

            # Étape 6: Vérificateur - Validation indépendante
            logger.info("Étape 6/9: Vérification indépendante (Vérificateur)")
            verification_result = self._execute_with_retry(
                "Vérification indépendante",
                self.verificateur.verify_proposal,
                coding_proposal,
                facts,
                cim10_version,
                ccam_version,
            )
            partial_data["verification_result"] = verification_result
            logger.info(f"Décision du Vérificateur: {verification_result.decision}")

            # Enregistrer la décision du Vérificateur dans l'audit
            self.audit_logger.record_verification_decision(
                stay_metadata.stay_id, verification_result, versions
            )

            # Étape 7: GroupageValidator - Validation tarifaire
            logger.info("Étape 7/9: Validation du groupage")
            groupage_result = self.groupage_validator.validate_groupage(
                coding_proposal, stay_metadata
            )
            partial_data["groupage_result"] = groupage_result
            logger.info(
                f"Groupage: GHM={groupage_result.ghm}, GHS={groupage_result.ghs}, "
                f"{len(groupage_result.groupage_errors)} erreurs"
            )

            # Vérifier le timeout
            if self.timeout and (time.time() - start_time) > self.timeout:
                raise TimeoutError(f"Timeout dépassé après groupage")

            # Étape 8: PMSIValidator + QuestionGenerator - Validation PMSI
            logger.info("Étape 8/9: Validation PMSI et génération de questions")
            validation_issues = self.pmsi_validator.validate_proposal(
                coding_proposal, structured_stay
            )

            # Ajouter les contradictions multi-documents aux problèmes de validation
            validation_issues.extend(contradiction_issues)

            # Vérifier les erreurs zéro-tolérance
            zero_tolerance_issues = self.pmsi_validator.check_zero_tolerance_errors(
                coding_proposal, structured_stay
            )
            validation_issues.extend(zero_tolerance_issues)

            partial_data["validation_issues"] = validation_issues

            logger.info(
                f"{len(validation_issues)} problèmes de validation détectés "
                f"({len([i for i in validation_issues if i.severity == 'bloquant'])} bloquants)"
            )

            # Générer les questions
            questions = self.question_generator.generate_questions(
                coding_proposal, structured_stay, validation_issues
            )
            partial_data["questions"] = questions
            logger.info(f"{len(questions)} questions générées")

            # Étape 9: AuditLogger - Enregistrement complet
            logger.info("Étape 9/9: Enregistrement de l'audit complet")
            self.audit_logger.log_coding_decision(
                stay_metadata.stay_id, coding_proposal, verification_result, versions
            )

            # Calculer le temps de traitement
            processing_time = time.time() - start_time
            logger.info(f"Temps de traitement: {processing_time:.2f}s")

            # Créer le résultat du pipeline
            result = PipelineResult(
                stay_id=stay_metadata.stay_id,
                structured_stay=structured_stay,
                coding_proposal=coding_proposal,
                verification_result=verification_result,
                validation_issues=validation_issues,
                questions=questions,
                groupage_result=groupage_result,
                versions=versions,
                success=True,
            )

            logger.info(
                f"Traitement du séjour {stay_metadata.stay_id} terminé avec succès. "
                f"Validation automatique possible: {result.can_auto_validate()}"
            )

            # Sauvegarder les codes en base de données
            if self.db_session:
                self._save_codes_to_database(stay_metadata.stay_id, coding_proposal)

            return result

        except TimeoutError as e:
            logger.error(f"Timeout pour séjour {stay_metadata.stay_id}: {e}")
            return self._handle_timeout(stay_metadata.stay_id, partial_data)

        except Exception as e:
            logger.error(
                f"Erreur lors du traitement du séjour {stay_metadata.stay_id}: {e}",
                exc_info=True,
            )
            return self._handle_errors(
                stay_metadata.stay_id,
                e,
                error_type="processing",
                partial_data=partial_data,
            )

    def _build_version_info(
        self, cim10_version: str, ccam_version: str
    ) -> VersionInfo:
        """
        Construit les informations de version complètes pour reproductibilité.

        Args:
            cim10_version: Version du référentiel CIM-10
            ccam_version: Version du référentiel CCAM

        Returns:
            VersionInfo avec toutes les versions des composants

        Exigences: 12.2, 12.3, 12.4, 12.5, 12.6
        """
        import hashlib

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

        # Générer les hashes des prompts
        codeur_prompt_hash = hashlib.sha256(
            self.codeur_prompt_version.encode()
        ).hexdigest()
        verificateur_prompt_hash = hashlib.sha256(
            self.verificateur_prompt_version.encode()
        ).hexdigest()

        # Générer le hash des règles
        rules_hash = hashlib.sha256(self.rules_version.encode()).hexdigest()

        # Créer un hash combiné pour les prompts (64 caractères minimum)
        combined_prompt_hash = hashlib.sha256(
            f"{codeur_prompt_hash}{verificateur_prompt_hash}".encode()
        ).hexdigest()

        # Récupérer les versions des référentiels depuis le RAG Engine
        referentiels: Dict[str, ReferentielVersion] = {}

        # CIM-10
        try:
            cim10_info = self.rag_engine.referentiels_manager.get_version_info("cim10")
            if cim10_info:
                referentiels["cim10"] = cim10_info
        except Exception as e:
            logger.warning(f"Impossible de récupérer la version CIM-10: {e}")

        # CCAM
        try:
            ccam_info = self.rag_engine.referentiels_manager.get_version_info("ccam")
            if ccam_info:
                referentiels["ccam"] = ccam_info
        except Exception as e:
            logger.warning(f"Impossible de récupérer la version CCAM: {e}")

        # Guide MCO
        try:
            guide_info = self.rag_engine.referentiels_manager.get_version_info(
                "guide_mco"
            )
            if guide_info:
                referentiels["guide_mco"] = guide_info
        except Exception as e:
            logger.warning(f"Impossible de récupérer la version Guide MCO: {e}")

        # Paramètres d'inférence (pour reproductibilité)
        inference_params = {
            "temperature": 0.0,  # Déterministe pour reproductibilité
            "top_p": 1.0,
            "max_tokens": 2048,
        }

        version_info = VersionInfo(
            model_name=self.model_name,
            model_tag=self.model_version,
            model_digest=model_digest,
            prompt_version=f"codeur={self.codeur_prompt_version},verificateur={self.verificateur_prompt_version}",
            prompt_hash=combined_prompt_hash,
            referentiels=referentiels,
            rules_version=self.rules_version,
            rules_hash=rules_hash,
            groupage_version=self.groupage_version,
            inference_params=inference_params,
        )

        return version_info

    def _save_codes_to_database(
        self, stay_id: str, coding_proposal: CodingProposal
    ) -> None:
        """
        Sauvegarde les codes proposés en base de données.

        Args:
            stay_id: Identifiant du séjour
            coding_proposal: Proposition de codage à sauvegarder
        """
        from .database.models import StayDB, CodeDB, EvidenceDB

        try:
            # Récupérer le séjour
            stay = self.db_session.query(StayDB).filter(StayDB.stay_id == stay_id).first()
            if not stay:
                logger.error(f"Séjour {stay_id} non trouvé en base")
                return

            # Supprimer les anciens codes proposés pour ce séjour
            self.db_session.query(CodeDB).filter(
                CodeDB.stay_id == stay.id,
                CodeDB.status == "proposed"
            ).delete()

            # Récupérer les informations de version depuis le coding_proposal
            model_name = coding_proposal.model_version.model_name
            model_digest = coding_proposal.model_version.model_digest
            prompt_version = coding_proposal.prompt_version

            # Sauvegarder le DP
            if coding_proposal.dp:
                self._save_code(stay.id, coding_proposal.dp, "dp", model_name, model_digest, prompt_version)

            # Sauvegarder le DR
            if coding_proposal.dr:
                self._save_code(stay.id, coding_proposal.dr, "dr", model_name, model_digest, prompt_version)

            # Sauvegarder les DAS
            for das in coding_proposal.das:
                self._save_code(stay.id, das, "das", model_name, model_digest, prompt_version)

            # Sauvegarder les CCAM
            for ccam in coding_proposal.ccam:
                self._save_code(stay.id, ccam, "ccam", model_name, model_digest, prompt_version)

            self.db_session.commit()
            logger.info(f"Codes sauvegardés en base pour le séjour {stay_id}")

        except Exception as e:
            logger.error(f"Erreur lors de la sauvegarde des codes: {e}")
            self.db_session.rollback()

    def _save_code(self, stay_db_id: int, code, code_type: str, model_name: str, model_digest: str, prompt_version: str) -> None:
        """
        Sauvegarde un code individuel en base.

        Args:
            stay_db_id: ID du séjour en base
            code: Code à sauvegarder
            code_type: Type de code (dp, dr, das, ccam)
            model_name: Nom du modèle LLM
            model_digest: Digest du modèle
            prompt_version: Version du prompt
        """
        from .database.models import CodeDB, EvidenceDB, ClinicalDocumentDB

        code_db = CodeDB(
            stay_id=stay_db_id,
            code=code.code,
            label=code.label,
            type=code_type,
            confidence=code.confidence,
            reasoning=code.reasoning,
            referentiel_version=code.referentiel_version,
            status="proposed",
            model_name=model_name,
            model_digest=model_digest,
            prompt_version=prompt_version
        )
        self.db_session.add(code_db)
        self.db_session.flush()  # Pour obtenir l'ID

        # Sauvegarder les preuves
        for evidence in code.evidence:
            # Trouver le document en base
            doc = self.db_session.query(ClinicalDocumentDB).filter(
                ClinicalDocumentDB.document_id == evidence.document_id
            ).first()

            if doc:
                evidence_db = EvidenceDB(
                    code_id=code_db.id,
                    document_id=doc.id,
                    span_start=evidence.span.start,
                    span_end=evidence.span.end,
                    text=evidence.text,
                    context=evidence.context
                )
                self.db_session.add(evidence_db)

    def _detect_multi_document_contradictions(
        self, facts: List[ClinicalFact], documents: List[ClinicalDocument]
    ) -> List[ValidationIssue]:
        """
        Détecte les contradictions entre faits provenant de documents différents.

        Cette méthode identifie les faits contradictoires en comparant:
        - Les diagnostics affirmés vs niés dans différents documents
        - Les informations temporelles contradictoires
        - Les qualificateurs contradictoires pour le même fait

        Args:
            facts: Liste de faits cliniques extraits
            documents: Liste de documents sources

        Returns:
            Liste de ValidationIssue pour les contradictions détectées

        Exigences: 15.4, 15.5, 15.6
        """
        contradictions: List[ValidationIssue] = []

        # Vérifier qu'il y a plusieurs documents
        if len(documents) <= 1:
            return contradictions

        # Grouper les faits par type et texte normalisé
        facts_by_content: Dict[str, List[ClinicalFact]] = {}
        for fact in facts:
            # Normaliser le texte pour la comparaison (minuscules, sans espaces multiples)
            normalized_text = " ".join(fact.text.lower().split())
            key = f"{fact.type}:{normalized_text}"

            if key not in facts_by_content:
                facts_by_content[key] = []
            facts_by_content[key].append(fact)

        # Détecter les contradictions
        for key, fact_group in facts_by_content.items():
            if len(fact_group) < 2:
                continue

            # Vérifier les contradictions de qualificateurs
            qualifiers = set(f.qualifier.certainty for f in fact_group)
            documents_involved = set(f.evidence.document_id for f in fact_group)

            # Contradiction: affirmé dans un document, nié dans un autre
            if "affirmé" in qualifiers and "nié" in qualifiers and len(documents_involved) > 1:
                affected_docs = [f.evidence.document_id for f in fact_group]
                contradictions.append(
                    ValidationIssue(
                        issue_id=f"contradiction_{uuid.uuid4().hex[:8]}",
                        severity="a_revoir",
                        category="contradiction",
                        message=f"Contradiction détectée pour '{fact_group[0].text}': "
                        f"affirmé dans certains documents, nié dans d'autres. "
                        f"Documents concernés: {', '.join(set(affected_docs))}",
                        affected_codes=[],
                        suggested_action="Arbitrage TIM requis pour résoudre la contradiction entre documents",
                    )
                )
                logger.warning(
                    f"Contradiction affirmé/nié détectée: {fact_group[0].text} "
                    f"dans documents {affected_docs}"
                )

            # Contradiction: temporalité contradictoire
            temporalities = set(f.temporality for f in fact_group)
            if len(temporalities) > 1 and len(documents_involved) > 1:
                # Contradiction significative: actuel vs antécédent
                if "actuel" in temporalities and "antécédent" in temporalities:
                    affected_docs = [f.evidence.document_id for f in fact_group]
                    contradictions.append(
                        ValidationIssue(
                            issue_id=f"contradiction_temp_{uuid.uuid4().hex[:8]}",
                            severity="a_revoir",
                            category="contradiction",
                            message=f"Contradiction temporelle pour '{fact_group[0].text}': "
                            f"marqué comme actuel dans certains documents, antécédent dans d'autres. "
                            f"Documents concernés: {', '.join(set(affected_docs))}",
                            affected_codes=[],
                            suggested_action="Vérifier la chronologie et arbitrer la temporalité correcte",
                        )
                    )
                    logger.warning(
                        f"Contradiction temporelle détectée: {fact_group[0].text} "
                        f"dans documents {affected_docs}"
                    )

        # Vérifier la traçabilité: tous les faits doivent avoir un document_id valide
        valid_document_ids = set(doc.document_id for doc in documents)
        for fact in facts:
            if fact.evidence.document_id not in valid_document_ids:
                contradictions.append(
                    ValidationIssue(
                        issue_id=f"data_integrity_{uuid.uuid4().hex[:8]}",
                        severity="bloquant",
                        category="data_integrity",
                        message=f"Fait '{fact.text}' référence un document_id invalide: "
                        f"{fact.evidence.document_id}",
                        affected_codes=[],
                        suggested_action="Vérifier l'intégrité des données et la traçabilité des faits",
                    )
                )
                logger.error(
                    f"Document_id invalide pour fait {fact.fact_id}: {fact.evidence.document_id}"
                )

        return contradictions

    def _execute_with_retry(
        self,
        operation_name: str,
        operation_func,
        *args,
        **kwargs,
    ):
        """
        Exécute une opération avec retry et exponential backoff.

        Args:
            operation_name: Nom de l'opération pour logging
            operation_func: Fonction à exécuter
            *args: Arguments positionnels pour la fonction
            **kwargs: Arguments nommés pour la fonction

        Returns:
            Résultat de l'opération

        Raises:
            Exception: Si toutes les tentatives échouent

        Exigences: 14.1, 14.6
        """
        last_exception = None
        
        for attempt in range(self.max_retries):
            try:
                logger.debug(
                    f"Tentative {attempt + 1}/{self.max_retries} pour {operation_name}"
                )
                result = operation_func(*args, **kwargs)
                
                if attempt > 0:
                    logger.info(
                        f"{operation_name} réussi après {attempt + 1} tentative(s)"
                    )
                
                return result
                
            except Exception as e:
                last_exception = e
                
                if attempt < self.max_retries - 1:
                    # Exponential backoff
                    delay = self.retry_delay * (2 ** attempt)
                    logger.warning(
                        f"{operation_name} échoué (tentative {attempt + 1}/{self.max_retries}): {e}. "
                        f"Nouvelle tentative dans {delay}s..."
                    )
                    time.sleep(delay)
                else:
                    logger.error(
                        f"{operation_name} échoué après {self.max_retries} tentatives: {e}"
                    )
        
        # Toutes les tentatives ont échoué
        raise last_exception

    def _handle_timeout(
        self,
        stay_id: str,
        partial_data: Dict,
    ) -> PipelineResult:
        """
        Gère les timeouts en retournant des résultats partiels si configuré.

        Args:
            stay_id: Identifiant du séjour
            partial_data: Données partielles collectées avant le timeout

        Returns:
            PipelineResult avec résultats partiels ou erreur

        Exigences: 14.6
        """
        if self.partial_results_mode:
            logger.warning(
                f"Timeout pour séjour {stay_id}. "
                f"Retour de résultats partiels."
            )
            
            # Construire un résultat partiel avec les données disponibles
            structured_stay = partial_data.get("structured_stay")
            coding_proposal = partial_data.get("coding_proposal")
            verification_result = partial_data.get("verification_result")
            validation_issues = partial_data.get("validation_issues", [])
            questions = partial_data.get("questions", [])
            groupage_result = partial_data.get("groupage_result")
            versions = partial_data.get("versions")
            
            # Ajouter un problème de validation pour indiquer le timeout
            validation_issues.append(
                ValidationIssue(
                    issue_id=f"timeout_{stay_id}",
                    severity="a_revoir",
                    category="other",
                    message="Le traitement a dépassé le timeout. Résultats partiels retournés.",
                    affected_codes=[],
                    suggested_action="Vérifier manuellement la complétude des résultats",
                )
            )
            
            return PipelineResult(
                stay_id=stay_id,
                structured_stay=structured_stay or StructuredStay(
                    stay_id=stay_id,
                    documents=partial_data.get("documents", []),
                    sections=[],
                    facts=[],
                ),
                coding_proposal=coding_proposal,
                verification_result=verification_result,
                validation_issues=validation_issues,
                questions=questions,
                groupage_result=groupage_result,
                versions=versions or self._build_version_info("2026", "2025"),
                success=False,
                error_message="Timeout - résultats partiels retournés",
            )
        else:
            logger.error(f"Timeout pour séjour {stay_id}. Mode résultats partiels désactivé.")
            # Ne pas lever d'exception, retourner un résultat d'erreur
            structured_stay = partial_data.get("structured_stay")
            if not structured_stay:
                # Créer un séjour minimal avec au moins un document vide si nécessaire
                documents = partial_data.get("documents", [])
                if not documents:
                    # Créer un document vide pour satisfaire la validation
                    from datetime import datetime
                    from .models.clinical import ClinicalDocument
                    documents = [
                        ClinicalDocument(
                            document_id="timeout_placeholder",
                            document_type="autre",
                            content="Timeout de traitement",
                            metadata={"error": "Document placeholder créé suite à un timeout"},
                            creation_date=datetime.utcnow(),
                            priority=5,
                        )
                    ]
                
                structured_stay = StructuredStay(
                    stay_id=stay_id,
                    documents=documents,
                    sections=[],
                    facts=[],
                )
            
            return PipelineResult(
                stay_id=stay_id,
                structured_stay=structured_stay,
                coding_proposal=None,
                verification_result=None,
                validation_issues=[
                    ValidationIssue(
                        issue_id=f"timeout_{stay_id}",
                        severity="bloquant",
                        category="other",
                        message="Le traitement a dépassé le timeout.",
                        affected_codes=[],
                        suggested_action="Augmenter le timeout ou optimiser le traitement",
                    )
                ],
                questions=[],
                groupage_result=None,
                versions=partial_data.get("versions") or self._build_version_info("2026", "2025"),
                success=False,
                error_message="Timeout dépassé",
            )

    def _handle_errors(
        self,
        stay_id: str,
        error: Exception,
        error_type: str = "processing",
        partial_data: Optional[Dict] = None,
    ) -> PipelineResult:
        """
        Gère les erreurs avec logging approprié et résultats partiels si possible.

        Args:
            stay_id: Identifiant du séjour
            error: Exception levée
            error_type: Type d'erreur (processing, validation, groupage, timeout, etc.)
            partial_data: Données partielles collectées avant l'erreur

        Returns:
            PipelineResult avec erreur ou résultats partiels

        Exigences: 14.1, 14.6
        """
        logger.error(
            f"Erreur {error_type} pour séjour {stay_id}: {error}",
            exc_info=True,
        )

        # Gérer les timeouts spécifiquement
        if isinstance(error, TimeoutError) and partial_data:
            return self._handle_timeout(stay_id, partial_data)

        # Pour les autres erreurs, retourner un résultat d'échec
        structured_stay = None
        if partial_data and "structured_stay" in partial_data:
            structured_stay = partial_data["structured_stay"]
        
        if not structured_stay:
            # Créer un séjour minimal avec au moins un document vide si nécessaire
            documents = partial_data.get("documents", []) if partial_data else []
            if not documents:
                # Créer un document vide pour satisfaire la validation
                from datetime import datetime
                from .models.clinical import ClinicalDocument
                documents = [
                    ClinicalDocument(
                        document_id="error_placeholder",
                        document_type="autre",
                        content="Erreur de traitement",
                        metadata={"error": "Document placeholder créé suite à une erreur"},
                        creation_date=datetime.utcnow(),
                        priority=5,
                    )
                ]
            
            structured_stay = StructuredStay(
                stay_id=stay_id,
                documents=documents,
                sections=[],
                facts=[],
            )

        return PipelineResult(
            stay_id=stay_id,
            structured_stay=structured_stay,
            coding_proposal=None,
            verification_result=None,
            validation_issues=[
                ValidationIssue(
                    issue_id=f"error_{error_type}_{stay_id}",
                    severity="bloquant",
                    category="other",
                    message=f"Erreur {error_type}: {str(error)}",
                    affected_codes=[],
                    suggested_action="Vérifier les logs et corriger le problème",
                )
            ],
            questions=[],
            groupage_result=None,
            versions=self._build_version_info("2026", "2025"),
            success=False,
            error_message=str(error),
        )

    def export_audit_trail(
        self, stay_id: str, include_pii: bool = False
    ) -> Dict:
        """
        Exporte la piste d'audit complète pour un séjour.

        Args:
            stay_id: Identifiant du séjour
            include_pii: Inclure les DIP dans l'export (False par défaut)

        Returns:
            Dictionnaire avec la piste d'audit complète

        Exigences: 5.8, 5.10
        """
        logger.info(f"Export de l'audit pour le séjour {stay_id} (include_pii={include_pii})")

        audit_trail = self.audit_logger.export_audit_trail(stay_id, include_pii)

        # Convertir en dictionnaire pour export JSON
        audit_dict = audit_trail.model_dump()

        logger.info(f"Audit exporté: {len(audit_dict['audit_records'])} enregistrements")

        return audit_dict
