"""Address detector: street addresses, ZIP/postal codes, PO Boxes."""
import re
from typing import Dict, List, Union

from upf.config import UPFConfig

# ---------------------------------------------------------------------------
# Street address patterns
# ---------------------------------------------------------------------------

# Common English street suffixes (abbreviated or full)
_STREET_SUFFIX = (
    r"(?:St(?:reet)?|Ave(?:nue)?|Blvd|Boulevard|Rd|Road|Dr(?:ive)?|Ln|Lane|"
    r"Ct|Court|Pl|Place|Cir|Circle|Way|Terr(?:ace)?|Pkwy|Parkway|Hwy|Highway|"
    r"Sq|Square|Crescent|Cres|Trail|Trl|Loop|Alley|Aly)"
)

# "123 Main Street" / "45B Oak Ave NW" / "1600 Pennsylvania Ave"
_STREET_ADDRESS_RE = re.compile(
    r"\b\d{1,5}[A-Za-z]?\s+(?:[A-Z][a-zA-Z]+\s+){1,3}" + _STREET_SUFFIX + r"\.?\b",
    re.IGNORECASE,
)

# Apartment / unit qualifiers that commonly follow a street address
_APT_RE = re.compile(
    r"(?i)\b(?:apt|apartment|unit|suite|ste|floor|fl|#)\s*[A-Za-z0-9-]+\b"
)

# ---------------------------------------------------------------------------
# Postal / ZIP code patterns
# ---------------------------------------------------------------------------

# US ZIP: 12345 or 12345-6789
_US_ZIP_RE = re.compile(r"\b\d{5}(?:-\d{4})?\b")

# UK postcode: SW1A 2AA / EC1A 1BB
_UK_POST_RE = re.compile(
    r"\b[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}\b", re.IGNORECASE
)

# Canadian postal code: A1A 1A1
_CA_POST_RE = re.compile(
    r"\b[A-Z]\d[A-Z]\s*\d[A-Z]\d\b", re.IGNORECASE
)

# Generic European 4–6 digit postal codes (DE, FR, IT, NL, PL, ES …)
# Only flag when preceded by a country-context keyword or followed by a city name.
_EU_POST_RE = re.compile(
    r"(?i)(?:postal\s+code|zip|postcode|plz|cedex|cp|cap|PLZ)\s*[:\-]?\s*(\d{4,6})\b"
)

# ---------------------------------------------------------------------------
# PO Box
# ---------------------------------------------------------------------------

_PO_BOX_RE = re.compile(r"(?i)\bP\.?\s*O\.?\s*Box\s+\d+\b")


def detect_address_entities(text: str, config: UPFConfig) -> List[Dict[str, Union[int, str]]]:
    """Detect address-type PII: street addresses, postal codes, PO Boxes."""
    findings: List[Dict[str, Union[int, str]]] = []
    seen: set = set()

    def _add(start: int, end: int, matched: str) -> None:
        if (start, end) not in seen:
            seen.add((start, end))
            findings.append({"type": "ADDRESS", "start": start, "end": end, "text": matched})

    # Street addresses
    for m in _STREET_ADDRESS_RE.finditer(text):
        _add(m.start(), m.end(), m.group())

    # PO Box
    for m in _PO_BOX_RE.finditer(text):
        _add(m.start(), m.end(), m.group())

    # UK postcodes
    for m in _UK_POST_RE.finditer(text):
        _add(m.start(), m.end(), m.group())

    # Canadian postal codes
    for m in _CA_POST_RE.finditer(text):
        _add(m.start(), m.end(), m.group())

    # EU postal codes (context-triggered only to avoid digit false positives)
    for m in _EU_POST_RE.finditer(text):
        # group(1) is the digit sequence; use the full match span for context
        _add(m.start(1), m.end(1), m.group(1))

    # US ZIP — only flag when there's an address-like context nearby to reduce
    # false positives (years, IDs, phone fragments all look like 5-digit numbers).
    for m in _US_ZIP_RE.finditer(text):
        start, end = m.start(), m.end()
        # Check 60 chars of surrounding context for address signals
        window = text[max(0, start - 60): end + 20].lower()
        address_signals = (
            "street", "st ", "ave", "blvd", "road", "rd ", "drive", "dr ",
            "lane", "ln ", "court", "ct ", "place", "pl ", "zip", "postal",
        )
        if any(sig in window for sig in address_signals):
            _add(start, end, m.group())

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