use std::collections::HashMap;
use std::sync::LazyLock;

use serde_json::Value;

/// Supported application locales.
///
/// Each variant maps to an ISO 639-1 code and a human-readable label
/// displayed in the language picker.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Locale {
    #[default]
    En,
    De,
    Fr,
    Es,
    Pt,
}

impl Locale {
    /// ISO 639-1 language code.
    pub fn code(self) -> &'static str {
        match self {
            Locale::En => "en",
            Locale::De => "de",
            Locale::Fr => "fr",
            Locale::Es => "es",
            Locale::Pt => "pt",
        }
    }

    /// Human-readable label in the locale's own language.
    pub fn label(self) -> &'static str {
        match self {
            Locale::En => "English",
            Locale::De => "Deutsch",
            Locale::Fr => "Francais",
            Locale::Es => "Espanol",
            Locale::Pt => "Portugues",
        }
    }

    /// All available locales.
    pub fn all() -> &'static [Locale] {
        &[Locale::En, Locale::De, Locale::Fr, Locale::Es, Locale::Pt]
    }

    /// Parse a locale from its ISO 639-1 code.
    ///
    /// Returns `Locale::En` for unrecognized codes.
    pub fn from_code(code: &str) -> Self {
        match code {
            "de" => Locale::De,
            "fr" => Locale::Fr,
            "es" => Locale::Es,
            "pt" => Locale::Pt,
            _ => Locale::En,
        }
    }
}

type TranslationMap = HashMap<String, String>;

/// All translations loaded at compile time and parsed lazily on first access.
///
/// Uses `LazyLock` (stable since Rust 1.80) to avoid runtime file I/O.
/// Each locale's JSON is embedded via `include_str!` and flattened into
/// dot-separated keys (e.g. `"nav.dashboard"` -> `"Dashboard"`).
static TRANSLATIONS: LazyLock<HashMap<&'static str, TranslationMap>> = LazyLock::new(|| {
    let mut map = HashMap::with_capacity(5);
    map.insert(
        "en",
        parse_translations(include_str!("../../assets/i18n/en.json")),
    );
    map.insert(
        "de",
        parse_translations(include_str!("../../assets/i18n/de.json")),
    );
    map.insert(
        "fr",
        parse_translations(include_str!("../../assets/i18n/fr.json")),
    );
    map.insert(
        "es",
        parse_translations(include_str!("../../assets/i18n/es.json")),
    );
    map.insert(
        "pt",
        parse_translations(include_str!("../../assets/i18n/pt.json")),
    );
    map
});

/// Parse a JSON string into a flat `key -> value` map.
///
/// Nested objects are flattened with dot separators:
/// `{ "nav": { "home": "Home" } }` becomes `"nav.home" -> "Home"`.
fn parse_translations(json: &str) -> TranslationMap {
    // SAFETY: translation JSON files are bundled at compile time and are
    // validated during development. A malformed file will panic here during
    // the first access, which surfaces immediately in testing.
    let value: Value = serde_json::from_str(json).unwrap_or(Value::Object(Default::default()));
    let mut map = TranslationMap::new();
    flatten_json("", &value, &mut map);
    map
}

/// Recursively flatten a JSON value into dot-separated keys.
fn flatten_json(prefix: &str, value: &Value, map: &mut TranslationMap) {
    match value {
        Value::Object(obj) => {
            for (key, val) in obj {
                let new_prefix = if prefix.is_empty() {
                    key.clone()
                } else {
                    format!("{prefix}.{key}")
                };
                flatten_json(&new_prefix, val, map);
            }
        }
        Value::String(s) => {
            map.insert(prefix.to_string(), s.clone());
        }
        // Non-string leaf values are skipped (numbers, bools, nulls)
        _ => {}
    }
}

/// Look up a translation for the given locale and key.
///
/// Falls back to English if the key is missing in the target locale.
/// Returns the raw key if not found in any locale (useful for debugging
/// missing translations).
///
/// # Arguments
///
/// * `locale` - The target locale
/// * `key` - Dot-separated translation key (e.g. `"nav.dashboard"`)
///
/// # Returns
///
/// The translated string, or the key itself as a fallback.
pub fn t(locale: Locale, key: &str) -> String {
    TRANSLATIONS
        .get(locale.code())
        .and_then(|map| map.get(key))
        .cloned()
        .unwrap_or_else(|| {
            // Fallback to English
            TRANSLATIONS
                .get("en")
                .and_then(|map| map.get(key))
                .cloned()
                .unwrap_or_else(|| key.to_string())
        })
}

/// Look up a translation and substitute variables.
///
/// Variables in the translation string use `{name}` syntax.
/// Each `(name, value)` pair in `vars` replaces `{name}` with `value`.
///
/// # Arguments
///
/// * `locale` - The target locale
/// * `key` - Dot-separated translation key
/// * `vars` - Slice of `(name, value)` pairs for substitution
///
/// # Returns
///
/// The translated string with all variables substituted.
///
/// # Examples
///
/// ```
/// use dashboard::i18n::{tw, Locale};
/// let text = tw(Locale::En, "common.up_to_seats", &[("n", "5")]);
/// assert_eq!(text, "Up to 5 seats");
/// ```
pub fn tw(locale: Locale, key: &str, vars: &[(&str, &str)]) -> String {
    let mut result = t(locale, key);
    for (name, value) in vars {
        result = result.replace(&format!("{{{name}}}"), value);
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn english_lookup() {
        let result = t(Locale::En, "nav.dashboard");
        assert_eq!(result, "Dashboard");
    }

    #[test]
    fn german_lookup() {
        let result = t(Locale::De, "nav.dashboard");
        assert_eq!(result, "Dashboard");
    }

    #[test]
    fn fallback_to_english() {
        // If a key exists in English but not in another locale, English is returned
        let en = t(Locale::En, "common.loading");
        let result = t(Locale::De, "common.loading");
        // German should have its own translation, but if missing, falls back to EN
        assert!(!result.is_empty());
        // Just verify it doesn't return the key itself
        assert_ne!(result, "common.loading");
        let _ = en; // suppress unused warning
    }

    #[test]
    fn missing_key_returns_key() {
        let result = t(Locale::En, "nonexistent.key");
        assert_eq!(result, "nonexistent.key");
    }

    #[test]
    fn variable_substitution() {
        let result = tw(Locale::En, "common.up_to_seats", &[("n", "5")]);
        assert_eq!(result, "Up to 5 seats");
    }

    #[test]
    fn locale_from_code() {
        assert_eq!(Locale::from_code("de"), Locale::De);
        assert_eq!(Locale::from_code("fr"), Locale::Fr);
        assert_eq!(Locale::from_code("unknown"), Locale::En);
    }

    #[test]
    fn all_locales_loaded() {
        for locale in Locale::all() {
            let result = t(*locale, "nav.dashboard");
            assert!(!result.is_empty());
        }
    }
}
