//! "A tiny Rust i18n library that loads JSON language maps (modeled after vue-i18n)
//! Loads languages from simple JSON key:value maps, files.
//! Supports singular, plural forms and variables in language strings using the `{param}` syntax.
//!
//! # Example
//!
//! ```
//! use tinyi18n_rs::I18n;
//!
//! let json = r#"{
//!     "_.code": "en",
//!     "_.name": "English",
//!     "greeting": "Hello, {name}!",
//!     "items": "One item|Many items"
//! }"#;
//!
//! let i18n = I18n::new(json, None, None).unwrap();
//!
//! assert_eq!(i18n.t("greeting"), "Hello, {name}!");
//! assert_eq!(i18n.ts("greeting", &[("name", "World".into())]), "Hello, World!");
//! assert_eq!(i18n.s("items"), "One item");
//! assert_eq!(i18n.p("items"), "Many items");
//! assert_eq!(i18n.tc("items", 1), "One item");
//! assert_eq!(i18n.tc("items", 5), "Many items");
//! ```

use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::LazyLock;

use regex::Regex;

/// Regex for matching `{param}` placeholders.
static RE_PARAM: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)\{([a-z0-9\-.]+)\}").unwrap());

/// Error type for I18n operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
    /// JSON parsing failed.
    Json(String),
    /// File read error.
    FileRead(String),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Json(e) => write!(f, "JSON parseerror: {}", e),
            Error::FileRead(e) => write!(f, "file read error: {}", e),
        }
    }
}

impl std::error::Error for Error {}

/// I18n enables simple translation functions over a language map.
#[derive(Debug, Clone)]
pub struct I18n {
    code: String,
    name: String,
    lang_map: HashMap<String, String>,
}

impl I18n {
    /// Creates a new I18n instance from JSON.
    ///
    /// If `code` or `name` are provided, they take precedence over the JSON values.
    /// values read from the special (optional) `_.code` and `_.name` keys in JSON.
    pub fn new(json: &str, code: Option<&str>, name: Option<&str>) -> Result<Self, Error> {
        let lang_map: HashMap<String, String> =
            serde_json::from_str(json).map_err(|e| Error::Json(e.to_string()))?;

        let code = code
            .map(String::from)
            .or_else(|| lang_map.get("_.code").cloned())
            .unwrap_or_default();
        let name = name
            .map(String::from)
            .or_else(|| lang_map.get("_.name").cloned())
            .unwrap_or_default();

        Ok(Self {
            code,
            name,
            lang_map,
        })
    }

    /// Creates a new I18n instance from a JSON file.
    ///
    /// If `code` or `name` are provided, they take precedence over the JSON values.
    /// values read from the special (optional) `_.code` and `_.name` keys in JSON.
    pub fn from_file<P: AsRef<Path>>(
        path: P,
        code: Option<&str>,
        name: Option<&str>,
    ) -> Result<Self, Error> {
        let b = fs::read_to_string(path).map_err(|e| Error::FileRead(e.to_string()))?;
        Self::new(&b, code, name)
    }

    /// Loads additional translations from JSON and merge with existing keys.
    pub fn load(&mut self, json: &str) -> Result<(), Error> {
        let new_map: HashMap<String, String> =
            serde_json::from_str(json).map_err(|e| Error::Json(e.to_string()))?;

        for (k, v) in new_map {
            self.lang_map.insert(k, v);
        }

        Ok(())
    }

    /// Returns the ISO language code.
    pub fn code(&self) -> &str {
        &self.code
    }

    /// Returns the canonical language name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the language map as JSON bytes.
    pub fn json(&self) -> Result<String, Error> {
        serde_json::to_string(&self.lang_map).map_err(|e| Error::Json(e.to_string()))
    }

