//! Unit conversion system for calcpad-engine.
//!
//! Provides a comprehensive unit registry with 200+ built-in units across 13 categories,
//! SI prefix decomposition, CSS/screen unit support with configurable PPI, case-sensitive
//! data unit handling (B vs b), and custom user-defined units.

pub mod categories;
pub mod css;
pub mod custom;
pub mod data;
pub mod registry;
pub mod si_prefix;

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

pub use categories::UnitCategory;
pub use css::CssConfig;
pub use custom::CustomUnitRegistry;

/// How to convert a unit to/from its category's base unit.
#[derive(Debug, Clone)]
pub enum Conversion {
    /// Linear: value_in_base = value * factor
    /// e.g., 1 km = 1000 m, so factor = 1000.0
    Linear(f64),

    /// Formula-based (non-linear): used for temperature etc.
    /// to_base(value) and from_base(value) are function pointers.
    Formula {
        to_base: fn(f64) -> f64,
        from_base: fn(f64) -> f64,
    },
}

/// A unit definition in the registry.
#[derive(Debug, Clone)]
pub struct UnitDef {
    /// Canonical name (e.g., "meter").
    pub name: &'static str,
    /// Short abbreviation for display (e.g., "m").
    pub abbreviation: &'static str,
    /// Category this unit belongs to.
    pub category: UnitCategory,
    /// How to convert to/from the base unit.
    pub conversion: Conversion,
}

impl UnitDef {
    /// Convert a value in this unit to the base unit.
    pub fn to_base(&self, value: f64) -> f64 {
        match &self.conversion {
            Conversion::Linear(factor) => value * factor,
            Conversion::Formula { to_base, .. } => to_base(value),
        }
    }

    /// Convert a value from the base unit to this unit.
    pub fn from_base(&self, value: f64) -> f64 {
        match &self.conversion {
            Conversion::Linear(factor) => value / factor,
            Conversion::Formula { from_base, .. } => from_base(value),
        }
    }
}

/// A resolved unit: a base unit definition combined with an SI prefix factor.
#[derive(Debug)]
pub struct ResolvedUnit<'a> {
    /// The base unit definition.
    pub unit: &'a UnitDef,
    /// The SI prefix multiplication factor (1.0 if no prefix).
    pub prefix_factor: f64,
}

impl<'a> ResolvedUnit<'a> {
    /// Convert a value in this (possibly prefixed) unit to the category's base unit.
    pub fn to_base(&self, value: f64) -> f64 {
        self.unit.to_base(value * self.prefix_factor)
    }

    /// Convert a value from the category's base unit to this (possibly prefixed) unit.
    pub fn from_base(&self, value: f64) -> f64 {
        self.unit.from_base(value) / self.prefix_factor
    }
}

/// The unit registry -- maps names/aliases to unit definitions.
pub struct UnitRegistry {
    /// Maps lowercase name/alias -> index into `units` vec.
    lookup: HashMap<String, usize>,
    /// Maps exact-case name/alias -> index for case-sensitive units (e.g., data: B vs b).
    case_sensitive_lookup: HashMap<String, usize>,
    /// All registered unit definitions.
    units: Vec<UnitDef>,
}

impl UnitRegistry {
    fn new() -> Self {
        let mut reg = UnitRegistry {
            lookup: HashMap::new(),
            case_sensitive_lookup: HashMap::new(),
            units: Vec::new(),
        };
        registry::register_all(&mut reg);
        reg
    }

    /// Register a unit with its aliases.
    pub(crate) fn register(&mut self, def: UnitDef, aliases: &[&str]) {
        let idx = self.units.len();
        // Register canonical name
        self.lookup.insert(def.name.to_lowercase(), idx);
        // Register abbreviation
        self.lookup.insert(def.abbreviation.to_lowercase(), idx);
        // Register additional aliases
        for alias in aliases {
            self.lookup.insert(alias.to_lowercase(), idx);
        }
        self.units.push(def);
    }

