"""
AliothPress i18n — lightweight JSON-based translation system.

Admin translations:
  Usage in templates:  {{ t('sidebar.dashboard') }}
  Usage in Python:     from i18n import t, t_for, set_admin_language
  Translations stored in static/i18n/{lang_code}.json

  t()      — translates using current session language (per-request)
  t_for()  — translates for a specific language (for emails, background tasks)

Public translations:
  Usage in templates:  {{ pt('read_more') }}
  Translations stored in static/i18n/public.json (all languages in one file)
  Language determined by visitor's current_language, NOT admin language.

  pt()     — translates using visitor's language from g.current_language
  pt_for() — translates for a specific language (e.g. unsubscribe page)
"""

import json
import os

from flask import g

# In-memory cache: { 'en': { 'sidebar.dashboard': 'Dashboard', ... }, 'de': { ... } }
_translations = {}

# Public translations cache: { 'read_more': { 'en': 'Read more', 'de': 'Weiterlesen', ... }, ... }
_public_translations = {}

# Current admin language (per-request, set via before_request)
_current_lang = 'en'
_fallback_lang = 'en'

# Path to translation files
_i18n_dir = None

# Extra translation directories registered by plugins.
# Each dir may contain {lang_code}.json files (admin strings, same nested
# format as core files) and/or public.json (public strings, same format
# as the core public.json). Plugin keys should be namespaced under the
# plugin id, e.g. { "my_plugin": { "nav_label": "My Plugin" } }.
_extra_i18n_dirs = []


def init_i18n(app):
    """Initialize i18n system. Call once at app startup."""
    global _i18n_dir
    _i18n_dir = os.path.join(app.static_folder, 'i18n')

    # Pre-load English (fallback) at startup
    _load_language('en')

    # Pre-load public translations (all languages in one file)
    _load_public_translations()

    # Make t(), pt(), t_for(), pt_for(), and get_admin_language() available in all Jinja2 templates
    app.jinja_env.globals['t'] = t
    app.jinja_env.globals['t_for'] = t_for
    app.jinja_env.globals['pt'] = pt
    app.jinja_env.globals['pt_for'] = pt_for
    app.jinja_env.globals['get_admin_language'] = get_admin_language

    # Register date formatting filters (localized month names + flexible format)
    app.jinja_env.filters['format_date'] = format_date
    app.jinja_env.filters['format_datetime'] = format_datetime
    app.jinja_env.filters['format_time'] = format_time
    # Raw converted datetime for templates that run their own strftime
    app.jinja_env.filters['site_tz'] = to_display_tz


def _flatten(data):
    """Flatten nested keys: { "sidebar": { "dashboard": "Dashboard" } }
    becomes: { "sidebar.dashboard": "Dashboard" }"""
    flat = {}
    for section_key, section_val in data.items():
        if section_key.startswith('_'):
            continue  # Skip _meta, _comment, etc.
        if isinstance(section_val, dict):
            for key, value in section_val.items():
                flat[f'{section_key}.{key}'] = value
        elif isinstance(section_val, str):
            flat[section_key] = section_val
    return flat


def _read_lang_file(directory, lang_code):
    """Read and flatten one {lang}.json file. Returns {} if missing/broken."""
    filepath = os.path.join(directory, f'{lang_code}.json')
    if not os.path.exists(filepath):
        return {}
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            return _flatten(json.load(f))
    except (json.JSONDecodeError, IOError):
        return {}  # Silently skip broken files


def _load_language(lang_code):
    """Load a language JSON file into memory cache (core + plugin dirs)."""
    global _translations
    if lang_code in _translations:
        return  # Already loaded

    flat = _read_lang_file(_i18n_dir, lang_code)

    # Merge plugin translations. Core keys win on collision, so a plugin
    # can never silently override core UI strings.
    for extra_dir in _extra_i18n_dirs:
        for key, value in _read_lang_file(extra_dir, lang_code).items():
            flat.setdefault(key, value)

    if flat:
        _translations[lang_code] = flat


def register_extra_i18n_dir(path):
    """Register a plugin's translation directory (admin + public strings).

    Called by the plugin system during startup. Merges immediately into
    every language already cached, and is consulted by all later loads.
    """
    global _extra_i18n_dirs
    if not path or not os.path.isdir(path) or path in _extra_i18n_dirs:
        return
    _extra_i18n_dirs.append(path)

    # Merge into languages that are already in memory (e.g. 'en' preloaded)
    for lang_code in list(_translations.keys()):
        for key, value in _read_lang_file(path, lang_code).items():
            _translations[lang_code].setdefault(key, value)

    # Merge the plugin's public.json (visitor-facing strings)
    public_path = os.path.join(path, 'public.json')
    if os.path.exists(public_path):
        try:
            with open(public_path, 'r', encoding='utf-8') as f:
                extra_public = json.load(f)
            for key, langs in extra_public.items():
                if key.startswith('_'):
                    continue
                _public_translations.setdefault(key, langs)
        except (json.JSONDecodeError, IOError):
            pass


