use fluent_bundle::concurrent::FluentBundle;
use fluent_bundle::{FluentArgs, FluentResource};
use std::collections::HashMap;
use std::sync::LazyLock;

/// Supported UI locales, matching the i18n.allium SupportedLanguage spec.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(usize)]
pub enum UiLocale {
    En = 0,
    De = 1,
    Fr = 2,
    It = 3,
    Es = 4,
}

impl UiLocale {
    /// BCP-47 base code for this locale.
    pub fn code(self) -> &'static str {
        match self {
            Self::En => "en",
            Self::De => "de",
            Self::Fr => "fr",
            Self::It => "it",
            Self::Es => "es",
        }
    }

    /// All supported locales.
    pub fn all() -> &'static [UiLocale] {
        &[Self::En, Self::De, Self::Fr, Self::It, Self::Es]
    }

    /// Unicode regional-indicator flag emoji for this locale.
    pub fn flag_emoji(self) -> &'static str {
        match self {
            Self::En => "\u{1F1EC}\u{1F1E7}", // 🇬🇧
            Self::De => "\u{1F1E9}\u{1F1EA}", // 🇩🇪
            Self::Fr => "\u{1F1EB}\u{1F1F7}", // 🇫🇷
            Self::It => "\u{1F1EE}\u{1F1F9}", // 🇮🇹
            Self::Es => "\u{1F1EA}\u{1F1F8}", // 🇪🇸
        }
    }
}

impl std::fmt::Display for UiLocale {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.code())
    }
}

/// Normalize a language code to a supported UiLocale.
///
/// Strips region suffix ("en-US" → "en"), lowercases, and falls back to English
/// for unrecognized codes. This implements the LanguageNormalization invariant
/// from i18n.allium.
pub fn normalize_language(code: &str) -> UiLocale {
    let base = code.split(['-', '_']).next().unwrap_or("en").to_lowercase();
    match base.as_str() {
        "en" => UiLocale::En,
        "de" => UiLocale::De,
        "fr" => UiLocale::Fr,
        "it" => UiLocale::It,
        "es" => UiLocale::Es,
        _ => UiLocale::En,
    }
}

/// Detect the OS locale and return the closest supported UiLocale.
///
/// Uses sys-locale to query the system. Falls back to English if detection
/// fails or the detected locale is not supported.
pub fn detect_os_locale() -> UiLocale {
    match sys_locale::get_locale() {
        Some(tag) => normalize_language(&tag),
        None => UiLocale::En,
    }
}

type Bundle = FluentBundle<FluentResource>;

const UI_EN: &str = include_str!("../../../../locales/ui/en.ftl");
const RENDER_EN: &str = include_str!("../../../../locales/render/en.ftl");

fn resource(source: &str) -> FluentResource {
    FluentResource::try_new(source.to_owned())
        .unwrap_or_else(|(_, errors)| panic!("invalid Fluent catalog: {errors:?}"))
}

fn bundle(locale: UiLocale, english: &str, localized: &str) -> Bundle {
    let mut bundle = FluentBundle::new_concurrent(vec![
        locale.code().parse().expect("supported locale is valid"),
    ]);
    bundle.set_use_isolating(false);
    bundle
        .add_resource(resource(english))
        .expect("English Fluent catalog has unique keys");
    if locale != UiLocale::En {
        bundle.add_resource_overriding(resource(localized));
    }
    bundle
}

static UI_CATALOGS: LazyLock<[Bundle; 5]> = LazyLock::new(|| {
    [
        bundle(UiLocale::En, UI_EN, UI_EN),
        bundle(
            UiLocale::De,
            UI_EN,
            include_str!("../../../../locales/ui/de.ftl"),
        ),
        bundle(
            UiLocale::Fr,
            UI_EN,
            include_str!("../../../../locales/ui/fr.ftl"),
        ),
        bundle(
            UiLocale::It,
            UI_EN,
            include_str!("../../../../locales/ui/it.ftl"),
        ),
        bundle(
            UiLocale::Es,
            UI_EN,
            include_str!("../../../../locales/ui/es.ftl"),
        ),
    ]
});

