"""
Processeur EDS-NLP pour l'extraction d'entités cliniques.

Ce module orchestre le pipeline EDS-NLP pour extraire des entités médicales
et détecter des qualificateurs contextuels dans les documents cliniques.
"""

import logging
import threading
from typing import ClassVar, Dict, List, Optional, Tuple

import spacy
from spacy.language import Language

from ..extractors.edsnlp_config import EDSNLPConfig
from ..extractors.edsnlp_exceptions import (
    EDSNLPProcessingError,
    PipelineInitializationError,
)
from ..extractors.edsnlp_types import (
    ExtractedEntity,
    ProcessingResult,
    QualifierResult,
)
from ..extractors import edsnlp_metrics

logger = logging.getLogger(__name__)


class EDSNLPProcessor:
    """Traite les documents cliniques avec le pipeline EDS-NLP."""

    # Cache de pipeline partagé entre toutes les instances
    _pipeline_cache: ClassVar[Dict[str, Language]] = {}
    _cache_lock: ClassVar[threading.Lock] = threading.Lock()

    def __init__(
        self,
        config: Optional[EDSNLPConfig] = None,
        model_name: Optional[str] = None,
        enable_components: Optional[List[str]] = None,
        cache_pipeline: bool = True,
    ):
        """
        Initialise le processeur EDS-NLP.

        Args:
            config: Configuration EDS-NLP complète
            model_name: Nom du modèle spaCy pour le français
            enable_components: Liste des composants EDS-NLP à activer
            cache_pipeline: Si True, met en cache l'instance du pipeline

        Note:
            Si config est fourni, il prend la priorité sur model_name et enable_components.
        """
        if config is not None:
            self.config = config
        else:
            # Créer une configuration par défaut
            self.config = EDSNLPConfig(
                model_name=model_name or "fr_core_news_sm",
            )
            if enable_components is not None:
                # Désactiver tous les composants puis activer ceux demandés
                self.config.enable_sentences = "sentences" in enable_components
                self.config.enable_negation = "negation" in enable_components
                self.config.enable_hypothesis = "hypothesis" in enable_components
                self.config.enable_history = "history" in enable_components
                self.config.enable_family = "family" in enable_components
                self.config.enable_reported_speech = "reported_speech" in enable_components

        self.cache_pipeline = cache_pipeline
        self._pipeline: Optional[Language] = None

        logger.info(
            "edsnlp_processor_initialized",
            model=self.config.model_name,
            cache_enabled=cache_pipeline,
            components=self.config.get_enabled_components(),
        )

    def get_pipeline(self) -> Language:
        """
        Obtient ou crée le pipeline spaCy avec les composants EDS-NLP.

        Utilise un cache thread-safe pour réutiliser les instances de pipeline
        et éviter les chargements répétés coûteux.

        Returns:
            Pipeline spaCy configuré avec EDS-NLP

        Raises:
            PipelineInitializationError: Si le pipeline ne peut pas être chargé
        """
        if not self.cache_pipeline:
            # Pas de cache, charger à chaque fois
            return self._load_pipeline()

        # Créer une clé de cache basée sur la configuration
        cache_key = self._get_cache_key()

        # Vérifier le cache sans lock (lecture rapide)
        if cache_key in self._pipeline_cache:
            logger.debug("pipeline_cache_hit", cache_key=cache_key)
            return self._pipeline_cache[cache_key]

        # Acquérir le lock pour charger le pipeline
        with self._cache_lock:
            # Double-check après acquisition du lock
            if cache_key in self._pipeline_cache:
                logger.debug("pipeline_cache_hit_after_lock", cache_key=cache_key)
                return self._pipeline_cache[cache_key]

            # Charger et mettre en cache le pipeline
            logger.info("pipeline_loading", cache_key=cache_key)
            pipeline = self._load_pipeline()
            self._pipeline_cache[cache_key] = pipeline
            logger.info("pipeline_cached", cache_key=cache_key)
            return pipeline

    def _get_cache_key(self) -> str:
        """
        Génère une clé de cache basée sur la configuration.

        Returns:
            Clé de cache unique pour cette configuration
        """
        components = ",".join(sorted(self.config.get_enabled_components()))
        return f"{self.config.model_name}:{components}"

    def _load_pipeline(self) -> Language:
        """
        Charge et configure le pipeline spaCy avec les composants EDS-NLP.

        Returns:
            Pipeline spaCy configuré

        Raises:
            PipelineInitializationError: Si le chargement échoue
        """
        try:
            # Charger le modèle spaCy de base
            logger.info("loading_spacy_model", model=self.config.model_name)
            nlp = spacy.load(self.config.model_name)

            # Configurer la longueur maximale des documents
            nlp.max_length = self.config.max_length

            # Ajouter les composants EDS-NLP
            self._add_edsnlp_components(nlp)

            logger.info(
                "pipeline_loaded_successfully",
                model=self.config.model_name,
                components=self.config.get_enabled_components(),
                max_length=self.config.max_length,
            )

            return nlp

        except OSError as e:
            raise PipelineInitializationError(
                message=f"Failed to load spaCy model: {self.config.model_name}",
                model_name=self.config.model_name,
                component_name=None,
                details={
                    "error": str(e),
                    "suggestion": f"Run: python -m spacy download {self.config.model_name}",
                },
            ) from e
        except Exception as e:
            raise PipelineInitializationError(
                message=f"Failed to initialize EDS-NLP pipeline",
                model_name=self.config.model_name,
                component_name=None,
                details={"error": str(e)},
            ) from e

    def _add_edsnlp_components(self, nlp: Language) -> None:
        """
        Ajoute les composants EDS-NLP au pipeline spaCy.

        Args:
            nlp: Pipeline spaCy

        Raises:
            PipelineInitializationError: Si l'ajout d'un composant échoue
        """
        try:
            # Importer edsnlp
            import edsnlp

            # Ajouter les composants selon la configuration
            if self.config.enable_sentences:
                nlp.add_pipe("eds.sentences", name="sentences")
                logger.debug("component_added", component="eds.sentences")

            if self.config.enable_negation:
                nlp.add_pipe("eds.negation", name="negation")
                logger.debug("component_added", component="eds.negation")

            if self.config.enable_hypothesis:
                nlp.add_pipe("eds.hypothesis", name="hypothesis")
                logger.debug("component_added", component="eds.hypothesis")

            if self.config.enable_history:
                nlp.add_pipe("eds.history", name="history")
                logger.debug("component_added", component="eds.history")

            if self.config.enable_family:
                nlp.add_pipe("eds.family", name="family")
                logger.debug("component_added", component="eds.family")

            if self.config.enable_reported_speech:
                nlp.add_pipe("eds.reported_speech", name="reported_speech")
                logger.debug("component_added", component="eds.reported_speech")

        except ImportError as e:
            raise PipelineInitializationError(
                message="EDS-NLP library not found",
                model_name=self.config.model_name,
                component_name="edsnlp",
                details={
                    "error": str(e),
                    "suggestion": "Run: pip install edsnlp>=0.10.0",
                },
            ) from e
        except Exception as e:
            raise PipelineInitializationError(
                message="Failed to add EDS-NLP components to pipeline",
                model_name=self.config.model_name,
                component_name="unknown",
                details={"error": str(e)},
            ) from e

    def process_document(
        self,
        text: str,
        document_id: str,
    ) -> ProcessingResult:
        """
        Traite un document clinique à travers le pipeline EDS-NLP.

        Args:
            text: Contenu textuel du document
            document_id: Identifiant du document pour le suivi

        Returns:
            ProcessingResult contenant les entités, qualificateurs et métadonnées

        Raises:
            EDSNLPProcessingError: Si le traitement échoue
        """
        import time

        start_time = time.time()

        try:
            # Obtenir le pipeline
            nlp = self.get_pipeline()

            # Traiter le document avec timeout
            try:
                doc = nlp(text)
            except Exception as e:
                # Vérifier si c'est un timeout
                elapsed = time.time() - start_time
                if elapsed > self.config.processing_timeout_seconds:
                    raise EDSNLPProcessingError(
                        message=f"Document processing timeout after {elapsed:.2f}s",
                        document_id=document_id,
                        operation="process_document",
                        details={
                            "timeout": self.config.processing_timeout_seconds,
                            "elapsed": elapsed,
                        },
                    ) from e
                else:
                    raise

            # Extraire les entités
            entities = self.extract_entities(doc)

            # Extraire les phrases avec segmentation
            sentences = self._extract_sentences(doc)

            # Calculer le temps de traitement
            processing_time = time.time() - start_time

            # Vérifier si le traitement a dépassé le timeout (warning)
            if processing_time > self.config.processing_timeout_seconds:
                logger.warning(
                    "processing_timeout_exceeded",
                    document_id=document_id,
                    processing_time=processing_time,
                    timeout=self.config.processing_timeout_seconds,
                )

            logger.info(
                "document_processed_successfully",
                document_id=document_id,
                entities_count=len(entities),
                sentences_count=len(sentences),
                processing_time=processing_time,
            )

            # Enregistrer les métriques
            edsnlp_metrics.track_processing_time(processing_time)
            for entity in entities:
                edsnlp_metrics.track_entity_extracted(entity.entity_type)
                if entity.qualifiers.negated:
                    edsnlp_metrics.track_qualifier_detected("negation")
                if entity.qualifiers.hypothesis:
                    edsnlp_metrics.track_qualifier_detected("hypothesis")
                if entity.qualifiers.historical:
                    edsnlp_metrics.track_qualifier_detected("history")
                if entity.qualifiers.family:
                    edsnlp_metrics.track_qualifier_detected("family")
                if entity.qualifiers.reported_speech:
                    edsnlp_metrics.track_qualifier_detected("reported_speech")

            return ProcessingResult(
                document_id=document_id,
                entities=entities,
                sentences=sentences,
                processing_time=processing_time,
                success=True,
            )

        except EDSNLPProcessingError:
            # Re-raise les erreurs EDS-NLP
            raise
        except Exception as e:
            processing_time = time.time() - start_time
            logger.error(
                "document_processing_failed",
                document_id=document_id,
                error=str(e),
                processing_time=processing_time,
                exc_info=True,
            )
            raise EDSNLPProcessingError(
                message=f"Failed to process document {document_id}",
                document_id=document_id,
                operation="process_document",
                details={
                    "error": str(e),
                    "processing_time": processing_time,
                },
            ) from e

    def _extract_sentences(
        self,
        doc: spacy.tokens.Doc,
    ) -> List:
        """
        Extrait les phrases avec segmentation depuis le document.

        Args:
            doc: Document spaCy traité

        Returns:
            Liste de Sentence avec spans et propositions
        """
        from ..extractors.edsnlp_types import Sentence

        sentences = []

        # Utiliser la segmentation de phrases d'EDS-NLP si disponible
        if self.config.enable_sentences and hasattr(doc._, "sentences"):
            # EDS-NLP fournit une segmentation avancée
            for sent in doc._.sentences:
                # Extraire les propositions si disponibles
                propositions = []
                if hasattr(sent, "_.propositions"):
                    propositions = [prop.text for prop in sent._.propositions]
                elif hasattr(sent, "text"):
                    # Pas de propositions, utiliser la phrase entière
                    propositions = [sent.text]

                # Filtrer les unités vides
                propositions = [p for p in propositions if p.strip()]

                if propositions:
                    sentence = Sentence(
                        text=sent.text if hasattr(sent, "text") else str(sent),
                        span=(sent.start_char, sent.end_char) if hasattr(sent, "start_char") else (0, 0),
                        propositions=propositions,
                    )
                    sentences.append(sentence)
        else:
            # Utiliser la segmentation standard de spaCy
            for sent in doc.sents:
                # Filtrer les phrases vides
                if sent.text.strip():
                    sentence = Sentence(
                        text=sent.text,
                        span=(sent.start_char, sent.end_char),
                        propositions=[sent.text],
                    )
                    sentences.append(sentence)

        logger.debug(
            "sentences_extracted",
            count=len(sentences),
        )

        return sentences

    def extract_entities(
        self,
        doc: spacy.tokens.Doc,
    ) -> List[ExtractedEntity]:
        """
        Extrait les entités médicales d'un document traité.

        Args:
            doc: Document spaCy traité

        Returns:
            Liste des entités extraites avec types et spans

        Raises:
            EDSNLPProcessingError: Si l'extraction échoue
        """
        try:
            entities: List[ExtractedEntity] = []

            # Extraire les entités depuis doc.ents (entités nommées)
            for ent in doc.ents:
                # Déterminer le type d'entité
                entity_type = self._map_entity_label(ent.label_)

                if entity_type is None:
                    # Type d'entité non supporté, ignorer
                    continue

                # Détecter les qualificateurs pour cette entité
                qualifier = self.detect_qualifiers(doc, (ent.start_char, ent.end_char))

                # Calculer la confiance de base (1.0 pour les entités détectées)
                confidence = 1.0

                # Créer l'entité extraite
                entity = ExtractedEntity(
                    text=ent.text,
                    entity_type=entity_type,
                    span=(ent.start_char, ent.end_char),
                    confidence=confidence,
                    qualifiers=qualifier,
                    normalized_text="",  # Sera rempli plus tard par le normalizer
                )
                entities.append(entity)

            logger.debug(
                "entities_extracted",
                count=len(entities),
                types={entity.entity_type for entity in entities},
            )

            return entities

        except Exception as e:
            raise EDSNLPProcessingError(
                message="Failed to extract entities from document",
                document_id="unknown",
                operation="extract_entities",
                details={"error": str(e)},
            ) from e

    def _map_entity_label(self, label: str) -> Optional[str]:
        """
        Mappe les labels d'entités spaCy/EDS-NLP vers nos types d'entités.

        Args:
            label: Label d'entité spaCy

        Returns:
            Type d'entité standardisé ou None si non supporté
        """
        # Mapping des labels spaCy/EDS-NLP vers nos types
        label_mapping = {
            # Labels médicaux EDS-NLP
            "disorder": "diagnostic",
            "disease": "diagnostic",
            "symptom": "diagnostic",
            "drug": "medication",
            "medication": "medication",
            "procedure": "procedure",
            "treatment": "procedure",
            "date": "date",
            "time": "date",
            "measurement": "measurement",
            "value": "measurement",
            # Labels spaCy standards
            "MISC": None,  # Ignorer les entités diverses
            "PER": None,   # Ignorer les personnes
            "LOC": None,   # Ignorer les lieux
            "ORG": None,   # Ignorer les organisations
        }

        return label_mapping.get(label.lower(), None)

    def detect_qualifiers(
        self,
        doc: spacy.tokens.Doc,
        entity_span: Tuple[int, int],
    ) -> QualifierResult:
        """
        Détecte les qualificateurs contextuels pour une entité.

        Args:
            doc: Document spaCy traité
            entity_span: Positions (start, end) en caractères de l'entité

        Returns:
            QualifierResult avec les qualificateurs détectés et la confiance

        Raises:
            EDSNLPProcessingError: Si la détection échoue
        """
        try:
            # Trouver le span correspondant dans le doc
            entity_start_char, entity_end_char = entity_span

            # Trouver les tokens correspondant à l'entité
            entity_tokens = []
            for token in doc:
                if token.idx >= entity_start_char and token.idx < entity_end_char:
                    entity_tokens.append(token)

            if not entity_tokens:
                # Pas de tokens trouvés, retourner des qualificateurs par défaut
                return QualifierResult(
                    negated=False,
                    hypothesis=False,
                    historical=False,
                    family=False,
                    reported_speech=False,
                    confidence=1.0,
                )

            # Initialiser les qualificateurs
            negated = False
            hypothesis = False
            historical = False
            family = False
            reported_speech = False
            negation_cue = None
            hypothesis_cue = None
            history_cue = None

            # Détecter la négation (eds.negation)
            if self.config.enable_negation:
                for token in entity_tokens:
                    if hasattr(token._, "negation") and token._.negation:
                        negated = True
                        # Essayer de trouver le marqueur de négation
                        if hasattr(token._, "negation_cue"):
                            negation_cue = token._.negation_cue
                        break

            # Détecter l'hypothèse (eds.hypothesis)
            if self.config.enable_hypothesis:
                for token in entity_tokens:
                    if hasattr(token._, "hypothesis") and token._.hypothesis:
                        hypothesis = True
                        # Essayer de trouver le marqueur d'hypothèse
                        if hasattr(token._, "hypothesis_cue"):
                            hypothesis_cue = token._.hypothesis_cue
                        break

            # Détecter le contexte historique (eds.history)
            if self.config.enable_history:
                for token in entity_tokens:
                    if hasattr(token._, "history") and token._.history:
                        historical = True
                        # Essayer de trouver le marqueur d'historique
                        if hasattr(token._, "history_cue"):
                            history_cue = token._.history_cue
                        break

            # Détecter le contexte familial (eds.family)
            if self.config.enable_family:
                for token in entity_tokens:
                    if hasattr(token._, "family") and token._.family:
                        family = True
                        break

            # Détecter le discours rapporté (eds.reported_speech)
            if self.config.enable_reported_speech:
                for token in entity_tokens:
                    if hasattr(token._, "reported_speech") and token._.reported_speech:
                        reported_speech = True
                        break

            # Calculer la confiance (réduite si plusieurs qualificateurs)
            confidence = 1.0
            if negated:
                confidence -= 0.1
            if hypothesis:
                confidence -= 0.2
            if family:
                confidence -= 0.5
            if reported_speech:
                confidence -= 0.1
            if historical:
                confidence -= 0.05
            confidence = max(0.0, min(1.0, confidence))

            return QualifierResult(
                negated=negated,
                hypothesis=hypothesis,
                historical=historical,
                family=family,
                reported_speech=reported_speech,
                negation_cue=negation_cue,
                hypothesis_cue=hypothesis_cue,
                history_cue=history_cue,
                confidence=confidence,
            )

        except Exception as e:
            raise EDSNLPProcessingError(
                message="Failed to detect qualifiers for entity",
                document_id="unknown",
                operation="detect_qualifiers",
                details={
                    "error": str(e),
                    "entity_span": entity_span,
                },
            ) from e

    def process_documents_batch(
        self,
        documents: List[Tuple[str, str]],
    ) -> List[ProcessingResult]:
        """
        Traite plusieurs documents en batch pour l'efficacité.

        Utilise la méthode pipe() de spaCy pour traiter les documents
        en parallèle, améliorant significativement le débit.

        Args:
            documents: Liste de tuples (text, document_id)

        Returns:
            Liste de ProcessingResult

        Raises:
            EDSNLPProcessingError: Si le traitement échoue
        """
        import time

        start_time = time.time()

        try:
            # Obtenir le pipeline
            nlp = self.get_pipeline()

            # Extraire les textes et IDs
            texts = [text for text, _ in documents]
            doc_ids = [doc_id for _, doc_id in documents]

            results = []

            # Traiter en batch avec pipe()
            for doc, doc_id in zip(nlp.pipe(texts, batch_size=self.config.batch_size), doc_ids):
                doc_start_time = time.time()

                try:
                    # Extraire les entités
                    entities = self.extract_entities(doc)

                    # Extraire les phrases
                    sentences = self._extract_sentences(doc)

                    # Calculer le temps de traitement pour ce document
                    doc_processing_time = time.time() - doc_start_time

                    result = ProcessingResult(
                        document_id=doc_id,
                        entities=entities,
                        sentences=sentences,
                        processing_time=doc_processing_time,
                        success=True,
                    )
                    results.append(result)

                except Exception as e:
                    # Enregistrer l'erreur mais continuer avec les autres documents
                    doc_processing_time = time.time() - doc_start_time
                    logger.error(
                        "batch_document_processing_failed",
                        document_id=doc_id,
                        error=str(e),
                        processing_time=doc_processing_time,
                    )
                    result = ProcessingResult(
                        document_id=doc_id,
                        entities=[],
                        sentences=[],
                        processing_time=doc_processing_time,
                        success=False,
                        error=str(e),
                    )
                    results.append(result)

            total_time = time.time() - start_time
            logger.info(
                "batch_processing_completed",
                documents_count=len(documents),
                successful_count=sum(1 for r in results if r.success),
                failed_count=sum(1 for r in results if not r.success),
                total_time=total_time,
                avg_time_per_doc=total_time / len(documents) if documents else 0,
            )

            return results

        except Exception as e:
            total_time = time.time() - start_time
            logger.error(
                "batch_processing_failed",
                documents_count=len(documents),
                error=str(e),
                total_time=total_time,
                exc_info=True,
            )
            raise EDSNLPProcessingError(
                message=f"Failed to process batch of {len(documents)} documents",
                document_id="batch",
                operation="process_documents_batch",
                details={
                    "error": str(e),
                    "documents_count": len(documents),
                    "total_time": total_time,
                },
            ) from e

    def _map_qualifier_to_model(
        self,
        qualifier_result: QualifierResult,
    ):
        """
        Mappe un QualifierResult EDS-NLP vers le modèle Qualifier.

        Args:
            qualifier_result: Résultat de détection des qualificateurs EDS-NLP

        Returns:
            Instance de Qualifier compatible avec le modèle existant
        """
        from ..models.clinical import Qualifier, Span

        # Déterminer la certitude selon les règles de mapping
        if qualifier_result.negated:
            certainty = "nié"
        elif qualifier_result.hypothesis:
            certainty = "suspecté"
        else:
            certainty = "affirmé"

        # Collecter les marqueurs linguistiques
        markers = []
        if qualifier_result.negation_cue:
            markers.append(qualifier_result.negation_cue)
        if qualifier_result.hypothesis_cue:
            markers.append(qualifier_result.hypothesis_cue)
        if qualifier_result.history_cue:
            markers.append(qualifier_result.history_cue)

        # Créer les spans des qualificateurs si disponibles
        qualifier_spans = None
        # Note: Les spans seront ajoutés plus tard si nécessaire

        return Qualifier(
            certainty=certainty,
            markers=markers,
            confidence=qualifier_result.confidence,
            negation_cue=qualifier_result.negation_cue,
            hypothesis_cue=qualifier_result.hypothesis_cue,
            history_cue=qualifier_result.history_cue,
            family_context=qualifier_result.family,
            reported_speech=qualifier_result.reported_speech,
            qualifier_spans=qualifier_spans,
        )


