"""Long numeric ID detector.

Catches sequences of digits that are long enough to be account numbers,
customer IDs, reference numbers, etc., while avoiding common false positives
such as years, order IDs with context, and short sequences.

False-positive guards:
- Minimum 8 digits (order IDs, customer numbers start around here).
- 4-digit sequences are always skipped (years, ports, PINs better handled elsewhere).
- 5-digit US ZIP already handled by address detector; skip if flagged there.
- Segments that are clearly part of a phone/card/IBAN (adjacent to +/- patterns)
  are left to the higher-priority detectors.
- Context-triggered path: if surrounded by ID-like keywords the threshold drops
  to 6 digits to catch shorter customer/order IDs with clear labelling.
"""
import re
from typing import Dict, List, Union

from upf.config import UPFConfig
from upf.detectors.deterministic_ids import is_valid_credit_card

# Raw digit runs (may include spaces/hyphens as separators).
# _LONG_NUM_RE: requires ≥8 total chars (standalone use)
_LONG_NUM_RE = re.compile(
    r"(?<!\d)(\d[\d \-]{6,}\d)(?!\d)"
)
# _SHORT_NUM_RE: 4–7 total chars — only used in context-triggered path
_SHORT_NUM_RE = re.compile(
    r"(?<!\d)(\d{4,7})(?!\d)"
)

# Context keywords that allow shorter (≥6 digit) numeric IDs to be flagged.
_ID_CONTEXT_RE = re.compile(
    r"(?i)\b(?:customer\s*(?:id|number|no\.?)|client\s*(?:id|number)|"
    r"account\s*(?:id|no\.?)|member\s*(?:id|number)|"
    r"reference\s*(?:id|number|no\.?)|order\s*(?:id|no\.?)|"
    r"patient\s*(?:id|number)|employee\s*(?:id|number)|"
    r"user\s*id|student\s*(?:id|number)|"
    r"id\s*[=:\-]|identificat(?:or|ion)|nr\.?|no\.?)\s*[=:\-]?\s*$"
)

# Patterns that indicate the number is part of a higher-priority type.
# We skip if the raw digits (stripped) match these.
_PHONE_CARD_HINT_RE = re.compile(r"^\+?\d[\d\s\-]{9,}\d$")  # phone-like with +
_YEAR_ONLY_RE = re.compile(r"^(19|20)\d{2}$")

# How many raw digits must the sequence contain (after stripping separators)?
_MIN_DIGITS_STANDALONE = 8
_MIN_DIGITS_WITH_CONTEXT = 6


def _digit_count(s: str) -> int:
    return sum(1 for c in s if c.isdigit())


def _is_likely_iban_tail(text: str, start: int, raw: str) -> bool:
    """Return True if raw numeric run appears to be inside an IBAN token."""
    digits = raw.replace(" ", "").replace("-", "")
    if len(digits) < 10:
        return False

    # Common case: "PT5000..." where numeric run starts right after country code.
    if start >= 2:
        prev2 = text[start - 2:start]
        if prev2.isalpha() and prev2.upper() == prev2:
            return True

    # "GB82WEST123..." tail starts after alnum letters; check nearby token prefix.
    token_start = start
    while token_start > 0 and text[token_start - 1].isalnum():
        token_start -= 1
    prefix = text[token_start:start]
    if re.fullmatch(r"[A-Z]{2}\d{2}[A-Z]{0,6}", prefix):
        return True
    return False


def detect_numeric_id_entities(text: str, config: UPFConfig) -> List[Dict[str, Union[int, str]]]:
    """Detect long numeric IDs that are likely account/reference/customer numbers."""
    if not getattr(config, "redact_numeric_ids", True):
        return []

    findings: List[Dict[str, Union[int, str]]] = []
    seen: set = set()

    def _try_add(m, raw: str) -> None:
        start, end = m.start(1), m.end(1)
        stripped = raw.replace(" ", "").replace("-", "")

        if _YEAR_ONLY_RE.match(stripped):
            return
        if is_valid_credit_card(raw):
            return
        if _is_likely_iban_tail(text, start, raw):
            return
        # Skip if preceded by + (phone number)
        if start > 0 and text[start - 1] == "+":
            return
        if raw.startswith("+"):
            return

        key = (start, end)
        if key not in seen:
            seen.add(key)
            findings.append({"type": "LONG_NUMERIC_ID", "start": start, "end": end, "text": raw})

    # Pass 1: standalone long numbers (≥8 digits total)
    for m in _LONG_NUM_RE.finditer(text):
        raw = m.group(1)
        if _digit_count(raw) >= _MIN_DIGITS_STANDALONE:
            _try_add(m, raw)

    # Pass 2: context-triggered shorter numbers (≥6 digits)
    for m in _SHORT_NUM_RE.finditer(text):
        raw = m.group(1)
        n_digits = _digit_count(raw)
        if n_digits < _MIN_DIGITS_WITH_CONTEXT:
            continue
        start = m.start(1)
        preceding = text[max(0, start - 80): start]
        if _ID_CONTEXT_RE.search(preceding):
            _try_add(m, raw)

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