"""Deterministic identifier detector (pure Python fallback).

Provides checksum-backed detection for high-confidence identifiers so recall
does not depend on optional Rust bindings.

Detected types:
- IBAN
- CREDIT_CARD
- PESEL
- PT_NIF
- BR_CPF
"""
import re
from typing import Dict, List, Union

from upf.config import UPFConfig

_IBAN_RE = re.compile(r"\b[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}\b", re.IGNORECASE)
_CARD_RE = re.compile(r"\b(?:\d[ -]?){13,19}\d\b")
_PESEL_RE = re.compile(r"\b\d{11}\b")
_PT_NIF_RE = re.compile(r"\b\d{9}\b")
_BR_CPF_RE = re.compile(r"\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b")


def _digits(value: str) -> str:
    return "".join(c for c in value if c.isdigit())


def is_valid_credit_card(value: str) -> bool:
    digits = _digits(value)
    if not (13 <= len(digits) <= 19):
        return False

    total = 0
    dbl = False
    for ch in reversed(digits):
        n = ord(ch) - ord("0")
        if dbl:
            n *= 2
            if n > 9:
                n -= 9
        total += n
        dbl = not dbl
    return total % 10 == 0


def is_valid_iban(value: str) -> bool:
    cleaned = "".join(c for c in value.upper() if c.isalnum())
    if not (15 <= len(cleaned) <= 34):
        return False

    rearranged = cleaned[4:] + cleaned[:4]
    rem = 0
    for ch in rearranged:
        if ch.isdigit():
            rem = (rem * 10 + int(ch)) % 97
        elif "A" <= ch <= "Z":
            mapped = ord(ch) - ord("A") + 10
            rem = (rem * 10 + mapped // 10) % 97
            rem = (rem * 10 + mapped % 10) % 97
        else:
            return False
    return rem == 1


def is_valid_pesel(value: str) -> bool:
    if len(value) != 11 or not value.isdigit():
        return False
    weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3]
    s = sum(weights[i] * int(value[i]) for i in range(10))
    checksum = (10 - (s % 10)) % 10
    return checksum == int(value[10])


def is_valid_portuguese_nif(value: str) -> bool:
    if len(value) != 9 or not value.isdigit():
        return False
    digits = [int(c) for c in value]
    if digits[0] == 0:
        return False
    s = sum(d * (9 - i) for i, d in enumerate(digits[:8]))
    chk = 11 - (s % 11)
    expected = 0 if chk >= 10 else chk
    return digits[8] == expected


def is_valid_brazilian_cpf(value: str) -> bool:
    digits = [int(c) for c in _digits(value)]
    if len(digits) != 11:
        return False
    if all(d == digits[0] for d in digits):
        return False

    s1 = sum(d * (10 - i) for i, d in enumerate(digits[:9]))
    c1 = 0 if s1 % 11 < 2 else 11 - (s1 % 11)
    if c1 != digits[9]:
        return False

    s2 = sum(d * (11 - i) for i, d in enumerate(digits[:10]))
    c2 = 0 if s2 % 11 < 2 else 11 - (s2 % 11)
    return c2 == digits[10]


def detect_deterministic_id_entities(
    text: str, config: UPFConfig
) -> List[Dict[str, Union[int, str]]]:
    """Detect deterministic identifiers with checksum-backed validation."""
    findings: List[Dict[str, Union[int, str]]] = []
    seen: set = set()

    def _add(start: int, end: int, raw: str, etype: str) -> None:
        key = (start, end, etype)
        if key in seen:
            return
        seen.add(key)
        findings.append({
            "type": etype,
            "start": start,
            "end": end,
            "text": raw,
            "detector": "deterministic",
        })

    # Structural deterministic IDs
    for m in _CARD_RE.finditer(text):
        raw = m.group()
        if is_valid_credit_card(raw):
            _add(m.start(), m.end(), raw, "CREDIT_CARD")

    for m in _IBAN_RE.finditer(text):
        raw = m.group()
        if is_valid_iban(raw):
            _add(m.start(), m.end(), raw, "IBAN")

    # National IDs (optional gate)
    if getattr(config, "redact_national_ids", True):
        for m in _PESEL_RE.finditer(text):
            raw = m.group()
            if is_valid_pesel(raw):
                _add(m.start(), m.end(), raw, "PESEL")

        for m in _PT_NIF_RE.finditer(text):
            raw = m.group()
            if is_valid_portuguese_nif(raw):
                _add(m.start(), m.end(), raw, "PT_NIF")

        for m in _BR_CPF_RE.finditer(text):
            raw = m.group()
            if is_valid_brazilian_cpf(raw):
                _add(m.start(), m.end(), raw, "BR_CPF")

    findings.sort(key=lambda f: (int(f["start"]), int(f["end"])))
    return findings