def calculate_confidence(
    entity: ExtractedEntity,
    qualifier: QualifierResult,
) -> float:
    """
    Calcule le score de confiance global pour une entité extraite.

    Le score de base provient de l'extraction d'entité (0.0-1.0).
    Des ajustements sont appliqués selon les qualificateurs détectés:
    - Négation détectée: -0.1
    - Hypothèse détectée: -0.2
    - Contexte familial: -0.5 (pénalité forte)
    - Discours rapporté: -0.1
    - Contexte historique: -0.05

    Args:
        entity: Entité extraite avec confiance de base
        qualifier: Qualificateurs détectés

    Returns:
        Score de confiance ajusté, borné à [0.0, 1.0]
    """
    confidence = entity.confidence

    # Appliquer les ajustements
    if qualifier.negated:
        confidence -= 0.1
    if qualifier.hypothesis:
        confidence -= 0.2
    if qualifier.family:
        confidence -= 0.5
    if qualifier.reported_speech:
        confidence -= 0.1
    if qualifier.historical:
        confidence -= 0.05

    # Borner à [0.0, 1.0]
    return max(0.0, min(1.0, confidence))

    def _should_exclude_fact(
        self,
        qualifier: QualifierResult,
    ) -> bool:
        """
        Détermine si un fait clinique doit être exclu du codage.

        Les faits avec contexte familial sont marqués comme non pertinents
        pour le codage du patient actuel.

        Args:
            qualifier: Qualificateurs détectés pour le fait

        Returns:
            True si le fait doit être exclu, False sinon
        """
        # Exclure les faits avec contexte familial
        if qualifier.family:
            logger.debug(
                "fact_excluded_family_context",
                family=qualifier.family,
            )
            return True

        return False
