"""Locale loader for the public landing page (and, eventually, other
public surfaces). Loads YAML translation files from ``app/locales/``
at process startup into in-memory dicts; templates access them via the
``t`` context variable.

Adding a new language: drop ``<code>.yaml`` into app/locales/ and add
the code to ``ACTIVE_PUBLIC_LANGS`` here. No code changes elsewhere
should be required.

Format: arbitrary nested dict. Values that contain inline HTML are
rendered with the Jinja2 ``safe`` filter in the template — keep
markup minimal (``<strong>``, ``<em>``, ``<a>``) and don't put user
input through here. The dicts are wrapped in a small dotted-access
helper so templates can write ``{{ t.hero.subhead }}`` instead of
``{{ t['hero']['subhead'] }}``.
"""
from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml

from app.logging import get_logger

log = get_logger("locales")

LOCALES_DIR = Path(__file__).resolve().parent.parent / "locales"

# Public-surface languages. Mirrors ``services.i18n.ACTIVE_LANGUAGES``
# but is kept separate because the public surface may roll a new
# language out independently of the in-app translations.
ACTIVE_PUBLIC_LANGS = ("en", "it")
DEFAULT_LANG = "en"


class _Dotted:
    """Read-only dotted-access view over a YAML-loaded dict tree.

    Templates write ``{{ t.hero.tagline }}`` instead of
    ``{{ t['hero']['tagline'] }}``. The wrapper deliberately does NOT
    subclass ``dict`` — dict's built-in methods (``items``, ``keys``,
    ``values``, ``copy``, ``update``, ``pop`` …) would shadow YAML
    keys with the same name. With this wrapper, a YAML key called
    ``items`` (which the landing copy actually has: ``not_strip.items``)
    resolves through ``__getattr__`` like any other key.

    Mutability is intentionally not supported: locale data is loaded
    once at startup and read-only afterwards.
    """
    __slots__ = ("_data",)

    def __init__(self, data: dict):
        self._data = data

    def __getattr__(self, key: str) -> Any:
        if key.startswith("_"):
            raise AttributeError(key)
        if key in self._data:
            return _wrap(self._data[key])
        raise AttributeError(key)

    def __getitem__(self, key: str) -> Any:
        return _wrap(self._data[key])

    def __contains__(self, key: object) -> bool:
        return key in self._data

    def __iter__(self):
        return iter(self._data)

    def __len__(self) -> int:
        return len(self._data)

    def __bool__(self) -> bool:
        return bool(self._data)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, _Dotted):
            return self._data == other._data
        return self._data == other

    def __repr__(self) -> str:
        return f"_Dotted({self._data!r})"


def _wrap(value: Any) -> Any:
    if isinstance(value, dict):
        return _Dotted(value)
    if isinstance(value, list):
        return [_wrap(v) for v in value]
    return value


_LOADED: dict[str, _Dotted] = {}


def _load_one(lang: str) -> _Dotted:
    path = LOCALES_DIR / f"{lang}.yaml"
    if not path.exists():
        log.warning("locale.missing_file", lang=lang, path=str(path))
        return _Dotted({})
    raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
    return _Dotted(raw)


def load_locales() -> None:
    """Populate the module-level cache. Called once at app startup;
    subsequent calls re-read from disk (useful for tests)."""
    _LOADED.clear()
    for lang in ACTIVE_PUBLIC_LANGS:
        _LOADED[lang] = _load_one(lang)
    log.info("locales.loaded", languages=list(_LOADED.keys()))


def get_locale(lang: str) -> _Dotted:
    """Return the loaded translations for ``lang``, falling back to
    ``DEFAULT_LANG`` for an unknown code. Lazy-loads on first call
    so importers don't have to remember to call ``load_locales()``
    explicitly."""
    if not _LOADED:
        load_locales()
    if lang in _LOADED:
        return _LOADED[lang]
    return _LOADED.get(DEFAULT_LANG, _Dotted({}))


# ----- request-time language detection ---------------------------------------


# Country codes whose primary language is Italian. cf-ipcountry uses
# ISO-3166 alpha-2.
_GEO_TO_LANG = {
    "IT": "it", "SM": "it", "VA": "it",
    # Italian-speaking Swiss canton: we can't detect canton from country
    # so a Swiss visitor defaults to en here. The Accept-Language path
    # above catches the actual Italian-speakers in CH.
}


def detect_public_lang(
    cookie_lang: str | None,
    accept_language: str | None,
    cf_country: str | None,
    user_lang: str | None,
) -> str:
    """Resolve the language for a public-page request.

    Priority (highest first):
      1. user_lang — a logged-in user's stored preference. Consistent
         with their dashboard, never overridden by detection.
      2. cookie_lang — sticky from a previous explicit toggle.
      3. accept_language — browser locale. First language tag only,
         stripped to its base subtag ("en-US" -> "en").
      4. cf_country — Cloudflare's IP-derived country code, mapped to
         a primary language via ``_GEO_TO_LANG``.
      5. ``DEFAULT_LANG`` ("en").

    Returns a value guaranteed to be in ``ACTIVE_PUBLIC_LANGS``.
    """
    if user_lang and user_lang in ACTIVE_PUBLIC_LANGS:
        return user_lang
    if cookie_lang and cookie_lang in ACTIVE_PUBLIC_LANGS:
        return cookie_lang
    if accept_language:
        first = accept_language.split(",", 1)[0].split(";", 1)[0]
        base = first.split("-", 1)[0].strip().lower()
        if base in ACTIVE_PUBLIC_LANGS:
            return base
    if cf_country:
        cc = cf_country.strip().upper()
        if cc in _GEO_TO_LANG:
            mapped = _GEO_TO_LANG[cc]
            if mapped in ACTIVE_PUBLIC_LANGS:
                return mapped
    return DEFAULT_LANG
