"""PII detection engine -- regex-based detection of personally identifiable information.

Detects common PII patterns including email addresses, SSNs, credit card numbers,
phone numbers, and IP addresses. Each PII type is categorised as either PRIVATE
(moderate risk) or SENSITIVE (high risk) to drive routing decisions.
"""

from __future__ import annotations

import logging
import re
import time
from dataclasses import dataclass
from enum import Enum
from typing import Any

from llmhosts.privacy.models import PrivacyClassification, PrivacyTier

logger = logging.getLogger(__name__)


class PIIType(str, Enum):
    """Types of PII that can be detected."""

    EMAIL = "email"
    SSN = "ssn"
    CREDIT_CARD = "credit_card"
    PHONE = "phone"
    IP_ADDRESS = "ip_address"
    DATE_OF_BIRTH = "date_of_birth"
    US_PASSPORT = "us_passport"
    IBAN = "iban"


# Map PII types to their privacy tier (the minimum tier triggered by detection)
_PII_TIER_MAP: dict[PIIType, PrivacyTier] = {
    PIIType.EMAIL: PrivacyTier.PRIVATE,
    PIIType.PHONE: PrivacyTier.PRIVATE,
    PIIType.IP_ADDRESS: PrivacyTier.PRIVATE,
    PIIType.DATE_OF_BIRTH: PrivacyTier.PRIVATE,
    PIIType.SSN: PrivacyTier.SENSITIVE,
    PIIType.CREDIT_CARD: PrivacyTier.SENSITIVE,
    PIIType.US_PASSPORT: PrivacyTier.SENSITIVE,
    PIIType.IBAN: PrivacyTier.SENSITIVE,
}


@dataclass
class PIIMatch:
    """A single PII detection match."""

    pii_type: PIIType
    tier: PrivacyTier
    start: int
    end: int
    # We intentionally do NOT store the matched text to avoid leaking PII in logs


# ---------------------------------------------------------------------------
# Compiled regex patterns
# ---------------------------------------------------------------------------

# Email: standard RFC-like pattern
_EMAIL_RE = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")

# SSN: ###-##-#### or ### ## #### (with word boundaries to avoid false positives)
_SSN_RE = re.compile(r"\b(?!000|666|9\d{2})\d{3}[-\s](?!00)\d{2}[-\s](?!0000)\d{4}\b")

# Credit card: major card patterns (Visa, MC, Amex, Discover)
# Matches with optional spaces/dashes between groups
_CREDIT_CARD_RE = re.compile(
    r"\b(?:"
    r"4\d{3}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}"  # Visa
    r"|5[1-5]\d{2}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}"  # Mastercard
    r"|3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}"  # Amex
    r"|6(?:011|5\d{2})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}"  # Discover
    r")\b"
)

# US/international phone: various formats
_PHONE_RE = re.compile(
    r"(?<!\d)"  # not preceded by digit
    r"(?:"
    r"\+?1[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"  # US: +1 (xxx) xxx-xxxx
    r"|\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"  # US: (xxx) xxx-xxxx
    r"|\+\d{1,3}[-.\s]?\d{4,14}"  # International: +CC xxxx...
    r")"
    r"(?!\d)"  # not followed by digit
)

