"""Per-column normalization functions for deduplication matching.

Every normalizer is ``str -> str``, handles None/empty gracefully, and is
idempotent (applying it twice yields the same result as once).
"""

from __future__ import annotations

import re
from enum import Enum
from typing import Callable, Optional

import phonenumbers


# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------

class NormalizerType(str, Enum):
    EMAIL = "email"
    PHONE = "phone"
    NAME = "name"
    ADDRESS = "address"
    STRING = "string"


# ---------------------------------------------------------------------------
# String normalizer (base)
# ---------------------------------------------------------------------------

def normalize_string(value: Optional[str]) -> str:
    """Trim, collapse internal whitespace, case-fold."""
    if not value or not isinstance(value, str):
        return ""
    return re.sub(r"\s+", " ", value.strip()).casefold()


# ---------------------------------------------------------------------------
# Email normalizer
# ---------------------------------------------------------------------------

_GMAIL_DOMAINS = {"gmail.com", "googlemail.com"}


def normalize_email(value: Optional[str]) -> str:
    """Lowercase, strip whitespace, strip Gmail dots, strip +tag suffixes."""
    if not value or not isinstance(value, str):
        return ""
    email = value.strip().lower()
    if "@" not in email:
        return email

    local, domain = email.rsplit("@", 1)

    # Strip +tag suffix
    if "+" in local:
        local = local.split("+", 1)[0]

    # Strip dots for Gmail addresses
    if domain in _GMAIL_DOMAINS:
        local = local.replace(".", "")

    return f"{local}@{domain}"


# ---------------------------------------------------------------------------
# Phone normalizer
# ---------------------------------------------------------------------------

def normalize_phone(value: Optional[str], default_region: str = "US") -> str:
    """Parse with phonenumbers lib, return E.164. Fallback: digits-only.

    Extensions are preserved as a ``;ext=N`` suffix (RFC 3966 syntax) so
    two records ``+15551234567 ext 100`` and ``+15551234567 ext 200``
    don't normalize to the same key — they're different people at the
    same business.
    """
    if not value or not isinstance(value, str):
        return ""
    stripped = value.strip()
    if not stripped:
        return ""

    try:
        parsed = phonenumbers.parse(stripped, default_region)
        if phonenumbers.is_possible_number(parsed):
            base = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
            if parsed.extension:
                return f"{base};ext={parsed.extension}"
            return base
    except phonenumbers.NumberParseException as e:
        # Surface the fallback so a "wrong duplicate match" investigation
        # can be traced — the fallback only keeps digits, so extensions
        # and country codes inferred from formatting are lost.
        from loguru import logger as _log
        _log.debug(
            "normalize_phone fallback for {!r} ({}): "
            "dropping to digits-only.", stripped, e,
        )

    # Fallback: digits only
    digits = re.sub(r"\D", "", stripped)
    return digits


# ---------------------------------------------------------------------------
# Name normalizer
# ---------------------------------------------------------------------------

_TITLE_PREFIXES = {
    "mr", "mrs", "ms", "miss", "dr", "prof", "professor",
    "sir", "madam", "rev", "reverend", "hon", "honorable",
}
_NAME_SUFFIXES = {
    "jr", "sr", "ii", "iii", "iv", "v",
    "phd", "md", "esq", "dds", "rn",
}
# Surname particles dropped during normalization so that
# ``Charles de Gaulle`` and ``Charles Gaulle`` produce the same key.
_NAME_PARTICLES_DROP = {
    "van", "von", "de", "da", "del", "della", "di", "du",
    "der", "den", "le", "la", "el",
}


def normalize_name(value: Optional[str]) -> str:
    """Strip titles/suffixes/particles, collapse whitespace, case-fold."""
    if not value or not isinstance(value, str):
        return ""
    name = value.strip()
    if not name:
        return ""

    # Case-fold first for matching
    name = name.casefold()

    # Remove periods and commas that are part of titles/suffixes
    name = name.replace(".", " ").replace(",", " ")

    parts = name.split()

    # Strip leading titles
    while parts and parts[0].rstrip(".") in _TITLE_PREFIXES:
        parts.pop(0)

    # Strip trailing suffixes
    while parts and parts[-1].rstrip(".") in _NAME_SUFFIXES:
        parts.pop()

    # Drop surname particles wherever they appear.
    parts = [p for p in parts if p not in _NAME_PARTICLES_DROP]

    return " ".join(parts)


# ---------------------------------------------------------------------------
# Address normalizer
# ---------------------------------------------------------------------------

# Long-form → short-form (USPS) for normalization-side matching keys.
# Sourced from the shared canonical map; lower-cased + augmented with a
# few normalize-only aliases (``saint`` → ``st``, identity entries for
# ``unit`` / ``loop`` / ``way``, ``number`` → ``#``).
from ._constants import USPS_COMPRESSIONS as _SHARED_COMPRESSIONS, US_STATE_NAMES as _SHARED_STATE_NAMES

_USPS_ABBREVIATIONS: dict[str, str] = {
    **{k: v.lower() for k, v in _SHARED_COMPRESSIONS.items()},
    "way": "way", "loop": "loop", "unit": "unit",
    "number": "#",
    "saint": "st",
}

# Lowercase variant of the shared state-name map for matching keys.
_US_STATE_NAMES_NORM: dict[str, str] = {
    name: code.lower() for name, code in _SHARED_STATE_NAMES.items()
}


def normalize_address(value: Optional[str]) -> str:
    """USPS abbreviation normalization, collapse whitespace, case-fold.

    Spelled-out US state names are folded to their 2-letter codes so
    ``California`` and ``CA`` normalize to the same matching key.
    """
    if not value or not isinstance(value, str):
        return ""
    addr = value.strip()
    if not addr:
        return ""

    # Case-fold and clean punctuation (keep #)
    addr = addr.casefold()
    addr = addr.replace(".", " ").replace(",", " ")

    # State names → 2-letter codes (longest first so ``new york`` wins
    # over ``new``-as-a-fragment).
    for full, code in sorted(
        _US_STATE_NAMES_NORM.items(), key=lambda kv: -len(kv[0])
    ):
        addr = re.sub(rf"(?<!\w){re.escape(full)}(?!\w)", code, addr)

    parts = addr.split()
    normalized_parts = []
    for part in parts:
        normalized_parts.append(_USPS_ABBREVIATIONS.get(part, part))

    return " ".join(normalized_parts)


# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------

_NORMALIZER_MAP: dict[NormalizerType, Callable[[str], str]] = {
    NormalizerType.EMAIL: normalize_email,
    NormalizerType.PHONE: normalize_phone,
    NormalizerType.NAME: normalize_name,
    NormalizerType.ADDRESS: normalize_address,
    NormalizerType.STRING: normalize_string,
}


def get_normalizer(normalizer_type: NormalizerType | str) -> Callable[[str], str]:
    """Return the normalizer function for the given type.

    Accepts both ``NormalizerType`` enum values and plain strings.
    """
    if isinstance(normalizer_type, str):
        normalizer_type = NormalizerType(normalizer_type.lower())
    func = _NORMALIZER_MAP.get(normalizer_type)
    if func is None:
        raise ValueError(f"Unknown normalizer type: {normalizer_type}")
    return func
