# app/utils/i18n.py
"""
Internationalization (i18n) utilities for multi-language support.

This module provides:
- Language constants and configuration
- JSON-based translation loading
- Translation helper functions
- Jinja2 template integration

Supported languages:
- en: English
- fr: French (default for Luxembourg)
- de: German
- lb: Luxembourgish
"""

import json
import logging
from functools import lru_cache
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

# ============================================================================
# Language Configuration
# ============================================================================

# Supported language codes
SUPPORTED_LANGUAGES = ["en", "fr", "de", "lb"]

# Default language (Luxembourg context)
DEFAULT_LANGUAGE = "fr"

# Language display names (in their native language)
LANGUAGE_NAMES = {
    "en": "English",
    "fr": "Français",
    "de": "Deutsch",
    "lb": "Lëtzebuergesch",
}

# Language display names in English (for admin)
LANGUAGE_NAMES_EN = {
    "en": "English",
    "fr": "French",
    "de": "German",
    "lb": "Luxembourgish",
}

# Flag emoji for language selector (optional)
LANGUAGE_FLAGS = {
    "en": "🇬🇧",
    "fr": "🇫🇷",
    "de": "🇩🇪",
    "lb": "🇱🇺",
}

# ============================================================================
# Translation Storage
# ============================================================================

# Path to core locale files
LOCALES_PATH = Path(__file__).parent.parent.parent / "static" / "locales"

# Path to modules directory (for module-specific locales)
MODULES_PATH = Path(__file__).parent.parent / "modules"

# In-memory cache for loaded translations
_translations: dict[str, dict] = {}

# Cache for module locale directories
_module_locale_dirs: list[tuple[str, Path]] | None = None


def get_locales_path() -> Path:
    """Get the path to core locale files."""
    return LOCALES_PATH


def get_module_locale_dirs() -> list[tuple[str, Path]]:
    """
    Discover module locale directories.

    Returns:
        List of (module_code, locales_path) tuples for modules with locales
    """
    global _module_locale_dirs

    if _module_locale_dirs is not None:
        return _module_locale_dirs

    _module_locale_dirs = []

    if not MODULES_PATH.exists():
        return _module_locale_dirs

    for module_dir in sorted(MODULES_PATH.iterdir()):
        if module_dir.is_dir():
            locales_dir = module_dir / "locales"
            if locales_dir.exists() and locales_dir.is_dir():
                module_code = module_dir.name
                _module_locale_dirs.append((module_code, locales_dir))
                logger.debug(f"[i18n] Found module locales: {module_code} at {locales_dir}")

    return _module_locale_dirs


def _load_json_file(file_path: Path) -> dict:
    """Load a JSON file and return its contents."""
    try:
        with open(file_path, encoding="utf-8") as f:
            return json.load(f)
    except json.JSONDecodeError as e:
        logger.error(f"Invalid JSON in translation file {file_path}: {e}")
        return {}
    except Exception as e:
        logger.error(f"Error loading translation file {file_path}: {e}")
        return {}


def _deep_merge(base: dict, override: dict) -> dict:
    """
    Deep merge two dictionaries. Override values take precedence.

    Args:
        base: Base dictionary
        override: Dictionary with values to merge/override

    Returns:
        Merged dictionary
    """
    result = base.copy()

    for key, value in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = _deep_merge(result[key], value)
        else:
            result[key] = value

    return result


@lru_cache(maxsize=10)
def load_translations(language: str) -> dict:
    """
    Load translations for a specific language from JSON files.

    Loads core translations first, then merges module-specific translations.
    Module translations are namespaced under their module code.

    Args:
        language: Language code (en, fr, de, lb)

    Returns:
        Dictionary of translations, empty dict if file not found

    Example:
        Core: {"common": {"save": "Save"}}
        CMS module: {"pages": {"title": "Page Title"}}
        Result: {"common": {"save": "Save"}, "cms": {"pages": {"title": "Page Title"}}}
    """
    if language not in SUPPORTED_LANGUAGES:
        logger.warning(f"Unsupported language requested: {language}")
        language = DEFAULT_LANGUAGE

    # Load core translations
    core_file = LOCALES_PATH / f"{language}.json"
    if core_file.exists():
        translations = _load_json_file(core_file)
    else:
        logger.warning(f"Core translation file not found: {core_file}")
        if language != DEFAULT_LANGUAGE:
            return load_translations(DEFAULT_LANGUAGE)
        translations = {}

    # Load and merge module translations
    for module_code, locales_dir in get_module_locale_dirs():
        module_file = locales_dir / f"{language}.json"
        if module_file.exists():
            module_translations = _load_json_file(module_file)
            if module_translations:
                # Namespace module translations under module code
                if module_code in translations:
                    # Merge with existing (e.g., if core has some cms.* keys)
                    translations[module_code] = _deep_merge(
                        translations[module_code], module_translations
                    )
                else:
                    translations[module_code] = module_translations
                logger.debug(f"[i18n] Loaded {language} translations for module: {module_code}")

    return translations