# IPv4 address (private and public)
_IPV4_RE = re.compile(r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b")

# Date of birth patterns: MM/DD/YYYY, DD-MM-YYYY, YYYY-MM-DD
_DOB_RE = re.compile(
    r"\b(?:"
    r"(?:0[1-9]|1[0-2])[/\-](?:0[1-9]|[12]\d|3[01])[/\-](?:19|20)\d{2}"  # MM/DD/YYYY
    r"|(?:19|20)\d{2}[/\-](?:0[1-9]|1[0-2])[/\-](?:0[1-9]|[12]\d|3[01])"  # YYYY-MM-DD
    r")\b"
)

# US Passport: 9 digits
_US_PASSPORT_RE = re.compile(r"\b[A-Z]?\d{8,9}\b")

# IBAN: 2 letter country code + 2 check digits + up to 30 alphanumeric
_IBAN_RE = re.compile(r"\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b")


# ---------------------------------------------------------------------------
# Pattern registry
# ---------------------------------------------------------------------------

_PATTERNS: list[tuple[PIIType, re.Pattern[str]]] = [
    (PIIType.SSN, _SSN_RE),
    (PIIType.CREDIT_CARD, _CREDIT_CARD_RE),
    (PIIType.EMAIL, _EMAIL_RE),
    (PIIType.PHONE, _PHONE_RE),
    (PIIType.IP_ADDRESS, _IPV4_RE),
    (PIIType.DATE_OF_BIRTH, _DOB_RE),
]

# Passport and IBAN patterns have higher false-positive rates, so we keep
# them out of the default scan but make them available for strict mode.
_STRICT_PATTERNS: list[tuple[PIIType, re.Pattern[str]]] = [
    *_PATTERNS,
    (PIIType.US_PASSPORT, _US_PASSPORT_RE),
    (PIIType.IBAN, _IBAN_RE),
]


class PIIDetector:
    """Regex-based PII detector.

    Scans text content for common PII patterns and returns a
    :class:`PrivacyClassification` indicating the appropriate routing tier.

    Usage::

        detector = PIIDetector()
        classification = detector.classify("Send email to user@example.com")
        assert classification.tier == PrivacyTier.PRIVATE
    """

    def __init__(self, strict: bool = False, custom_patterns: dict[str, str] | None = None) -> None:
        self._patterns = list(_STRICT_PATTERNS if strict else _PATTERNS)
        self._custom_patterns: list[tuple[str, re.Pattern[str]]] = []
        if custom_patterns:
            for name, pattern in custom_patterns.items():
                try:
                    self._custom_patterns.append((name, re.compile(pattern)))
                except re.error:
                    logger.warning("Invalid custom PII pattern '%s': %s", name, pattern)

    def scan(self, text: str) -> list[PIIMatch]:
        """Scan text for PII and return all matches."""
        if not text:
            return []

        matches: list[PIIMatch] = []

        for pii_type, pattern in self._patterns:
            for m in pattern.finditer(text):
                matches.append(
                    PIIMatch(
                        pii_type=pii_type,
                        tier=_PII_TIER_MAP[pii_type],
                        start=m.start(),
                        end=m.end(),
                    )
                )

        # Custom patterns are treated as PRIVATE tier
        for _name, pattern in self._custom_patterns:
            for m in pattern.finditer(text):
                matches.append(
                    PIIMatch(
                        pii_type=PIIType.EMAIL,  # placeholder type
                        tier=PrivacyTier.PRIVATE,
                        start=m.start(),
                        end=m.end(),
                    )
                )

        return matches

    def classify(self, text: str) -> PrivacyClassification:
        """Classify text and return the privacy tier with details."""
        start = time.perf_counter()
        matches = self.scan(text)
        elapsed_ms = (time.perf_counter() - start) * 1000

        if not matches:
            return PrivacyClassification(
                tier=PrivacyTier.PUBLIC,
                pii_types_found=[],
                pii_count=0,
                reasoning="No PII detected",
                scan_time_ms=elapsed_ms,
            )

        # Determine the highest tier among all matches
        pii_types = sorted({m.pii_type.value for m in matches})
        has_sensitive = any(m.tier == PrivacyTier.SENSITIVE for m in matches)
        tier = PrivacyTier.SENSITIVE if has_sensitive else PrivacyTier.PRIVATE

        reasoning_parts = [f"{len(matches)} PII match(es) found"]
        reasoning_parts.append(f"types: {', '.join(pii_types)}")
        if has_sensitive:
            reasoning_parts.append("contains high-risk PII")

        return PrivacyClassification(
            tier=tier,
            pii_types_found=pii_types,
            pii_count=len(matches),
            reasoning="; ".join(reasoning_parts),
            scan_time_ms=elapsed_ms,
        )

    def classify_messages(self, messages: list[dict[str, Any]]) -> PrivacyClassification:
        """Classify a list of message dicts (with 'content' keys).

        Aggregates PII detection across all messages and returns the
        highest-tier classification.
        """
        start = time.perf_counter()
        all_matches: list[PIIMatch] = []

        for msg in messages:
            content = msg.get("content", "")
            if isinstance(content, str) and content:
                all_matches.extend(self.scan(content))
            elif isinstance(content, list):
                # Multimodal content blocks
                for block in content:
                    if isinstance(block, dict):
                        text = block.get("text", "")
                        if text:
                            all_matches.extend(self.scan(text))

        elapsed_ms = (time.perf_counter() - start) * 1000

        if not all_matches:
            return PrivacyClassification(
                tier=PrivacyTier.PUBLIC,
                pii_types_found=[],
                pii_count=0,
                reasoning="No PII detected across messages",
                scan_time_ms=elapsed_ms,
            )

        pii_types = sorted({m.pii_type.value for m in all_matches})
        has_sensitive = any(m.tier == PrivacyTier.SENSITIVE for m in all_matches)
        tier = PrivacyTier.SENSITIVE if has_sensitive else PrivacyTier.PRIVATE

        reasoning_parts = [f"{len(all_matches)} PII match(es) across messages"]
        reasoning_parts.append(f"types: {', '.join(pii_types)}")
        if has_sensitive:
            reasoning_parts.append("contains high-risk PII")

        return PrivacyClassification(
            tier=tier,
            pii_types_found=pii_types,
            pii_count=len(all_matches),
            reasoning="; ".join(reasoning_parts),
            scan_time_ms=elapsed_ms,
        )
