//! Plugin API: the `CalcPadPlugin` trait and supporting types.
//!
//! Developers implement [`CalcPadPlugin`] to extend calcpad-engine with
//! custom functions, units, and variables.  The trait is object-safe so
//! plugins can be loaded dynamically at runtime.

use std::collections::HashMap;
use std::sync::Arc;

// ---------------------------------------------------------------------------
// Plugin Value type (simplified, f64-based)
// ---------------------------------------------------------------------------

/// A value exchanged between the engine and plugins.
///
/// Plugin functions receive arguments as `PluginValue` and return one.
/// When the user writes `80kg`, the plugin receives
/// `PluginValue { number: 80.0, unit: Some("kg") }`.
#[derive(Clone, Debug, PartialEq)]
pub struct PluginValue {
    pub number: f64,
    pub unit: Option<String>,
}

impl PluginValue {
    /// Create a plain numeric value (no unit).
    pub fn new(number: f64) -> Self {
        Self { number, unit: None }
    }

    /// Create a value with an attached unit string.
    pub fn with_unit(number: f64, unit: impl Into<String>) -> Self {
        Self {
            number,
            unit: Some(unit.into()),
        }
    }
}

// ---------------------------------------------------------------------------
// Plugin function signature
// ---------------------------------------------------------------------------

/// A plugin function: takes a slice of `PluginValue` args and returns
/// a `PluginValue` or an error message.
pub type PluginFn = Arc<dyn Fn(&[PluginValue]) -> Result<PluginValue, String> + Send + Sync>;

// ---------------------------------------------------------------------------
// Per-category registries (passed to the plugin during registration)
// ---------------------------------------------------------------------------

/// Registry that a plugin fills with custom functions.
pub struct PluginFunctionRegistry {
    pub(crate) functions: HashMap<String, PluginFn>,
}

impl PluginFunctionRegistry {
    pub fn new() -> Self {
        Self {
            functions: HashMap::new(),
        }
    }

    /// Register a named function.
    pub fn register(
        &mut self,
        name: impl Into<String>,
        func: impl Fn(&[PluginValue]) -> Result<PluginValue, String> + Send + Sync + 'static,
    ) {
        self.functions.insert(name.into(), Arc::new(func));
    }

    /// Iterate over registered function names.
    pub fn names(&self) -> impl Iterator<Item = &str> {
        self.functions.keys().map(|s| s.as_str())
    }
}

impl Default for PluginFunctionRegistry {
    fn default() -> Self {
        Self::new()
    }
}

/// Registry that a plugin fills with custom units.
///
/// Each unit is stored as a conversion factor relative to some base.
/// For example, `"lb"` with factor `0.453592` means `80 lb` evaluates
/// to `80 * 0.453592 = 36.287` in the base unit (kg).
pub struct PluginUnitRegistry {
    pub(crate) units: HashMap<String, f64>,
}

impl PluginUnitRegistry {
    pub fn new() -> Self {
        Self {
            units: HashMap::new(),
        }
    }

    /// Register a unit with its conversion factor.
    pub fn register(&mut self, name: impl Into<String>, factor: f64) {
        self.units.insert(name.into(), factor);
    }

    /// Iterate over registered unit names.
    pub fn names(&self) -> impl Iterator<Item = &str> {
        self.units.keys().map(|s| s.as_str())
    }
}

impl Default for PluginUnitRegistry {
    fn default() -> Self {
        Self::new()
    }
}

/// Registry that a plugin fills with custom variables (constants).
pub struct PluginVariableRegistry {
    pub(crate) variables: HashMap<String, f64>,
}

impl PluginVariableRegistry {
    pub fn new() -> Self {
        Self {
            variables: HashMap::new(),
        }
    }

    /// Register a named constant.
    pub fn register(&mut self, name: impl Into<String>, value: f64) {
        self.variables.insert(name.into(), value);
    }

    /// Iterate over registered variable names.
    pub fn names(&self) -> impl Iterator<Item = &str> {
        self.variables.keys().map(|s| s.as_str())
    }
}

impl Default for PluginVariableRegistry {
    fn default() -> Self {
        Self::new()
    }
}

// ---------------------------------------------------------------------------
// Plugin lifecycle
// ---------------------------------------------------------------------------

/// Errors that can occur during plugin initialisation.
#[derive(Clone, Debug, PartialEq)]
pub struct PluginError {
    pub plugin_name: String,
    pub message: String,
}