    /// Register a case-sensitive alias for a unit already in the registry.
    /// Used for data units where case distinguishes bytes (B) from bits (b).
    pub(crate) fn register_case_sensitive_alias(&mut self, alias: &str, canonical_name: &str) {
        if let Some(&idx) = self.lookup.get(&canonical_name.to_lowercase()) {
            self.case_sensitive_lookup.insert(alias.to_string(), idx);
        }
    }

    /// Look up a unit by any name or alias.
    /// Checks case-sensitive overrides first (for data units), then falls back to
    /// case-insensitive lookup.
    pub fn lookup(&self, name: &str) -> Option<&UnitDef> {
        // Case-sensitive lookup first (e.g., "Mb" -> megabit, "MB" -> megabyte)
        if let Some(&idx) = self.case_sensitive_lookup.get(name) {
            return Some(&self.units[idx]);
        }
        // Fall back to case-insensitive
        self.lookup
            .get(&name.to_lowercase())
            .map(|&idx| &self.units[idx])
    }

    /// Resolve a unit name, trying direct lookup first, then SI prefix decomposition.
    ///
    /// Returns a `ResolvedUnit` with the base unit definition and prefix factor.
    /// Pre-registered units always take priority over prefix decomposition.
    pub fn resolve_with_prefix(&self, name: &str) -> Option<ResolvedUnit<'_>> {
        // 1. Try direct lookup first (pre-registered units take priority)
        if let Some(unit) = self.lookup(name) {
            return Some(ResolvedUnit {
                unit,
                prefix_factor: 1.0,
            });
        }

        // 2. Try short-form SI prefix decomposition (case-sensitive on original input)
        if let Some((prefix, base_abbrev)) = si_prefix::match_short_prefix(name) {
            if let Some(unit) = self.lookup(base_abbrev) {
                return Some(ResolvedUnit {
                    unit,
                    prefix_factor: prefix.factor,
                });
            }
        }

        // 3. Try long-form SI prefix decomposition
        if let Some((prefix, base_name)) = si_prefix::match_long_prefix(name) {
            if let Some(unit) = self.lookup(base_name) {
                return Some(ResolvedUnit {
                    unit,
                    prefix_factor: prefix.factor,
                });
            }
        }

        None
    }

    /// Get all registered unit definitions.
    pub fn all_units(&self) -> &[UnitDef] {
        &self.units
    }

    /// Count total registered units.
    pub fn unit_count(&self) -> usize {
        self.units.len()
    }

    /// Get all supported categories.
    pub fn categories(&self) -> Vec<UnitCategory> {
        let mut cats: Vec<UnitCategory> = Vec::new();
        for unit in &self.units {
            if !cats.contains(&unit.category) {
                cats.push(unit.category);
            }
        }
        cats
    }
}

/// Global static registry instance.
static REGISTRY: LazyLock<UnitRegistry> = LazyLock::new(UnitRegistry::new);

/// Get the global unit registry.
pub fn registry() -> &'static UnitRegistry {
    &REGISTRY
}

/// Convert a value from one unit to another.
/// Supports SI-prefixed units (e.g., "km", "MHz", "nanoseconds").
/// Returns Err if units are incompatible or not found.
pub fn convert(value: f64, from: &str, to: &str) -> Result<f64, String> {
    let reg = registry();

    let from_resolved = reg
        .resolve_with_prefix(from)
        .ok_or_else(|| format!("Unknown unit: {}", from))?;
    let to_resolved = reg
        .resolve_with_prefix(to)
        .ok_or_else(|| format!("Unknown unit: {}", to))?;

    if from_resolved.unit.category != to_resolved.unit.category {
        return Err(format!(
            "Cannot convert between {} ({}) and {} ({})",
            from_resolved.unit.name,
            from_resolved.unit.category,
            to_resolved.unit.name,
            to_resolved.unit.category,
        ));
    }

    // Convert: from_unit (with prefix) -> base -> to_unit (with prefix)
    let base_value = from_resolved.to_base(value);
    let result = to_resolved.from_base(base_value);
    Ok(result)
}

