"""
Starvex PII Engine - Personal Identifiable Information detection

Uses Microsoft Presidio for ML-based PII detection with regex fallback
for environments without the full Presidio installation.

Optionally supports GLiNER for NER-based PII detection using a small
local model (~150MB) with zero API cost.
"""

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

logger = logging.getLogger(__name__)


@dataclass
class PIIEntity:
    """Detected PII entity"""

    entity_type: str
    text: str
    start: int
    end: int
    score: float


@dataclass
class PIIResult:
    """Result of PII detection"""

    has_pii: bool
    high_risk: bool
    entities: List[PIIEntity] = field(default_factory=list)
    anonymized_text: Optional[str] = None
    backend: str = "regex"


class PIIEngine:
    """
    PII detection engine with GLiNER, Presidio, and regex fallback.

    Fallback chain (when backend="auto"):
        1. GLiNER (NER-based, ~150MB model, CPU)
        2. Presidio (ML-based, spaCy)
        3. Regex patterns

    Usage:
        engine = PIIEngine(backend="auto")
        result = engine.detect("My SSN is 123-45-6789")
    """

    # High-risk PII types that should always be blocked
    HIGH_RISK_TYPES = {"CREDIT_CARD", "US_SSN", "US_BANK_NUMBER", "CRYPTO"}

    # Regex patterns for common PII
    PATTERNS = {
        "US_SSN": r"\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b",
        "CREDIT_CARD": r"\b(?:\d{4}[-\s]?){3}\d{4}\b",
        "EMAIL": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
        "PHONE": r"\b(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b",
        "IP_ADDRESS": r"\b(?:\d{1,3}\.){3}\d{1,3}\b",
        "DATE_OF_BIRTH": (r"\b(?:0?[1-9]|1[0-2])[/-](?:0?[1-9]|[12]\d|3[01])[/-](?:19|20)?\d{2}\b"),
    }

    # Anonymization placeholders
    PLACEHOLDERS = {
        "US_SSN": "[SSN REDACTED]",
        "CREDIT_CARD": "[CARD REDACTED]",
        "EMAIL": "[EMAIL REDACTED]",
        "PHONE": "[PHONE REDACTED]",
        "IP_ADDRESS": "[IP REDACTED]",
        "DATE_OF_BIRTH": "[DOB REDACTED]",
        "PERSON": "[NAME REDACTED]",
        "LOCATION": "[LOCATION REDACTED]",
    }

    # GLiNER entity labels to detect
    GLINER_LABELS = [
        "person",
        "email",
        "phone number",
        "credit card number",
        "social security number",
        "address",
        "date of birth",
        "ip address",
        "bank account number",
        "passport number",
        "driver license number",
    ]

    # Map GLiNER labels to standard PII entity types
    GLINER_LABEL_MAP = {
        "person": "PERSON",
        "email": "EMAIL",
        "phone number": "PHONE",
        "credit card number": "CREDIT_CARD",
        "social security number": "US_SSN",
        "address": "LOCATION",
        "date of birth": "DATE_OF_BIRTH",
        "ip address": "IP_ADDRESS",
        "bank account number": "US_BANK_NUMBER",
        "passport number": "US_PASSPORT",
        "driver license number": "US_DRIVER_LICENSE",
    }

    # Default GLiNER model
    GLINER_MODEL = "urchade/gliner_small-v2.1"

    def __init__(
        self,
        score_threshold: float = 0.5,
        backend: str = "auto",
    ):
        """
        Initialize PII engine.

        Args:
            score_threshold: Minimum confidence score for PII detection
            backend: Detection backend - "auto" (GLiNER > Presidio > regex),
                     "gliner" (NER-based), "presidio" (ML-based), or "regex"
        """
        self.score_threshold = score_threshold
        self.backend = backend
        self._presidio_analyzer = None
        self._presidio_anonymizer = None
        self._gliner_model = None
        self._gliner_available: Optional[bool] = None
        self._active_backend = "regex"

        if backend in ("auto", "presidio"):
            self._load_presidio()
        if backend in ("auto", "gliner"):
            self._check_gliner_available()

    def _load_presidio(self) -> None:
        """Attempt to load Presidio if available"""
        try:
            from presidio_analyzer import AnalyzerEngine
            from presidio_anonymizer import AnonymizerEngine

            self._presidio_analyzer = AnalyzerEngine()
            self._presidio_anonymizer = AnonymizerEngine()
            self._active_backend = "presidio"
            logger.debug("Presidio loaded successfully")
        except ImportError:
            logger.debug("Presidio not installed, using regex fallback")

    def _check_gliner_available(self) -> bool:
        """Check if GLiNER is available."""
        if self._gliner_available is not None:
            return self._gliner_available
        try:
            from gliner import GLiNER  # noqa: F401

            self._gliner_available = True
            return True
        except ImportError:
            self._gliner_available = False
            logger.debug("GLiNER not installed. Install with: pip install 'starvex[gliner]'")
            return False

    def _ensure_gliner_model(self) -> bool:
        """Load GLiNER model lazily. Returns True if loaded."""
        if self._gliner_model is not None:
            return True
        if not self._check_gliner_available():
            return False
        try:
            from gliner import GLiNER

            self._gliner_model = GLiNER.from_pretrained(self.GLINER_MODEL)
            self._active_backend = "gliner"
            logger.debug(f"GLiNER model loaded: {self.GLINER_MODEL}")
            return True
        except Exception as e:
            logger.warning(f"Failed to load GLiNER model: {e}")
            self._gliner_available = False
            return False

    def detect(self, text: str) -> PIIResult:
        """
        Detect PII in text.

        Args:
            text: Text to analyze

        Returns:
            PIIResult with detected entities
        """
        if self.backend == "gliner":
            if self._ensure_gliner_model():
                return self._detect_gliner(text)
            logger.warning("GLiNER requested but not available, falling back")
        elif self.backend == "auto":
            if self._ensure_gliner_model():
                return self._detect_gliner(text)

        if self.backend in ("presidio", "auto") and self._presidio_analyzer:
            return self._detect_presidio(text)

        return self._detect_regex(text)

    def _detect_gliner(self, text: str) -> PIIResult:
        """Detect PII using GLiNER NER model"""
        try:
            entities_raw = self._gliner_model.predict_entities(
                text,
                self.GLINER_LABELS,
                threshold=self.score_threshold,
            )

            entities = []
            for ent in entities_raw:
                entity_type = self.GLINER_LABEL_MAP.get(ent["label"], ent["label"].upper())
                entities.append(
                    PIIEntity(
                        entity_type=entity_type,
                        text=ent["text"],
                        start=ent["start"],
                        end=ent["end"],
                        score=ent.get("score", 0.9),
                    )
                )

            has_pii = len(entities) > 0
            high_risk = any(e.entity_type in self.HIGH_RISK_TYPES for e in entities)

            anonymized_text = None
            if has_pii:
                anonymized_text = self._regex_anonymize(text, entities)

            return PIIResult(
                has_pii=has_pii,
                high_risk=high_risk,
                entities=entities,
                anonymized_text=anonymized_text,
                backend="gliner",
            )

        except Exception as e:
            logger.error(f"GLiNER detection error: {e}, falling back")
            if self._presidio_analyzer:
                return self._detect_presidio(text)
            return self._detect_regex(text)

    def _detect_presidio(self, text: str) -> PIIResult:
        """Detect PII using Presidio"""
        try:
            results = self._presidio_analyzer.analyze(
                text=text,
                language="en",
                score_threshold=self.score_threshold,
            )

            entities = [
                PIIEntity(
                    entity_type=r.entity_type,
                    text=text[r.start : r.end],
                    start=r.start,
                    end=r.end,
                    score=r.score,
                )
                for r in results
            ]

            has_pii = len(entities) > 0
            high_risk = any(e.entity_type in self.HIGH_RISK_TYPES for e in entities)

            # Anonymize if PII found
            anonymized_text = None
            if has_pii:
                try:
                    anon_result = self._presidio_anonymizer.anonymize(
                        text=text,
                        analyzer_results=results,
                    )
                    anonymized_text = anon_result.text
                except Exception as e:
                    logger.warning(f"Anonymization error: {e}")
                    anonymized_text = self._regex_anonymize(text, entities)

            return PIIResult(
                has_pii=has_pii,
                high_risk=high_risk,
                entities=entities,
                anonymized_text=anonymized_text,
                backend="presidio",
            )

        except Exception as e:
            logger.error(f"Presidio error: {e}, falling back to regex")
            return self._detect_regex(text)

    def _detect_regex(self, text: str) -> PIIResult:
        """Detect PII using regex patterns"""
        entities = []

        for entity_type, pattern in self.PATTERNS.items():
            for match in re.finditer(pattern, text, re.IGNORECASE):
                entities.append(
                    PIIEntity(
                        entity_type=entity_type,
                        text=match.group(),
                        start=match.start(),
                        end=match.end(),
                        score=0.85,  # High confidence for exact matches
                    )
                )

        has_pii = len(entities) > 0
        high_risk = any(e.entity_type in self.HIGH_RISK_TYPES for e in entities)

        anonymized_text = None
        if has_pii:
            anonymized_text = self._regex_anonymize(text, entities)

        return PIIResult(
            has_pii=has_pii,
            high_risk=high_risk,
            entities=entities,
            anonymized_text=anonymized_text,
            backend="regex",
        )

    def _regex_anonymize(self, text: str, entities: List[PIIEntity]) -> str:
        """Anonymize text using regex replacement"""
        # Sort by position (reverse) to replace from end to start
        sorted_entities = sorted(entities, key=lambda e: e.start, reverse=True)

        result = text
        for entity in sorted_entities:
            placeholder = self.PLACEHOLDERS.get(
                entity.entity_type, f"[{entity.entity_type} REDACTED]"
            )
            result = result[: entity.start] + placeholder + result[entity.end :]

        return result