def clear_translation_cache():
    """Clear the translation cache (useful for development/testing)."""
    global _module_locale_dirs
    load_translations.cache_clear()
    _translations.clear()
    _module_locale_dirs = None


# ============================================================================
# Translation Functions
# ============================================================================


def get_nested_value(data: dict, key_path: str, default: str = None) -> str:
    """
    Get a nested value from a dictionary using dot notation.

    Args:
        data: Dictionary to search
        key_path: Dot-separated path (e.g., "common.save")
        default: Default value if key not found

    Returns:
        Value at the key path, or default if not found
    """
    keys = key_path.split(".")
    value = data

    for key in keys:
        if isinstance(value, dict) and key in value:
            value = value[key]
        else:
            return default if default is not None else key_path

    return (
        value
        if isinstance(value, str)
        else (default if default is not None else key_path)
    )


def translate(
    key: str,
    language: str = None,
    default: str = None,
    **kwargs: Any,
) -> str:
    """
    Translate a key to the specified language.

    Args:
        key: Translation key (dot notation, e.g., "common.save")
        language: Target language code (defaults to DEFAULT_LANGUAGE)
        default: Default value if translation not found
        **kwargs: Variables for string interpolation

    Returns:
        Translated string with variables interpolated

    Example:
        translate("common.welcome", language="fr", name="John")
        # Returns: "Bienvenue, John!" (if translation is "Bienvenue, {name}!")
    """
    if language is None:
        language = DEFAULT_LANGUAGE

    translations = load_translations(language)
    text = get_nested_value(translations, key, default)

    # If translation not found and we're not already in default language, try default
    if text == key and language != DEFAULT_LANGUAGE:
        translations = load_translations(DEFAULT_LANGUAGE)
        text = get_nested_value(translations, key, default)

    # Interpolate variables
    if kwargs and isinstance(text, str):
        try:
            text = text.format(**kwargs)
        except KeyError as e:
            logger.warning(
                f"Missing interpolation variable in translation '{key}': {e}"
            )

    return text


def t(key: str, language: str = None, **kwargs: Any) -> str:
    """
    Shorthand alias for translate().

    Example:
        t("common.save", "fr")  # Returns French translation
    """
    return translate(key, language, **kwargs)


# ============================================================================
# Jinja2 Integration
# ============================================================================


class TranslationContext:
    """
    Translation context for use in Jinja2 templates.

    Provides a callable interface for the _() function in templates.
    """

    def __init__(self, language: str = None):
        """Initialize with a specific language."""
        self.language = language or DEFAULT_LANGUAGE

    def __call__(self, key: str, **kwargs: Any) -> str:
        """Translate a key."""
        return translate(key, self.language, **kwargs)

    def set_language(self, language: str):
        """Change the current language."""
        if language in SUPPORTED_LANGUAGES:
            self.language = language
        else:
            logger.warning(f"Attempted to set unsupported language: {language}")


def create_translation_context(language: str = None) -> TranslationContext:
    """Create a translation context for templates."""
    return TranslationContext(language)


def get_jinja2_globals(language: str = None) -> dict:
    """
    Get globals to add to Jinja2 environment for i18n support.

    Returns:
        Dictionary of globals for Jinja2 templates
    """
    ctx = create_translation_context(language)

    return {
        "_": ctx,  # Main translation function: {{ _("common.save") }}
        "t": ctx,  # Alias: {{ t("common.save") }}
        "SUPPORTED_LANGUAGES": SUPPORTED_LANGUAGES,
        "DEFAULT_LANGUAGE": DEFAULT_LANGUAGE,
        "LANGUAGE_NAMES": LANGUAGE_NAMES,
        "LANGUAGE_FLAGS": LANGUAGE_FLAGS,
        "current_language": language or DEFAULT_LANGUAGE,
    }


