"""
PII Protector - Détection et anonymisation des données identifiantes patients (DIP).

Ce module implémente une approche hybride combinant regex et NER pour détecter
les DIP dans le texte clinique. L'approche privilégie le rappel élevé (préférer
les faux positifs) pour éviter les fuites de données sensibles.

Exigences : 11.1, 11.2, 11.3, 5.10
"""

import re
from dataclasses import dataclass
from typing import List, Optional

from ..models.clinical import Span


@dataclass
class PIISpan:
    """Span contenant une donnée identifiante patient (DIP)."""

    span: Span
    type: str  # "name", "birth_date", "nss", "address", "phone", "email"
    text: str
    confidence: float  # 0.0 à 1.0


class PIIProtector:
    """
    Détecteur et anonymiseur de données identifiantes patients (DIP).

    Utilise une approche hybride :
    - Regex pour patterns connus (dates, NSS, téléphones, emails, adresses)
    - NER (Named Entity Recognition) pour noms de personnes

    Privilégie le rappel élevé (préférer faux positifs) pour éviter fuites de DIP.
    """

    # Patterns regex pour détection de DIP
    # Note: Ces patterns sont volontairement larges pour maximiser le rappel

    # Dates de naissance - formats variés
    DATE_PATTERNS = [
        # Format JJ/MM/AAAA ou JJ-MM-AAAA
        r"\b(?:0?[1-9]|[12][0-9]|3[01])[/-](?:0?[1-9]|1[0-2])[/-](?:19|20)\d{2}\b",
        # Format AAAA-MM-JJ (ISO)
        r"\b(?:19|20)\d{2}-(?:0?[1-9]|1[0-2])-(?:0?[1-9]|[12][0-9]|3[01])\b",
        # Format JJ mois AAAA (ex: 15 mars 1960)
        r"\b(?:0?[1-9]|[12][0-9]|3[01])\s+(?:janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)\s+(?:19|20)\d{2}\b",
        # Format abrégé (ex: 15/03/60)
        r"\b(?:0?[1-9]|[12][0-9]|3[01])[/-](?:0?[1-9]|1[0-2])[/-]\d{2}\b",
    ]

    # Numéro de sécurité sociale (NSS) - formats variés
    # Format: 1 YY MM DD XXX YYY CC (15 chiffres avec ou sans espaces)
    NSS_PATTERNS = [
        # Avec espaces (plus flexible)
        r"\b[12]\s+\d{2}\s+\d{2}\s+\d{2,3}\s+\d{3}\s+\d{3}\s+\d{2}\b",
        # Sans espaces
        r"\b[12]\d{14}\b",
    ]

    # Numéros de téléphone français
    PHONE_PATTERNS = [
        # Format avec espaces (ex: 01 23 45 67 89)
        r"\b0[1-9](?:\s?\d{2}){4}\b",
        # Format avec points (ex: 01.23.45.67.89)
        r"\b0[1-9](?:\.\d{2}){4}\b",
        # Format avec tirets (ex: 01-23-45-67-89)
        r"\b0[1-9](?:-\d{2}){4}\b",
        # Format international (ex: +33 1 23 45 67 89)
        r"\+33\s?[1-9](?:\s?\d{2}){4}\b",
    ]

    # Adresses emails
    EMAIL_PATTERN = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"

    # Adresses postales - patterns pour détecter des fragments d'adresses
    ADDRESS_PATTERNS = [
        # Numéro + rue/avenue/boulevard
        r"\b\d{1,4}\s+(?:rue|avenue|boulevard|allée|impasse|place|chemin|route)\s+[A-Za-zÀ-ÿ\s-]+\b",
        # Code postal français (5 chiffres)
        r"\b\d{5}\b",
        # Ville avec code postal
        r"\b\d{5}\s+[A-Za-zÀ-ÿ\s-]+\b",
    ]

    # Mots-clés indiquant des noms de personnes dans le contexte médical
    NAME_CONTEXT_KEYWORDS = [
        r"patient\s+(?:nommé|appelé|dénommé)?\s*:?\s*",
        r"(?:M\.|Mme|Mlle|Monsieur|Madame|Mademoiselle)\s+",
        r"nom\s*:?\s*",
        r"prénom\s*:?\s*",
    ]

    def __init__(self, use_ner: bool = True, ner_model: Optional[str] = None):
        """
        Initialise le PII Protector.

        Args:
            use_ner: Utiliser NER pour détection de noms (True par défaut)
            ner_model: Modèle spaCy à utiliser (None = modèle par défaut)
        """
        self.use_ner = use_ner
        self.ner_model = ner_model
        self._nlp = None

        # Compiler les patterns regex pour performance
        self._compiled_date_patterns = [re.compile(p, re.IGNORECASE) for p in self.DATE_PATTERNS]
        self._compiled_nss_patterns = [re.compile(p) for p in self.NSS_PATTERNS]
        self._compiled_phone_patterns = [re.compile(p) for p in self.PHONE_PATTERNS]
        self._compiled_email_pattern = re.compile(self.EMAIL_PATTERN)
        self._compiled_address_patterns = [
            re.compile(p, re.IGNORECASE) for p in self.ADDRESS_PATTERNS
        ]
        self._compiled_name_context_patterns = [
            re.compile(p, re.IGNORECASE) for p in self.NAME_CONTEXT_KEYWORDS
        ]

    def _load_ner_model(self) -> None:
        """Charge le modèle NER spaCy de manière lazy."""
        if self._nlp is None and self.use_ner:
            try:
                import spacy

                # Utiliser le modèle français par défaut ou le modèle spécifié
                model_name = self.ner_model or "fr_core_news_sm"
                self._nlp = spacy.load(model_name)
            except (ImportError, OSError) as e:
                # Si spaCy n'est pas disponible ou le modèle n'est pas installé,
                # continuer sans NER (regex seulement)
                self._nlp = None
                self.use_ner = False

    def detect_pii(self, text: str) -> List[PIISpan]:
        """
        Détecte les DIP dans le texte avec approche hybride (regex + NER).

        Args:
            text: Texte à analyser

        Returns:
            Liste de PIISpan détectés, triés par position

        Exigences: 11.1
        """
        pii_spans: List[PIISpan] = []

        # 1. Détection par regex
        pii_spans.extend(self._detect_dates(text))
        pii_spans.extend(self._detect_nss(text))
        pii_spans.extend(self._detect_phones(text))
        pii_spans.extend(self._detect_emails(text))
        pii_spans.extend(self._detect_addresses(text))

        # 2. Détection de noms par NER
        if self.use_ner:
            self._load_ner_model()
            if self._nlp is not None:
                pii_spans.extend(self._detect_names_ner(text))

        # 3. Détection de noms par contexte (fallback si pas de NER)
        pii_spans.extend(self._detect_names_context(text))

        # 4. Fusionner les spans qui se chevauchent et trier
        pii_spans = self._merge_overlapping_spans(pii_spans)

        return sorted(pii_spans, key=lambda x: x.span.start)

    def _detect_dates(self, text: str) -> List[PIISpan]:
        """Détecte les dates de naissance potentielles."""
        spans = []
        for pattern in self._compiled_date_patterns:
            for match in pattern.finditer(text):
                spans.append(
                    PIISpan(
                        span=Span(start=match.start(), end=match.end()),
                        type="birth_date",
                        text=match.group(),
                        confidence=0.8,  # Confiance élevée pour les patterns de dates
                    )
                )
        return spans

    def _detect_nss(self, text: str) -> List[PIISpan]:
        """Détecte les numéros de sécurité sociale."""
        spans = []
        for pattern in self._compiled_nss_patterns:
            for match in pattern.finditer(text):
                spans.append(
                    PIISpan(
                        span=Span(start=match.start(), end=match.end()),
                        type="nss",
                        text=match.group(),
                        confidence=0.95,  # Confiance très élevée pour NSS
                    )
                )
        return spans

    def _detect_phones(self, text: str) -> List[PIISpan]:
        """Détecte les numéros de téléphone."""
        spans = []
        for pattern in self._compiled_phone_patterns:
            for match in pattern.finditer(text):
                spans.append(
                    PIISpan(
                        span=Span(start=match.start(), end=match.end()),
                        type="phone",
                        text=match.group(),
                        confidence=0.85,
                    )
                )
        return spans

    def _detect_emails(self, text: str) -> List[PIISpan]:
        """Détecte les adresses email."""
        spans = []
        for match in self._compiled_email_pattern.finditer(text):
            spans.append(
                PIISpan(
                    span=Span(start=match.start(), end=match.end()),
                    type="email",
                    text=match.group(),
                    confidence=0.9,
                )
            )
        return spans

    def _detect_addresses(self, text: str) -> List[PIISpan]:
        """Détecte les fragments d'adresses postales."""
        spans = []
        for pattern in self._compiled_address_patterns:
            for match in pattern.finditer(text):
                # Confiance plus faible pour adresses car plus de faux positifs possibles
                spans.append(
                    PIISpan(
                        span=Span(start=match.start(), end=match.end()),
                        type="address",
                        text=match.group(),
                        confidence=0.6,
                    )
                )
        return spans

    def _detect_names_ner(self, text: str) -> List[PIISpan]:
        """Détecte les noms de personnes avec NER (spaCy)."""
        if self._nlp is None:
            return []

        spans = []
        doc = self._nlp(text)

        for ent in doc.ents:
            # Détecter les entités de type PERSON
            if ent.label_ == "PER" or ent.label_ == "PERSON":
                spans.append(
                    PIISpan(
                        span=Span(start=ent.start_char, end=ent.end_char),
                        type="name",
                        text=ent.text,
                        confidence=0.75,  # Confiance modérée pour NER
                    )
                )

        return spans

    def _detect_names_context(self, text: str) -> List[PIISpan]:
        """
        Détecte les noms de personnes par analyse de contexte.

        Recherche des patterns comme "Patient Jean Dupont" ou "M. Martin".
        """
        spans = []

        for pattern in self._compiled_name_context_patterns:
            for match in pattern.finditer(text):
                # Extraire le texte après le mot-clé (potentiellement un nom)
                context_end = match.end()
                # Chercher jusqu'à 50 caractères après le mot-clé
                remaining_text = text[context_end : context_end + 50]

                # Pattern pour capturer un nom (2-4 mots capitalisés)
                name_pattern = r"^([A-ZÀ-Ÿ][a-zà-ÿ-]+(?:\s+[A-ZÀ-Ÿ][a-zà-ÿ-]+){0,3})"
                name_match = re.match(name_pattern, remaining_text)

                if name_match:
                    name_text = name_match.group(1)
                    spans.append(
                        PIISpan(
                            span=Span(
                                start=context_end, end=context_end + len(name_text)
                            ),
                            type="name",
                            text=name_text,
                            confidence=0.7,  # Confiance modérée pour détection contextuelle
                        )
                    )

        return spans

    def _merge_overlapping_spans(self, spans: List[PIISpan]) -> List[PIISpan]:
        """
        Fusionne les spans qui se chevauchent.

        Garde le span avec la confiance la plus élevée en cas de chevauchement.
        """
        if not spans:
            return []

        # Trier par position de début
        sorted_spans = sorted(spans, key=lambda x: x.span.start)

        merged = [sorted_spans[0]]

        for current in sorted_spans[1:]:
            last = merged[-1]

            # Vérifier le chevauchement
            if current.span.start <= last.span.end:
                # Chevauchement détecté - garder le span avec la plus haute confiance
                if current.confidence > last.confidence:
                    merged[-1] = current
                # Sinon, ignorer le span actuel (garder le précédent)
            else:
                # Pas de chevauchement - ajouter le span
                merged.append(current)

        return merged

    def anonymize_text(self, text: str, pii_spans: Optional[List[PIISpan]] = None) -> str:
        """
        Anonymise les DIP dans le texte en les remplaçant par des placeholders.

        Args:
            text: Texte à anonymiser
            pii_spans: Spans de DIP détectés (si None, détection automatique)

        Returns:
            Texte anonymisé

        Exigences: 11.5
        """
        if pii_spans is None:
            pii_spans = self.detect_pii(text)

        if not pii_spans:
            return text

        # Remplacer les DIP par des placeholders en partant de la fin
        # pour ne pas décaler les positions
        result = text
        for pii_span in reversed(pii_spans):
            placeholder = self._get_placeholder(pii_span.type)
            result = (
                result[: pii_span.span.start]
                + placeholder
                + result[pii_span.span.end :]
            )

        return result

    def _get_placeholder(self, pii_type: str) -> str:
        """Retourne le placeholder approprié pour un type de DIP."""
        placeholders = {
            "name": "[NOM_ANONYMISÉ]",
            "birth_date": "[DATE_NAISSANCE]",
            "nss": "[NSS]",
            "phone": "[TÉLÉPHONE]",
            "email": "[EMAIL]",
            "address": "[ADRESSE]",
        }
        return placeholders.get(pii_type, "[DIP]")

    def filter_logs(self, log_entry: str) -> str:
        """
        Filtre les DIP des logs système.

        Détecte et anonymise automatiquement toute DIP dans les logs.

        Args:
            log_entry: Entrée de log à filtrer

        Returns:
            Log filtré sans DIP

        Exigences: 11.2, 5.10
        """
        return self.anonymize_text(log_entry)

    def has_pii(self, text: str) -> bool:
        """
        Vérifie si le texte contient des DIP.

        Args:
            text: Texte à vérifier

        Returns:
            True si des DIP sont détectées, False sinon
        """
        pii_spans = self.detect_pii(text)
        return len(pii_spans) > 0