def set_admin_language(lang_code):
    """Set the current admin language for this request."""
    global _current_lang
    _current_lang = lang_code
    _load_language(lang_code)


def get_admin_language():
    """Get current admin language code."""
    return _current_lang


def t(key, **kwargs):
    """Translate a key. Supports {variable} substitution.

    Examples:
        t('sidebar.dashboard')           → "Dashboard"
        t('flash.invite_sent', email=x)  → "Invitation sent to user@example.com!"

    Falls back to English if key not found in current language.
    Falls back to the key itself if not found anywhere.
    """
    # Try current language
    lang_data = _translations.get(_current_lang, {})
    value = lang_data.get(key)

    # Fallback to English
    if not value and _current_lang != _fallback_lang:
        fallback_data = _translations.get(_fallback_lang, {})
        value = fallback_data.get(key)

    # Final fallback: return the key itself (makes missing translations visible)
    if not value:
        return key

    # Substitute {variables} if any kwargs provided
    if kwargs:
        try:
            value = value.format(**kwargs)
        except (KeyError, IndexError):
            pass  # Return unformatted if substitution fails

    return value


def t_for(key, lang_code, **kwargs):
    """Translate a key for a specific language (not tied to current session).

    Use this when the recipient's language differs from the logged-in admin's:
      - Password reset emails  → user.admin_language
      - Invite emails          → current_user.admin_language (inviter's language)
      - Any out-of-request context

    Examples:
        t_for('email.reset_title', user.admin_language)
        t_for('email.invite_subject', 'de', site_name='My Site')

    Falls back to English, then to the key itself.
    """
    _load_language(lang_code)

    # Try requested language
    lang_data = _translations.get(lang_code, {})
    value = lang_data.get(key)

    # Fallback to English
    if not value and lang_code != _fallback_lang:
        fallback_data = _translations.get(_fallback_lang, {})
        value = fallback_data.get(key)

    # Final fallback: return the key itself
    if not value:
        return key

    # Substitute {variables}
    if kwargs:
        try:
            value = value.format(**kwargs)
        except (KeyError, IndexError):
            pass

    return value


def _load_public_translations():
    """Load public.json into memory cache at startup.

    public.json structure:
      { "read_more": { "en": "Read more", "de": "Weiterlesen", ... }, ... }
    """
    global _public_translations
    filepath = os.path.join(_i18n_dir, 'public.json')
    if not os.path.exists(filepath):
        return

    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            _public_translations = json.load(f)
    except (json.JSONDecodeError, IOError):
        pass  # Silently skip if broken


def pt(key, **kwargs):
    """Public translate — uses visitor's language, NOT admin language.

    Reads from static/i18n/public.json (cached in memory).
    Language is taken from g.current_language (set in app.py context_processor).
    Falls back to English, then to the key itself.
    Supports {variable} substitution.

    Usage in templates:  {{ pt('read_more') }}
    Usage in Python:     pt('unsubscribed_body', site_name='My Site')
    """
    translations = _public_translations.get(key)
    if not translations:
        return key

    lang = getattr(g, 'current_language', None) or _fallback_lang
    value = translations.get(lang)

    # Fallback to English
    if not value and lang != _fallback_lang:
        value = translations.get(_fallback_lang)

    if not value:
        return key

    # Substitute {variables}
    if kwargs:
        try:
            value = value.format(**kwargs)
        except (KeyError, IndexError):
            pass

    return value


def pt_for(key, lang_code, **kwargs):
    """Public translate for a specific language (not tied to g.current_language).

    Use this when rendering public content outside a normal request context,
    e.g. the unsubscribe page where the language comes from a URL parameter.

    Usage:  pt_for('unsubscribed_body', 'de', site_name='My Site')
    """
    translations = _public_translations.get(key)
    if not translations:
        return key

    value = translations.get(lang_code)

    # Fallback to English
    if not value and lang_code != _fallback_lang:
        value = translations.get(_fallback_lang)

    if not value:
        return key

    if kwargs:
        try:
            value = value.format(**kwargs)
        except (KeyError, IndexError):
            pass

    return value


