"""PII Detection Module.

Detects personally identifiable information in text using regex patterns
and optional spaCy NER.
"""

from __future__ import annotations

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

# ---------------------------------------------------------------------------
# PII patterns:  (label, regex, severity)
# ---------------------------------------------------------------------------

_PII_PATTERNS: list[tuple[str, re.Pattern, float]] = [
    (
        "email_address",
        re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}"),
        0.7,
    ),
    (
        "phone_number",
        re.compile(
            r"(?<!\d)"
            r"(?:\+?1[-.\s]?)?"
            r"(?:\(?\d{3}\)?[-.\s]?)"
            r"\d{3}[-.\s]?\d{4}"
            r"(?!\d)"
        ),
        0.6,
    ),
    (
        "credit_card",
        re.compile(
            r"(?<!\d)"
            r"(?:4\d{3}|5[1-5]\d{2}|6(?:011|5\d{2})|3[47]\d{2}|3(?:0[0-5]|[68]\d)\d)"
            r"[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}"
            r"(?!\d)"
        ),
        0.9,
    ),
    (
        "ssn",
        re.compile(
            r"(?<!\d)\d{3}[-\s]?\d{2}[-\s]?\d{4}(?!\d)"
        ),
        0.9,
    ),
    (
        "aadhaar_number",
        re.compile(
            r"(?<!\d)\d{4}[-\s]?\d{4}[-\s]?\d{4}(?!\d)"
        ),
        0.8,
    ),
    (
        "ip_address",
        re.compile(
            r"(?<!\d)(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}"
            r"(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?!\d)"
        ),
        0.5,
    ),
    (
        "date_of_birth",
        re.compile(
            r"(?i)(?:dob|date of birth|born)[:\s]*"
            r"\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4}"
        ),
        0.6,
    ),
    (
        "passport_number",
        re.compile(
            r"(?i)passport[:\s#]*[A-Z]{1,2}\d{6,9}"
        ),
        0.8,
    ),
]

# Luhn check for credit cards
def _luhn_check(number: str) -> bool:
    """Validate a credit card number using the Luhn algorithm."""
    digits = [int(d) for d in number if d.isdigit()]
    if len(digits) < 13 or len(digits) > 19:
        return False
    checksum = 0
    reverse = digits[::-1]
    for i, d in enumerate(reverse):
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        checksum += d
    return checksum % 10 == 0


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

    label: str
    matched_text: str
    severity: float
    start: int
    end: int
    redacted: str = ""

    def __post_init__(self) -> None:
        if not self.redacted:
            self.redacted = self._redact(self.matched_text, self.label)

    @staticmethod
    def _redact(text: str, label: str) -> str:
        if label == "email_address":
            parts = text.split("@")
            if len(parts) == 2:
                return parts[0][:2] + "***@" + parts[1]
        if label == "phone_number":
            digits = re.sub(r"\D", "", text)
            return "***" + digits[-4:]
        if label in ("credit_card", "ssn", "aadhaar_number"):
            digits = re.sub(r"\D", "", text)
            return "***" + digits[-4:]
        if label == "ip_address":
            return "***.***.***.***"
        return "***"


@dataclass
class PIIResult:
    """Result of PII scanning."""

    is_detected: bool = False
    score: float = 0.0
    matches: List[PIIMatch] = field(default_factory=list)

    @property
    def labels(self) -> list[str]:
        return [m.label for m in self.matches]

    @property
    def redaction_map(self) -> dict[str, str]:
        return {m.matched_text: m.redacted for m in self.matches}


class PIIDetector:
    """Detects personally identifiable information in text."""

    def __init__(
        self,
        custom_patterns: Optional[list[tuple[str, str, float]]] = None,
        use_ner: bool = False,
    ) -> None:
        self._patterns = list(_PII_PATTERNS)
        if custom_patterns:
            for label, pattern, severity in custom_patterns:
                self._patterns.append((label, re.compile(pattern), severity))
        self._use_ner = use_ner
        self._nlp = None
        if use_ner:
            self._init_ner()

    def _init_ner(self) -> None:
        """Lazy-load spaCy for NER-based PII detection."""
        try:
            import spacy

            try:
                self._nlp = spacy.load("en_core_web_sm")
            except OSError:
                self._use_ner = False
        except ImportError:
            self._use_ner = False

    def _ner_detect(self, text: str) -> list[PIIMatch]:
        """Use spaCy NER to find person names and organizations."""
        if not self._use_ner or self._nlp is None:
            return []
        matches: list[PIIMatch] = []
        doc = self._nlp(text)
        for ent in doc.ents:
            if ent.label_ == "PERSON":
                matches.append(
                    PIIMatch(
                        label="person_name",
                        matched_text=ent.text,
                        severity=0.6,
                        start=ent.start_char,
                        end=ent.end_char,
                        redacted="[PERSON]",
                    )
                )
            elif ent.label_ == "ORG":
                matches.append(
                    PIIMatch(
                        label="organization",
                        matched_text=ent.text,
                        severity=0.3,
                        start=ent.start_char,
                        end=ent.end_char,
                        redacted="[ORG]",
                    )
                )
        return matches

    def detect(self, text: str) -> PIIResult:
        """Scan *text* for personally identifiable information."""
        matches: list[PIIMatch] = []
        seen_spans: set[tuple[int, int]] = set()

        for label, pattern, severity in self._patterns:
            for m in pattern.finditer(text):
                span = (m.start(), m.end())
                if span in seen_spans:
                    continue

                matched = m.group()

                # Extra validation for credit cards
                if label == "credit_card":
                    digits = re.sub(r"\D", "", matched)
                    if not _luhn_check(digits):
                        continue

                seen_spans.add(span)
                matches.append(
                    PIIMatch(
                        label=label,
                        matched_text=matched,
                        severity=severity,
                        start=m.start(),
                        end=m.end(),
                    )
                )

        # NER-based detection
        ner_matches = self._ner_detect(text)
        for nm in ner_matches:
            span = (nm.start, nm.end)
            if span not in seen_spans:
                seen_spans.add(span)
                matches.append(nm)

        score = max((m.severity for m in matches), default=0.0)

        return PIIResult(
            is_detected=len(matches) > 0,
            score=round(score, 4),
            matches=matches,
        )