# ============================================================================
# Language Resolution Helpers
# ============================================================================


def resolve_store_dashboard_language(
    user_preferred: str | None,
    store_dashboard: str | None,
    cookie_language: str | None = None,
) -> str:
    """
    Resolve language for store dashboard.

    Priority:
    1. Cookie (explicit UI action via language switcher)
    2. User's preferred_language (DB preference from profile settings)
    3. Store's dashboard_language
    4. System default (fr)
    """
    if cookie_language and cookie_language in SUPPORTED_LANGUAGES:
        return cookie_language

    if user_preferred and user_preferred in SUPPORTED_LANGUAGES:
        return user_preferred

    if store_dashboard and store_dashboard in SUPPORTED_LANGUAGES:
        return store_dashboard

    return DEFAULT_LANGUAGE


def resolve_storefront_language(
    customer_preferred: str | None,
    session_language: str | None,
    store_storefront: str | None,
    browser_language: str | None,
    enabled_languages: list[str] | None = None,
) -> str:
    """
    Resolve language for storefront.

    Priority:
    1. Customer's preferred_language (if logged in and set)
    2. Session/cookie language
    3. Store's storefront_language
    4. Browser Accept-Language header
    5. System default (fr)

    Args:
        customer_preferred: Customer's saved preference
        session_language: Language from session/cookie
        store_storefront: Store's default storefront language
        browser_language: Primary language from Accept-Language header
        enabled_languages: List of languages enabled for this storefront
    """
    # Explicit user choices (customer preference, session/cookie) should be
    # respected if they are valid platform languages, regardless of
    # store's enabled_languages. The store setting only constrains automatic
    # fallback (browser language, store default).
    explicit_choices = [customer_preferred, session_language]
    for lang in explicit_choices:
        if lang and lang in SUPPORTED_LANGUAGES:
            return lang

    # For automatic fallback, filter by store's enabled languages
    fallback_candidates = [store_storefront, browser_language, DEFAULT_LANGUAGE]
    if enabled_languages:
        enabled_set = set(enabled_languages)
        for lang in fallback_candidates:
            if lang and lang in SUPPORTED_LANGUAGES and lang in enabled_set:
                return lang
    else:
        for lang in fallback_candidates:
            if lang and lang in SUPPORTED_LANGUAGES:
                return lang

    return DEFAULT_LANGUAGE


def parse_accept_language(accept_language: str | None) -> str | None:
    """
    Parse Accept-Language header and return the best supported language.

    Args:
        accept_language: Accept-Language header value

    Returns:
        Best matching supported language code, or None
    """
    if not accept_language:
        return None

    # Parse header: "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"
    languages = []
    for part in accept_language.split(","):
        part = part.strip()
        if ";q=" in part:
            lang, q = part.split(";q=")
            try:
                quality = float(q)
            except ValueError:
                quality = 0.0
        else:
            lang = part
            quality = 1.0

        # Extract base language code (e.g., "fr-FR" -> "fr")
        lang_code = lang.split("-")[0].lower()
        languages.append((lang_code, quality))

    # Sort by quality (highest first)
    languages.sort(key=lambda x: x[1], reverse=True)

    # Return first supported language
    for lang_code, _ in languages:
        if lang_code in SUPPORTED_LANGUAGES:
            return lang_code

    return None


# ============================================================================
# Utility Functions
# ============================================================================


def get_language_choices() -> list[tuple[str, str]]:
    """
    Get language choices for form select fields.

    Returns:
        List of (code, name) tuples
    """
    return [(code, LANGUAGE_NAMES[code]) for code in SUPPORTED_LANGUAGES]


def get_language_info(language: str) -> dict:
    """
    Get full information about a language.

    Args:
        language: Language code

    Returns:
        Dictionary with code, name, name_en, flag
    """
    if language not in SUPPORTED_LANGUAGES:
        language = DEFAULT_LANGUAGE

    return {
        "code": language,
        "name": LANGUAGE_NAMES.get(language, language),
        "name_en": LANGUAGE_NAMES_EN.get(language, language),
        "flag": LANGUAGE_FLAGS.get(language, "🌐"),
    }


def is_rtl_language(language: str) -> bool:
    """
    Check if a language is right-to-left.

    Currently all supported languages are LTR.
    """
    # RTL languages (not currently supported, but ready for future)
    rtl_languages = ["ar", "he", "fa"]
    return language in rtl_languages