    /// Returns the translation for a key, singular form if pipe-separated.
    ///
    /// If not found in the map, returns the key itself.
    pub fn t(&self, key: &str) -> String {
        self.lang_map
            .get(key)
            .map_or_else(|| key.to_string(), |s| self.get_singular(s))
    }

    /// Returns the translation with param substitution.
    ///
    /// Parameters are provided as `(key, value)` pairs. Values are `String` to support
    /// any type via `.to_string()`. Params use `{key}` syntax in the language string.
    pub fn ts(&self, key: &str, params: &[(&str, String)]) -> String {
        let Some(s) = self.lang_map.get(key) else {
            return key.to_string();
        };

        let mut out = self.get_singular(s);
        for (param_key, value) in params {
            let val = self.sub_all_params(value);
            out = out.replace(&format!("{{{}}}", param_key), &val);
        }

        out
    }

    /// Returns singular or plural form of a value based on count.
    ///
    /// Returns plural if `count > 1`, singular otherwise.
    pub fn tc(&self, key: &str, count: i64) -> String {
        let Some(s) = self.lang_map.get(key) else {
            return key.to_string();
        };
        if count > 1 {
            self.get_plural(s)
        } else {
            self.get_singular(s)
        }
    }

    /// Returns the singular form of a value (handy shortcut for `tc(key, 1)`).
    pub fn s(&self, key: &str) -> String {
        self.tc(key, 1)
    }

    /// Returns the plural form of a value (handy shortcut for `tc(key, 2)`).
    pub fn p(&self, key: &str) -> String {
        self.tc(key, 2)
    }

    /// Extracts the singular term from a pipe-separated string.
    fn get_singular(&self, s: &str) -> String {
        s.split('|')
            .next()
            .map_or_else(|| s.to_string(), |part| part.trim().to_string())
    }

    /// Extracts the plural term from a pipe-separated string.
    fn get_plural(&self, s: &str) -> String {
        let mut parts = s.split('|');
        let first = parts.next().unwrap_or(s);

        parts
            .next()
            .map_or_else(|| first.trim().to_string(), |part| part.trim().to_string())
    }