impl std::fmt::Display for PluginError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[plugin '{}'] {}", self.plugin_name, self.message)
    }
}

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

/// A warning generated during plugin loading (e.g., name conflicts with
/// built-in functions).
#[derive(Clone, Debug, PartialEq)]
pub struct PluginWarning {
    pub plugin_name: String,
    pub message: String,
}

impl std::fmt::Display for PluginWarning {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[plugin '{}'] {}", self.plugin_name, self.message)
    }
}

// ---------------------------------------------------------------------------
// The CalcPadPlugin trait
// ---------------------------------------------------------------------------

/// The trait that all plugins implement to extend CalcPad.
///
/// # Lifecycle
///
/// 1. **`init()`** — Called once when the plugin is loaded.  Perform any
///    one-time setup here.  Return `Err` to abort loading.
/// 2. **`register_functions()`** / **`register_units()`** /
///    **`register_variables()`** — Called after a successful `init()` to
///    collect the plugin's contributions.
/// 3. **`shutdown()`** — Called when the plugin is unloaded or the engine
///    is torn down.  Clean up resources here.
///
/// # Example
///
/// ```rust
/// use calcpad_engine::plugins::api::*;
///
/// struct BmiPlugin;
///
/// impl CalcPadPlugin for BmiPlugin {
///     fn name(&self) -> &str { "bmi" }
///
///     fn register_functions(&self, reg: &mut PluginFunctionRegistry) {
///         reg.register("bmi", |args| {
///             if args.len() != 2 {
///                 return Err("bmi() requires 2 arguments: weight, height".into());
///             }
///             let weight = args[0].number;
///             let height = args[1].number;
///             if height == 0.0 {
///                 return Err("height cannot be zero".into());
///             }
///             Ok(PluginValue::new(weight / (height * height)))
///         });
///     }
/// }
/// ```
pub trait CalcPadPlugin: Send + Sync {
    /// A short, unique name used for identification and conflict reporting.
    fn name(&self) -> &str;

    /// Called once when the plugin is loaded.
    ///
    /// Return `Err` to abort loading; the error will be surfaced to the user.
    fn init(&self) -> Result<(), String> {
        Ok(())
    }

    /// Register custom functions with the engine.
    fn register_functions(&self, _registry: &mut PluginFunctionRegistry) {}

    /// Register custom units with the engine.
    fn register_units(&self, _registry: &mut PluginUnitRegistry) {}

    /// Register custom variables / constants with the engine.
    fn register_variables(&self, _registry: &mut PluginVariableRegistry) {}

    /// Called when the plugin is unloaded.  Clean up any resources here.
    fn shutdown(&self) {}
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn test_plugin_value_plain() {
        let v = PluginValue::new(42.0);
        assert_eq!(v.number, 42.0);
        assert!(v.unit.is_none());
    }

    #[test]
    fn test_plugin_value_with_unit() {
        let v = PluginValue::with_unit(80.0, "kg");
        assert_eq!(v.number, 80.0);
        assert_eq!(v.unit.as_deref(), Some("kg"));
    }

    #[test]
    fn test_function_registry_register_and_call() {
        let mut reg = PluginFunctionRegistry::new();
        reg.register("double", |args| {
            Ok(PluginValue::new(args[0].number * 2.0))
        });
        let func = reg.functions.get("double").unwrap();
        let result = func(&[PluginValue::new(21.0)]).unwrap();
        assert_eq!(result.number, 42.0);
    }

    #[test]
    fn test_function_registry_names() {
        let mut reg = PluginFunctionRegistry::new();
        reg.register("foo", |_| Ok(PluginValue::new(0.0)));
        reg.register("bar", |_| Ok(PluginValue::new(0.0)));
        let mut names: Vec<&str> = reg.names().collect();
        names.sort();
        assert_eq!(names, vec!["bar", "foo"]);
    }

    #[test]
    fn test_unit_registry() {
        let mut reg = PluginUnitRegistry::new();
        reg.register("kg", 1.0);
        reg.register("lb", 0.453592);
        assert_eq!(reg.units.get("kg"), Some(&1.0));
        assert_eq!(reg.units.get("lb"), Some(&0.453592));
    }

    #[test]
    fn test_unit_registry_names() {
        let mut reg = PluginUnitRegistry::new();
        reg.register("m", 1.0);
        reg.register("ft", 0.3048);
        let mut names: Vec<&str> = reg.names().collect();
        names.sort();
        assert_eq!(names, vec!["ft", "m"]);
    }

