"""Parse postal addresses into structured components."""

from __future__ import annotations

import re
from typing import Any, Dict, Optional, Sequence, Tuple

from .assign_us_region import STATE_ABBREV_SYNONYMS, STATE_NAME_TO_CODE
from ._decorators import safe_transform, with_metrics

# Precompile regex patterns for better performance
US_ABBR_RE = re.compile(
    r"^(?P<street>.+?),\s*(?P<city>[^,]+),\s*(?P<state>[A-Za-z]{2})\s+(?P<postal>\d{5}(?:-\d{4})?)$",
    re.IGNORECASE,
)
US_FULL_RE = re.compile(
    r"^(?P<street>.+?),\s*(?P<city>[^,]+),\s*(?P<state>[A-Za-z][A-Za-z\s]+)\s+(?P<postal>\d{5}(?:-\d{4})?)$",
    re.IGNORECASE,
)
POSTAL_TAIL_RE = re.compile(
    r"(?P<postal>\d{5}(?:-\d{4})?|[A-Z]\d[A-Z]\s?\d[A-Z]\d|[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}|[0-9][A-Z0-9 -]{2,8})$",
    re.IGNORECASE,
)
CANADA_POSTAL_RE = re.compile(r"^[A-Z]\d[A-Z]\s?\d[A-Z]\d$", re.IGNORECASE)
UK_POSTAL_RE = re.compile(r"^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$", re.IGNORECASE)

# Additional international postal patterns
AUSTRALIA_POSTAL_RE = re.compile(r"^\d{4}$")
GERMANY_POSTAL_RE = re.compile(r"^\d{5}$")
JAPAN_POSTAL_RE = re.compile(r"^\d{3}-?\d{4}$")
_STATE_CODES = frozenset(STATE_NAME_TO_CODE.values())


def _normalize_state(state: str) -> str:
    state = (state or "").strip()
    if not state:
        return ""
    upper = state.upper()
    if upper in STATE_ABBREV_SYNONYMS:
        upper = STATE_ABBREV_SYNONYMS[upper]
    if upper in STATE_NAME_TO_CODE:
        return STATE_NAME_TO_CODE[upper]
    if len(upper) == 2:
        return upper
    return state.strip()


def _standardize_postal(postal: str) -> str:
    postal = (postal or "").strip().upper()
    if not postal:
        return ""
    if CANADA_POSTAL_RE.match(postal):
        postal = postal.replace(" ", "")
        return f"{postal[:3]} {postal[3:]}"
    if UK_POSTAL_RE.match(postal):
        postal = postal.replace("  ", " ")
        if " " not in postal:
            return f"{postal[:-3]} {postal[-3:]}"
        return postal
    return postal


def _known_state_code(value: str) -> str:
    upper = (value or "").strip().upper()
    if not upper:
        return ""
    if upper in STATE_ABBREV_SYNONYMS:
        upper = STATE_ABBREV_SYNONYMS[upper]
    if upper in STATE_NAME_TO_CODE:
        return STATE_NAME_TO_CODE[upper]
    if upper in _STATE_CODES:
        return upper
    return ""


def _split_state_postal(part: str) -> Tuple[str, str]:
    part = (part or "").strip()
    if not part:
        return "", ""
    match = POSTAL_TAIL_RE.search(part)
    if not match:
        return part.strip(), ""
    postal = _standardize_postal(match.group("postal"))
    state = part[: match.start()].strip(", ")
    return state, postal


def _split_city_state_postal(part: str) -> Tuple[str, str, str]:
    part = (part or "").strip()
    if not part:
        return "", "", ""

    leading, postal = _split_state_postal(part)
    if not postal:
        leading = part

    tokens = leading.split()
    max_state_tokens = min(4, len(tokens))
    for size in range(max_state_tokens, 0, -1):
        candidate = " ".join(tokens[-size:])
        state_code = _known_state_code(candidate)
        if state_code and len(tokens) > size:
            city = " ".join(tokens[:-size]).strip()
            return city, state_code, postal

    return leading, "", postal


def _match_us_address(text: str) -> Optional[Tuple[str, str, str, str]]:
    for pattern in (US_ABBR_RE, US_FULL_RE):
        match = pattern.match(text)
        if match:
            street = match.group("street").strip()
            city = match.group("city").strip()
            state = _normalize_state(match.group("state"))
            postal = _standardize_postal(match.group("postal"))
            return street, city, state, postal
    return None


def _fallback_parse(parts: Sequence[str]) -> Tuple[str, str, str, str]:
    parts = [p.strip() for p in parts if p and str(p).strip()]
    street = city = state = postal = ""

    if len(parts) >= 4:
        postal = parts[-1]
        state = parts[-2]
        city = parts[-3]
        street = ", ".join(parts[:-3])
    elif len(parts) == 3:
        street, city = parts[0], parts[1]
        state, postal = _split_state_postal(parts[2])
        if not postal:
            parsed_city, parsed_state, parsed_postal = _split_city_state_postal(city)
            if parsed_postal:
                city = parsed_city
                postal = parsed_postal
    elif len(parts) == 2:
        street = parts[0]
        city, state, postal = _split_city_state_postal(parts[1])
    elif parts:
        street = parts[0]

    if not city and state:
        maybe_state, maybe_postal = _split_state_postal(state)
        if maybe_postal:
            postal = maybe_postal
            state = maybe_state
        tokens = state.split()
        if len(tokens) > 1:
            city = " ".join(tokens[:-1])
            state = tokens[-1]

    state_code = _normalize_state(state)
    postal = _standardize_postal(postal)
    return street, city, state_code or state, postal


def _parse_address(raw: Optional[str]) -> Tuple[str, str, str, str]:
    if not raw or not str(raw).strip():
        return "", "", "", ""
    text = re.sub(r"\s+", " ", str(raw).strip())

    us_match = _match_us_address(text)
    if us_match:
        return us_match

    parts = text.split(",")
    return _fallback_parse(parts)


@safe_transform
@with_metrics
def parse_address_components(value: str | None) -> Dict[str, Any]:
    """Parse address into street, city, state, postal components."""
    street, city, state, postal = _parse_address(value)
    meta: Dict[str, Any] = {
        "confidence": 1.0 if all([street, city, state, postal]) else 0.5
    }
    if not any([street, city, state, postal]):
        meta["reason"] = "empty"
    return {"value": [street, city, state, postal], "meta": meta}