# ============================================================================
# DATE FORMATTING (localized)
# ============================================================================

# ── Site timezone hook ──────────────────────────────────────────────────────
# All datetimes are STORED as naive UTC (utc_now() in models.py). For display
# they are converted to the site timezone configured in Settings. The actual
# converter lives in app.py (it needs the Settings model); it registers itself
# here to avoid an import cycle. When no converter is registered (e.g. unit
# tests importing i18n alone) datetimes pass through unchanged.
_tz_converter = None


def set_timezone_converter(fn):
    """Register a callable(dt) -> dt that converts stored naive-UTC datetimes
    to the site's display timezone. Called once from app.py at startup."""
    global _tz_converter
    _tz_converter = fn


def to_display_tz(dt):
    """Convert a stored datetime to the site timezone for display.
    Safe: returns the input unchanged if no converter is set or on error."""
    if dt is None or _tz_converter is None:
        return dt
    try:
        return _tz_converter(dt)
    except Exception:
        return dt


# Fallback English month names (used if translation file has no dates section)
_EN_MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
_EN_MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June',
                   'July', 'August', 'September', 'October', 'November', 'December']


def _get_date_translations(lang=None):
    """Get month names and format patterns for a language.

    Returns (months_short, months_long, fmt_date, fmt_datetime).
    Falls back to English, then to hardcoded defaults.
    """
    lang = lang or _current_lang

    # Helper: try lang, then fallback
    def _get(key):
        val = _translations.get(lang, {}).get(key)
        if val is None and lang != _fallback_lang:
            val = _translations.get(_fallback_lang, {}).get(key)
        return val

    months_short = _get('dates.months_short') or _EN_MONTHS_SHORT
    months_long = _get('dates.months_long') or _EN_MONTHS_LONG
    fmt_date = _get('dates.format_date') or '{month_short} {day}, {year}'
    fmt_datetime = _get('dates.format_datetime') or '{month_short} {day}, {time}'

    return months_short, months_long, fmt_date, fmt_datetime


def format_date(dt, fmt=None):
    """Jinja filter: format a date using current admin language.

    Usage in templates:
        {{ post.updated_at|format_date }}           → "Mar 12, 2026"  (en)
        {{ post.updated_at|format_date('long') }}   → "March 12, 2026"

    Each language JSON can customize:
        - dates.months_short / dates.months_long  (month names)
        - dates.format_date  (order and separators)

    Safe: returns '' for None, never raises.
    """
    if dt is None:
        return ''

    dt = to_display_tz(dt)
    months_short, months_long, fmt_pattern, _ = _get_date_translations()

    try:
        month_idx = dt.month - 1  # 0-based
        result = fmt_pattern.format(
            day=dt.day,
            day0=str(dt.day).zfill(2),
            month=dt.month,
            month0=str(dt.month).zfill(2),
            month_short=months_short[month_idx],
            month_long=months_long[month_idx],
            year=dt.year
        )
        return result
    except (AttributeError, IndexError, KeyError):
        # Fallback: ISO format
        try:
            return dt.strftime('%Y-%m-%d')
        except Exception:
            return str(dt)


def format_datetime(dt, fmt=None):
    """Jinja filter: format a datetime using current admin language.

    Usage in templates:
        {{ log.created_at|format_datetime }}         → "Mar 12, 14:30"  (en)

    Each language JSON can customize:
        - dates.months_short  (month names)
        - dates.format_datetime  (order and separators)

    Safe: returns '' for None, never raises.
    """
    if dt is None:
        return ''

    dt = to_display_tz(dt)
    months_short, months_long, _, fmt_pattern = _get_date_translations()

    try:
        month_idx = dt.month - 1
        time_str = dt.strftime('%H:%M')
        result = fmt_pattern.format(
            day=dt.day,
            day0=str(dt.day).zfill(2),
            month=dt.month,
            month0=str(dt.month).zfill(2),
            month_short=months_short[month_idx],
            month_long=months_long[month_idx],
            year=dt.year,
            time=time_str
        )
        return result
    except (AttributeError, IndexError, KeyError):
        try:
            return dt.strftime('%Y-%m-%d %H:%M')
        except Exception:
            return str(dt)


def format_time(dt, fmt='%H:%M'):
    """Jinja filter: time-of-day in the site timezone.

    Usage in templates:
        {{ post.published_at|format_time }}          → "14:30"

    Safe: returns '' for None, never raises.
    """
    if dt is None:
        return ''
    dt = to_display_tz(dt)
    try:
        return dt.strftime(fmt)
    except Exception:
        return str(dt)