    /// Recursively resolves nested `{param}` instances in a value.
    fn sub_all_params(&self, s: &str) -> String {
        if !s.contains('{') {
            return s.to_string();
        }

        let res = RE_PARAM.replace_all(s, |caps: &regex::Captures| {
            let param_key = caps.get(1).unwrap().as_str();
            self.t(param_key)
        });

        if res == s {
            res.into_owned()
        } else {
            self.sub_all_params(&res)
        }
    }
}

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

    const TEST_JSON: &str = r#"{
        "_.code": "en",
        "_.name": "English",
        "pageTitle": "Welcome to the page",
        "foo": "Foo",
        "page": "Single page|Many pages",
        "pageVars": "The page is named {name} and has {count} items",
        "nested": "This is {nested}",
        "priceMsg": "The price is ${price} with {discount}% discount",
        "statusMsg": "The system is {status} and auto-save is {autosave}",
        "mixedMsg": "User {user} has {count} items worth ${total} (active: {active})",
        "app.name": "MyApp",
        "nestedParams": "Welcome to {appName}"
    }"#;

    #[test]
    fn test_basic() {
        let i = I18n::new(TEST_JSON, None, None).unwrap();

        assert_eq!(i.code(), "en");
        assert_eq!(i.name(), "English");
        assert_eq!(i.t("pageTitle"), "Welcome to the page");
    }

    #[test]
    fn test_singular_plural() {
        let i = I18n::new(TEST_JSON, None, None).unwrap();

        assert_eq!(i.t("page"), "Single page");
        assert_eq!(i.s("page"), "Single page");
        assert_eq!(i.p("page"), "Many pages");
        assert_eq!(i.tc("page", 0), "Single page");
        assert_eq!(i.tc("page", 1), "Single page");
        assert_eq!(i.tc("page", 2), "Many pages");

        // No pipe separator returns same value for both.
        assert_eq!(i.s("foo"), "Foo");
        assert_eq!(i.p("foo"), "Foo");
    }

    #[test]
    fn test_missing_key() {
        let i = I18n::new(TEST_JSON, None, None).unwrap();
        assert_eq!(i.t("nonexistent"), "nonexistent");
        assert_eq!(i.tc("nonexistent", 1), "nonexistent");
    }

    #[test]
    fn test_params() {
        let i = I18n::new(TEST_JSON, None, None).unwrap();

        // No params leaves placeholders as-is.
        assert_eq!(
            i.t("pageVars"),
            "The page is named {name} and has {count} items"
        );

        // Partial params where some are left.
        assert_eq!(
            i.ts("pageVars", &[("name", "Foo".into())]),
            "The page is named Foo and has {count} items"
        );

        // All params strings set.
        assert_eq!(
            i.ts(
                "pageVars",
                &[("name", "Foo".into()), ("count", "1234".into())]
            ),
            "The page is named Foo and has 1234 items"
        );
    }

    #[test]
    fn test_typed_params() {
        let i = I18n::new(TEST_JSON, None, None).unwrap();

        assert_eq!(
            i.ts(
                "pageVars",
                &[("name", "Product".into()), ("count", 42.to_string())]
            ),
            "The page is named Product and has 42 items"
        );

        assert_eq!(
            i.ts(
                "priceMsg",
                &[("price", 19.99.to_string()), ("discount", 15.5.to_string())]
            ),
            "The price is $19.99 with 15.5% discount"
        );

        assert_eq!(
            i.ts(
                "statusMsg",
                &[("status", "online".into()), ("autosave", true.to_string())]
            ),
            "The system is online and auto-save is true"
        );

        assert_eq!(
            i.ts(
                "mixedMsg",
                &[
                    ("user", "John".into()),
                    ("count", 5.to_string()),
                    ("total", 99.99.to_string()),
                    ("active", true.to_string())
                ]
            ),
            "User John has 5 items worth $99.99 (active: true)"
        );
    }

    #[test]
    fn test_nested_translations() {
        let i = I18n::new(TEST_JSON, None, None).unwrap();

        assert_eq!(
            i.ts("nestedParams", &[("appName", "{app.name}".into())]),
            "Welcome to MyApp"
        );
    }

    #[test]
    fn test_load() {
        let mut i = I18n::new(TEST_JSON, None, None).unwrap();

        i.load(r#"{"newKey": "new value", "pageTitle": "Updated title"}"#)
            .unwrap();

        assert_eq!(i.t("newKey"), "new value");
        assert_eq!(i.t("pageTitle"), "Updated title");
    }

    #[test]
    fn test_optional_code_name() {
        // No code/name in JSON, defaults to empty.
        let minimal = r#"{"hello": "world"}"#;
        let i = I18n::new(minimal, None, None).unwrap();
        assert_eq!(i.code(), "");
        assert_eq!(i.name(), "");
        assert_eq!(i.t("hello"), "world");

        // Code/name from JSON.
        let with_meta = r#"{"_.code": "fr", "_.name": "French"}"#;
        let i = I18n::new(with_meta, None, None).unwrap();
        assert_eq!(i.code(), "fr");
        assert_eq!(i.name(), "French");

        // Explicit code/name override of JSON.
        let i = I18n::new(with_meta, Some("de"), Some("German")).unwrap();
        assert_eq!(i.code(), "de");
        assert_eq!(i.name(), "German");
    }

    #[test]
    fn test_invalid_json() {
        let invalid = "not json";
        assert!(matches!(
            I18n::new(invalid, None, None),
            Err(Error::Json(_))
        ));
    }

    #[test]
    fn test_json_output() {
        let i = I18n::new(TEST_JSON, None, None).unwrap();
        let json = i.json().unwrap();

        assert!(json.contains("_.code"));
        assert!(json.contains("English"));
    }
}
