"""Country, currency, language, timezone, and subdivision data from standards packages."""
from __future__ import annotations

from datetime import UTC, datetime
from functools import lru_cache
from zoneinfo import ZoneInfo, available_timezones

import pycountry
from phonenumbers.phonenumberutil import COUNTRY_CODE_TO_REGION_CODE

CATALOG_VERSION = "iso-3166-2026-09"

# ISO 4217 currencies whose minor unit (decimal digits) is not the default of 2.
# None means "no minor unit applies" (precious metals, testing/no-currency codes).
# Source: ISO 4217 amendment tables; review when adding a currency that trades at a
# non-standard precision.
_CURRENCY_MINOR_UNIT_OVERRIDES: dict[str, int | None] = {
    "BHD": 3, "BIF": 0, "CLF": 4, "CLP": 0, "DJF": 0, "GNF": 0, "IQD": 3, "ISK": 0,
    "JOD": 3, "JPY": 0, "KMF": 0, "KRW": 0, "KWD": 3, "LYD": 3, "OMR": 3, "PYG": 0,
    "RWF": 0, "TND": 3, "UGX": 0, "UYI": 0, "UYW": 4, "VND": 0, "VUV": 0, "XAF": 0,
    "XAG": None, "XAU": None, "XOF": 0, "XPD": None, "XPF": 0, "XPT": None,
    "XSU": None, "XTS": None, "XUA": None, "XXX": None,
}

# Legacy/backward-compatible IANA zone groupings that duplicate a canonical zone
# elsewhere in the database (e.g. "US/Eastern" == "America/New_York"). Excluded so
# the catalog lists each real timezone once. Not exhaustive of every tzdata alias.
_LEGACY_ZONE_PREFIXES = ("US/", "Canada/", "Brazil/", "Mexico/", "Chile/", "Etc/")


def _flag(alpha2: str) -> str:
    return "".join(chr(127397 + ord(char)) for char in alpha2.upper())


@lru_cache(maxsize=1)
def countries() -> tuple[dict[str, object], ...]:
    """Return every ISO 3166-1 country with its E.164 dialing metadata where available."""
    calling_codes: dict[str, list[str]] = {}
    for calling_code, regions in COUNTRY_CODE_TO_REGION_CODE.items():
        for region in regions:
            calling_codes.setdefault(region, []).append(f"+{calling_code}")

    rows = []
    for country in pycountry.countries:
        alpha2 = country.alpha_2
        rows.append(
            {
                "code": alpha2,
                "name": country.name,
                "official_name": getattr(country, "official_name", None),
                "flag": _flag(alpha2),
                "calling_codes": sorted(
                    calling_codes.get(alpha2, []), key=lambda code: (len(code), code)
                ),
            }
        )
    return tuple(sorted(rows, key=lambda row: str(row["name"])))


def normalize_country_codes(codes: list[str] | None) -> list[str]:
    """Normalize and validate ISO alpha-2 codes supplied in a project policy."""
    known = {str(country["code"]) for country in countries()}
    normalized = sorted({code.strip().upper() for code in (codes or []) if code and code.strip()})
    unknown = [code for code in normalized if code not in known]
    if unknown:
        raise ValueError(f"Unknown ISO country code(s): {', '.join(unknown)}")
    return normalized


def subdivisions(country_code: str) -> list[dict[str, str]]:
    """Return ISO 3166-2 subdivisions for an ISO alpha-2 country code."""
    code = country_code.strip().upper()
    if code not in {str(country["code"]) for country in countries()}:
        raise ValueError(f"Unknown ISO country code: {code}")
    rows = [
        {
            "code": subdivision.code,
            "name": subdivision.name,
            "type": subdivision.type,
        }
        for subdivision in pycountry.subdivisions.get(country_code=code)
    ]
    return sorted(rows, key=lambda row: row["name"])


@lru_cache(maxsize=1)
def currencies() -> tuple[dict[str, object], ...]:
    """Return every ISO 4217 currency with its minor-unit (decimal digit) precision."""
    rows = [
        {
            "code": currency.alpha_3,
            "name": currency.name,
            "numeric_code": getattr(currency, "numeric", None),
            "decimal_digits": _CURRENCY_MINOR_UNIT_OVERRIDES.get(currency.alpha_3, 2),
        }
        for currency in pycountry.currencies
    ]
    return tuple(sorted(rows, key=lambda row: str(row["code"])))


@lru_cache(maxsize=1)
def languages() -> tuple[dict[str, object], ...]:
    """Return ISO 639-1 languages (two-letter subtags) — the practical set for locale selectors."""
    rows = [
        {
            "code": language.alpha_2,
            "alpha_3": getattr(language, "alpha_3", None),
            "name": language.name,
        }
        for language in pycountry.languages
        if getattr(language, "alpha_2", None)
    ]
    return tuple(sorted(rows, key=lambda row: str(row["name"])))


def timezones() -> tuple[dict[str, object], ...]:
    """Return canonical IANA timezone identifiers with their current UTC offset.

    Offsets change with daylight saving and are computed at call time — do not cache
    this result across a DST transition.
    """
    now = datetime.now(UTC)
    rows = []
    for tz_id in available_timezones():
        if tz_id != "UTC" and "/" not in tz_id:
            continue
        if tz_id.startswith(_LEGACY_ZONE_PREFIXES):
            continue
        offset = ZoneInfo(tz_id).utcoffset(now)
        rows.append(
            {
                "id": tz_id,
                "label": tz_id.replace("_", " "),
                "utc_offset_minutes": int(offset.total_seconds() // 60) if offset else 0,
            }
        )
    return tuple(sorted(rows, key=lambda row: (row["utc_offset_minutes"], row["id"])))