static RENDER_CATALOGS: LazyLock<[Bundle; 5]> = LazyLock::new(|| {
    [
        bundle(UiLocale::En, RENDER_EN, RENDER_EN),
        bundle(
            UiLocale::De,
            RENDER_EN,
            include_str!("../../../../locales/render/de.ftl"),
        ),
        bundle(
            UiLocale::Fr,
            RENDER_EN,
            include_str!("../../../../locales/render/fr.ftl"),
        ),
        bundle(
            UiLocale::It,
            RENDER_EN,
            include_str!("../../../../locales/render/it.ftl"),
        ),
        bundle(
            UiLocale::Es,
            RENDER_EN,
            include_str!("../../../../locales/render/es.ftl"),
        ),
    ]
});

fn locale_index(locale: UiLocale) -> usize {
    locale as usize
}

fn format(bundle: &Bundle, key: &str, params: &[(&str, &str)]) -> Option<String> {
    let id = key.replace('.', "-");
    let pattern = bundle.get_message(&id)?.value()?;
    let mut args = FluentArgs::new();
    for (name, value) in params {
        args.set(*name, *value);
    }
    let mut errors = Vec::new();
    let value = bundle
        .format_pattern(pattern, Some(&args), &mut errors)
        .into_owned();
    Some(value)
}

fn ui_catalog_for(locale: UiLocale) -> &'static Bundle {
    &UI_CATALOGS[locale_index(locale)]
}

fn render_catalog_for(language: &str) -> &'static Bundle {
    &RENDER_CATALOGS[locale_index(normalize_language(language))]
}

/// Look up a translation key for the given locale.
///
/// Fallback chain: requested locale → English → key itself.
/// This implements the MenuTranslations invariant from i18n.allium.
pub fn translate(locale: UiLocale, key: &str) -> String {
    format(ui_catalog_for(locale), key, &[]).unwrap_or_else(|| key.to_owned())
}

/// Look up a translation key and substitute `{param}` placeholders.
///
/// Parameters are provided as `&[("param_name", "value")]`.
pub fn translate_with(locale: UiLocale, key: &str, params: &[(&str, &str)]) -> String {
    format(ui_catalog_for(locale), key, params).unwrap_or_else(|| key.to_owned())
}

/// Look up a render/template translation key by content language code.
///
/// This is independent of the UI locale — it uses the project's content language.
/// Fallback chain: requested language → English → key itself.
/// Implements the RenderTranslations invariant from i18n.allium.
pub fn translate_render(language: &str, key: &str) -> String {
    format(render_catalog_for(language), key, &[]).unwrap_or_else(|| key.to_owned())
}

/// Return the entire render translation map for a language.
///
/// Used to inject as `translations` into the Liquid template context.
pub fn get_render_translations(language: &str) -> &'static HashMap<String, String> {
    &RENDER_MAPS[locale_index(normalize_language(language))]
}

static RENDER_KEYS: LazyLock<Vec<String>> = LazyLock::new(|| {
    resource(RENDER_EN)
        .entries()
        .filter_map(|entry| match entry {
            fluent_syntax::ast::Entry::Message(message) => Some(message.id.name.replace('-', ".")),
            _ => None,
        })
        .collect()
});