/// Convert a value from one unit to another, with CSS config for screen units.
/// Uses CSS-specific conversion for CSS units, standard conversion otherwise.
pub fn convert_with_config(
    value: f64,
    from: &str,
    to: &str,
    css_config: &CssConfig,
) -> Result<f64, String> {
    let reg = registry();

    let from_resolved = reg
        .resolve_with_prefix(from)
        .ok_or_else(|| format!("Unknown unit: {}", from))?;
    let to_resolved = reg
        .resolve_with_prefix(to)
        .ok_or_else(|| format!("Unknown unit: {}", to))?;

    if from_resolved.unit.category != to_resolved.unit.category {
        return Err(format!(
            "Cannot convert between {} ({}) and {} ({})",
            from_resolved.unit.name,
            from_resolved.unit.category,
            to_resolved.unit.name,
            to_resolved.unit.category,
        ));
    }

    // For CSS units, use config-aware conversion
    if from_resolved.unit.category == UnitCategory::CssScreen {
        return css::convert_css(value, from, to, css_config);
    }

    // Standard conversion through base unit
    let base_value = from_resolved.to_base(value);
    Ok(to_resolved.from_base(base_value))
}

/// Check if a unit name belongs to the CSS/screen category.
pub fn is_css_unit(name: &str) -> bool {
    let reg = registry();
    if let Some(resolved) = reg.resolve_with_prefix(name) {
        resolved.unit.category == UnitCategory::CssScreen
    } else {
        false
    }
}

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

    // ─── Registry fundamentals ──────────────────────────────────────────

    #[test]
    fn test_registry_has_13_categories() {
        let reg = registry();
        let cats = reg.categories();
        assert!(
            cats.len() >= 13,
            "Expected at least 13 categories, got {}",
            cats.len()
        );
        for cat in UnitCategory::all() {
            assert!(cats.contains(cat), "Missing category: {}", cat);
        }
    }

    #[test]
    fn test_registry_has_200_plus_units() {
        let reg = registry();
        assert!(
            reg.unit_count() >= 200,
            "Expected at least 200 units, got {}",
            reg.unit_count()
        );
    }

    // ─── Basic conversions ──────────────────────────────────────────────

    #[test]
    fn test_linear_conversion_miles_to_km() {
        let result = convert(5.0, "miles", "km").unwrap();
        assert!(
            (result - 8.04672).abs() < 1e-4,
            "Expected 8.04672, got {}",
            result
        );
    }

    #[test]
    fn test_km_to_miles() {
        let result = convert(5.0, "km", "miles").unwrap();
        assert!(
            (result - 3.10686).abs() < 1e-4,
            "Expected ~3.10686, got {}",
            result
        );
    }

    #[test]
    fn test_temperature_fahrenheit_to_celsius() {
        let result = convert(100.0, "F", "C").unwrap();
        let expected = (100.0 - 32.0) * 5.0 / 9.0;
        assert!(
            (result - expected).abs() < 1e-10,
            "Expected {}, got {}",
            expected,
            result
        );
    }

    #[test]
    fn test_temperature_celsius_to_fahrenheit() {
        let result = convert(0.0, "C", "F").unwrap();
        assert!((result - 32.0).abs() < 1e-10);
    }

    #[test]
    fn test_temperature_kelvin_roundtrip() {
        let result = convert(100.0, "C", "K").unwrap();
        assert!((result - 373.15).abs() < 1e-10);
        let back = convert(result, "K", "C").unwrap();
        assert!((back - 100.0).abs() < 1e-10);
    }

    #[test]
    fn test_volume_gallon_to_liters() {
        let result = convert(1.0, "gallon", "liters").unwrap();
        assert!(
            (result - 3.78541).abs() < 1e-4,
            "Expected 3.78541, got {}",
            result
        );
    }

    #[test]
    fn test_incompatible_categories() {
        let result = convert(5.0, "kg", "meters");
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.contains("mass"), "Error should mention mass: {}", err);
        assert!(err.contains("length"), "Error should mention length: {}", err);
    }

    #[test]
    fn test_multiple_aliases_resolve_same() {
        let reg = registry();
        let m1 = reg.lookup("meter").unwrap();
        let m2 = reg.lookup("metre").unwrap();
        let m3 = reg.lookup("m").unwrap();
        assert_eq!(m1.name, m2.name);
        assert_eq!(m2.name, m3.name);
    }

    #[test]
    fn test_case_insensitive_lookup() {
        let reg = registry();
        assert!(reg.lookup("KM").is_some());
        assert!(reg.lookup("Km").is_some());
        assert!(reg.lookup("km").is_some());
    }

    #[test]
    fn test_unknown_unit() {
        let result = convert(1.0, "frobnitz", "meters");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Unknown unit"));
    }

    #[test]
    fn test_identity_conversion() {
        let result = convert(42.0, "meters", "meters").unwrap();
        assert!((result - 42.0).abs() < 1e-10);
    }

    // ─── SI prefix integration ──────────────────────────────────────────

    #[test]
    fn test_all_prefix_factors_via_conversion() {
        let r = convert(1.0, "nm", "m").unwrap();
        assert!((r - 1e-9).abs() < 1e-20, "nano: got {}", r);

        let r = convert(1.0, "ms", "s").unwrap();
        assert!((r - 1e-3).abs() < 1e-14, "milli: got {}", r);

        let r = convert(1.0, "cm", "m").unwrap();
        assert!((r - 1e-2).abs() < 1e-14, "centi: got {}", r);

        let r = convert(1.0, "km", "m").unwrap();
        assert!((r - 1e3).abs() < 1e-8, "kilo: got {}", r);

        let r = convert(1.0, "MB", "B").unwrap();
        assert!((r - 1e6).abs() < 1e-4, "mega: got {}", r);

        let r = convert(1.0, "GB", "B").unwrap();
        assert!((r - 1e9).abs() < 1e-1, "giga: got {}", r);

        let r = convert(1.0, "TB", "B").unwrap();
        assert!((r - 1e12).abs() < 1e2, "tera: got {}", r);
    }

    #[test]
    fn test_resolve_direct_lookup_priority() {
        let reg = registry();
        let resolved = reg.resolve_with_prefix("km").unwrap();
        assert_eq!(resolved.prefix_factor, 1.0);
        assert_eq!(resolved.unit.name, "kilometer");
    }

    #[test]
    fn test_resolve_novel_prefix_combination() {
        let reg = registry();
        let resolved = reg.resolve_with_prefix("nJ");
        assert!(resolved.is_some(), "nJ should resolve via SI prefix");
        let r = resolved.unwrap();
        assert_eq!(r.unit.name, "joule");
        assert_eq!(r.prefix_factor, 1e-9);
    }

    #[test]
    fn test_resolve_long_form_novel() {
        let reg = registry();
        let resolved = reg.resolve_with_prefix("terawatts");
        assert!(resolved.is_some(), "terawatts should resolve via long-form SI prefix");
        let r = resolved.unwrap();
        assert_eq!(r.unit.name, "watt");
        assert_eq!(r.prefix_factor, 1e12);
    }

    #[test]
    fn test_kilofahrenheit_rejected() {
        let result = convert(1.0, "kilofahrenheit", "celsius");
        assert!(result.is_err());
    }

    #[test]
    fn test_kilomiles_rejected() {
        let result = convert(1.0, "kilomiles", "meters");
        assert!(result.is_err());
    }

    #[test]
    fn test_micro_sign_u00b5_in_convert() {
        let result = convert(3.0, "\u{00B5}s", "ms").unwrap();
        assert!(
            (result - 0.003).abs() < 1e-10,
            "Expected 0.003, got {}",
            result
        );
    }

    // ─── Data unit case sensitivity ─────────────────────────────────────

    #[test]
    fn test_case_sensitive_b_vs_big_b() {
        let reg = registry();
        let big_b = reg.lookup("B").unwrap();
        assert_eq!(big_b.name, "byte");
        let small_b = reg.lookup("b").unwrap();
        assert_eq!(small_b.name, "bit");
    }

    #[test]
    fn test_case_sensitive_mb_vs_big_mb() {
        let reg = registry();
        let big_mb = reg.lookup("MB").unwrap();
        assert_eq!(big_mb.name, "megabyte");
        let small_mb = reg.lookup("Mb").unwrap();
        assert_eq!(small_mb.name, "megabit");
    }

    #[test]
    fn test_megabytes_to_megabits() {
        let result = convert(1.0, "MB", "Mb").unwrap();
        assert!((result - 8.0).abs() < 1e-10, "Expected 8, got {}", result);
    }

    #[test]
    fn test_binary_mib_to_kib() {
        let result = convert(1.0, "MiB", "KiB").unwrap();
        assert!((result - 1024.0).abs() < 1e-10, "Expected 1024, got {}", result);
    }

    #[test]
    fn test_cross_convert_gib_to_gb() {
        let result = convert(5.0, "GiB", "GB").unwrap();
        let expected = 5.0 * 1_073_741_824.0 / 1e9;
        assert!((result - expected).abs() < 1e-6, "Expected {}, got {}", expected, result);
    }

    // ─── CSS unit tests ─────────────────────────────────────────────────

    #[test]
    fn test_css_12pt_to_px_default() {
        let config = CssConfig::default();
        let result = css::convert_css(12.0, "pt", "px", &config).unwrap();
        assert!((result - 16.0).abs() < 1e-10, "12pt should be 16px at PPI=96, got {}", result);
    }

    #[test]
    fn test_css_2em_to_px_default() {
        let config = CssConfig::default();
        let result = css::convert_css(2.0, "em", "px", &config).unwrap();
        assert!((result - 32.0).abs() < 1e-10, "2em should be 32px, got {}", result);
    }

    #[test]
    fn test_css_12pt_to_px_retina() {
        let config = CssConfig { ppi: 326.0, em_base_px: 16.0 };
        let result = css::convert_css(12.0, "pt", "px", &config).unwrap();
        let expected = 12.0 * 326.0 / 72.0;
        assert!((result - expected).abs() < 1e-10, "Expected {}, got {}", expected, result);
    }

    #[test]
    fn test_css_2em_custom_base() {
        let config = CssConfig { ppi: 96.0, em_base_px: 20.0 };
        let result = css::convert_css(2.0, "em", "px", &config).unwrap();
        assert!((result - 40.0).abs() < 1e-10, "2em at em=20 should be 40px, got {}", result);
    }

    #[test]
    fn test_css_units_in_css_category() {
        let reg = registry();
        for name in &["px", "pt", "em", "rem"] {
            let unit = reg.lookup(name).unwrap();
            assert_eq!(unit.category, UnitCategory::CssScreen, "{} should be CssScreen", name);
        }
    }

    #[test]
    fn test_css_incompatible_with_length() {
        let config = CssConfig::default();
        let result = convert_with_config(1.0, "px", "meters", &config);
        assert!(result.is_err());
    }

    #[test]
    fn test_is_css_unit() {
        assert!(is_css_unit("px"));
        assert!(is_css_unit("pt"));
        assert!(is_css_unit("em"));
        assert!(is_css_unit("rem"));
        assert!(!is_css_unit("kg"));
        assert!(!is_css_unit("meters"));
    }

    // ─── Performance ────────────────────────────────────────────────────

    #[test]
    fn test_o1_lookup_performance() {
        let reg = registry();
        let _ = reg.lookup("meter");
        let start = std::time::Instant::now();
        for _ in 0..10_000 {
            let _ = reg.lookup("kilometer");
            let _ = reg.lookup("lb");
            let _ = reg.lookup("F");
        }
        let elapsed = start.elapsed();
        assert!(
            elapsed.as_millis() < 50,
            "Lookup too slow: {}ms for 30k lookups",
            elapsed.as_millis()
        );
    }
}