    #[test]
    fn test_variable_registry() {
        let mut reg = PluginVariableRegistry::new();
        reg.register("pi", std::f64::consts::PI);
        assert_eq!(reg.variables.get("pi"), Some(&std::f64::consts::PI));
    }

    #[test]
    fn test_plugin_trait_full_implementation() {
        struct TestPlugin;
        impl CalcPadPlugin for TestPlugin {
            fn name(&self) -> &str { "test" }

            fn init(&self) -> Result<(), String> { Ok(()) }

            fn register_functions(&self, reg: &mut PluginFunctionRegistry) {
                reg.register("square", |args| {
                    if args.len() != 1 {
                        return Err("square() requires 1 argument".into());
                    }
                    Ok(PluginValue::new(args[0].number * args[0].number))
                });
            }

            fn register_units(&self, reg: &mut PluginUnitRegistry) {
                reg.register("cm", 0.01);
            }

            fn register_variables(&self, reg: &mut PluginVariableRegistry) {
                reg.register("tau", std::f64::consts::TAU);
            }

            fn shutdown(&self) {}
        }

        let plugin = TestPlugin;
        assert_eq!(plugin.name(), "test");
        assert!(plugin.init().is_ok());

        let mut freg = PluginFunctionRegistry::new();
        plugin.register_functions(&mut freg);
        let func = freg.functions.get("square").unwrap();
        assert_eq!(func(&[PluginValue::new(5.0)]).unwrap().number, 25.0);

        let mut ureg = PluginUnitRegistry::new();
        plugin.register_units(&mut ureg);
        assert_eq!(ureg.units.get("cm"), Some(&0.01));

        let mut vreg = PluginVariableRegistry::new();
        plugin.register_variables(&mut vreg);
        assert_eq!(vreg.variables.get("tau"), Some(&std::f64::consts::TAU));

        plugin.shutdown();
    }

    #[test]
    fn test_plugin_default_methods_are_noops() {
        struct MinimalPlugin;
        impl CalcPadPlugin for MinimalPlugin {
            fn name(&self) -> &str { "minimal" }
        }

        let p = MinimalPlugin;
        assert!(p.init().is_ok());
        // Should not panic:
        let mut freg = PluginFunctionRegistry::new();
        p.register_functions(&mut freg);
        assert_eq!(freg.functions.len(), 0);
        p.shutdown();
    }

    #[test]
    fn test_plugin_init_can_fail() {
        struct FailPlugin;
        impl CalcPadPlugin for FailPlugin {
            fn name(&self) -> &str { "fail" }
            fn init(&self) -> Result<(), String> {
                Err("missing required config".into())
            }
        }
        let p = FailPlugin;
        let err = p.init().unwrap_err();
        assert_eq!(err, "missing required config");
    }

    #[test]
    fn test_plugin_warning_display() {
        let w = PluginWarning {
            plugin_name: "myplugin".to_string(),
            message: "function 'sin' conflicts with built-in".to_string(),
        };
        assert_eq!(
            w.to_string(),
            "[plugin 'myplugin'] function 'sin' conflicts with built-in"
        );
    }

    #[test]
    fn test_plugin_error_display() {
        let e = PluginError {
            plugin_name: "bad".to_string(),
            message: "init failed".to_string(),
        };
        assert_eq!(e.to_string(), "[plugin 'bad'] init failed");
    }

    #[test]
    fn test_function_returns_error() {
        let mut reg = PluginFunctionRegistry::new();
        reg.register("safe_div", |args| {
            if args.len() != 2 {
                return Err("safe_div() requires 2 args".into());
            }
            if args[1].number == 0.0 {
                return Err("division by zero".into());
            }
            Ok(PluginValue::new(args[0].number / args[1].number))
        });
        let func = reg.functions.get("safe_div").unwrap();
        assert_eq!(func(&[PluginValue::new(10.0), PluginValue::new(2.0)]).unwrap().number, 5.0);
        let err = func(&[PluginValue::new(1.0), PluginValue::new(0.0)]).unwrap_err();
        assert!(err.contains("division by zero"));
    }

    #[test]
    fn test_plugin_fn_is_send_sync() {
        // Compile-time check: PluginFn must be Send + Sync
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<PluginFn>();
    }
}