static RENDER_MAPS: LazyLock<[HashMap<String, String>; 5]> = LazyLock::new(|| {
    std::array::from_fn(|index| {
        let locale = UiLocale::all()[index];
        RENDER_KEYS
            .iter()
            .map(|key| (key.clone(), translate_render(locale.code(), key)))
            .collect()
    })
});

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

    #[test]
    fn engine_progress_keys_exist_in_every_ui_catalog() {
        let keys = [
            "engine-progress-scanningPublishedPosts",
            "engine-progress-translatingPost",
            "engine-progress-translationBatchComplete",
            "engine-progress-loadingProjectMetadata",
            "engine-progress-scanningPosts",
            "engine-progress-postItem",
            "engine-progress-scanningMedia",
            "engine-progress-mediaItem",
            "engine-progress-rebuildingTemplates",
            "engine-progress-rebuildingScripts",
            "engine-progress-importingTags",
            "engine-progress-refreshingSemanticIndex",
            "engine-progress-rebuildComplete",
        ];
        let catalogs = [
            ("en", UI_EN),
            ("de", include_str!("../../../../locales/ui/de.ftl")),
            ("fr", include_str!("../../../../locales/ui/fr.ftl")),
            ("it", include_str!("../../../../locales/ui/it.ftl")),
            ("es", include_str!("../../../../locales/ui/es.ftl")),
        ];

        for (language, source) in catalogs {
            let present = resource(source)
                .entries()
                .filter_map(|entry| match entry {
                    fluent_syntax::ast::Entry::Message(message) => {
                        Some(message.id.name.to_string())
                    }
                    _ => None,
                })
                .collect::<std::collections::HashSet<_>>();
            for key in keys {
                assert!(present.contains(key), "{language} is missing {key}");
            }
        }
    }

    // LanguageNormalization invariant: base-extract and fallback
    #[test]
    fn normalize_strips_region() {
        assert_eq!(normalize_language("en-US"), UiLocale::En);
        assert_eq!(normalize_language("de-AT"), UiLocale::De);
        assert_eq!(normalize_language("fr-CA"), UiLocale::Fr);
        assert_eq!(normalize_language("it-CH"), UiLocale::It);
        assert_eq!(normalize_language("es-MX"), UiLocale::Es);
    }

    #[test]
    fn normalize_underscore_separator() {
        assert_eq!(normalize_language("de_DE"), UiLocale::De);
    }

    #[test]
    fn normalize_unknown_falls_back_to_en() {
        assert_eq!(normalize_language("xx"), UiLocale::En);
        assert_eq!(normalize_language("ja"), UiLocale::En);
        assert_eq!(normalize_language(""), UiLocale::En);
    }

    #[test]
    fn normalize_case_insensitive() {
        assert_eq!(normalize_language("DE"), UiLocale::De);
        assert_eq!(normalize_language("FR-fr"), UiLocale::Fr);
    }

    // MenuTranslations invariant: menu labels come from locale catalog
    #[test]
    fn translate_menu_labels() {
        let label = translate(UiLocale::De, "menu.group.file");
        assert_eq!(label, "Datei");

        let label = translate(UiLocale::Fr, "menu.item.save");
        assert_eq!(label, "Enregistrer");
    }

    #[test]
    fn translate_missing_key_returns_key() {
        let result = translate(UiLocale::En, "nonexistent.key.xyz");
        assert_eq!(result, "nonexistent.key.xyz");
    }

    #[test]
    fn translate_with_interpolation() {
        let result = translate_with(
            UiLocale::En,
            "tasks.triggerTitle",
            &[("running", "2"), ("pending", "3")],
        );
        assert_eq!(result, "2 running, 3 pending");
    }

    #[test]
    fn translate_with_interpolation_german() {
        let result = translate_with(
            UiLocale::De,
            "tasks.triggerTitle",
            &[("running", "1"), ("pending", "5")],
        );
        assert_eq!(result, "1 laufend, 5 ausstehend");
    }

    #[test]
    fn all_locales_have_menu_keys() {
        let key = "menu.group.file";
        for locale in UiLocale::all() {
            let val = translate(*locale, key);
            assert_ne!(val, key, "missing {key} for locale {locale}");
        }
    }

    #[test]
    fn locale_code_roundtrip() {
        for locale in UiLocale::all() {
            assert_eq!(normalize_language(locale.code()), *locale);
        }
    }

    #[test]
    fn detect_os_locale_does_not_panic() {
        let _ = detect_os_locale();
    }

    // RenderTranslations invariant: separate catalog for content/template locale
    #[test]
    fn translate_render_english() {
        assert_eq!(translate_render("en", "render.archive"), "Archive");
        assert_eq!(translate_render("en", "render.month.1"), "January");
    }

    #[test]
    fn translate_render_german() {
        assert_eq!(translate_render("de", "render.archive"), "Archiv");
        assert_eq!(translate_render("de", "render.month.1"), "Januar");
    }

    #[test]
    fn translate_render_falls_back_to_english() {
        assert_eq!(translate_render("ja", "render.archive"), "Archive");
    }

    #[test]
    fn translate_render_missing_key_returns_key() {
        assert_eq!(
            translate_render("en", "render.nonexistent"),
            "render.nonexistent"
        );
    }

    #[test]
    fn get_render_translations_not_empty() {
        let map = get_render_translations("en");
        assert!(map.contains_key("render.archive"));
        assert!(map.contains_key("render.month.12"));
        assert!(map.len() >= 34);
    }

    #[test]
    fn all_render_locales_have_archive_key() {
        for code in &["en", "de", "fr", "it", "es"] {
            let val = translate_render(code, "render.archive");
            assert_ne!(val, "render.archive", "missing render.archive for {code}");
        }
    }
}
