//! Plugin registry: loads, manages, and queries plugins.
//!
//! [`PluginRegistry`] is the central coordinator that:
//! - Accepts plugins (via [`add`] / [`add_boxed`])
//! - Calls their lifecycle hooks (init, registration, shutdown)
//! - Merges their contributions (functions, units, variables)
//! - Detects and reports naming conflicts with built-in symbols

use std::collections::HashMap;

use super::api::{
    CalcPadPlugin, PluginError, PluginFn, PluginFunctionRegistry, PluginUnitRegistry,
    PluginValue, PluginVariableRegistry, PluginWarning,
};

/// Names that are reserved by the engine's built-in function set.
/// Plugin functions with any of these names will be skipped, and a
/// warning will be emitted instead.
const BUILTIN_FUNCTION_NAMES: &[&str] = &[
    "sin", "cos", "tan", "asin", "acos", "atan",
    "sinh", "cosh", "tanh",
    "sqrt", "abs", "ceil", "floor", "round",
    "ln", "log", "log2", "log10",
    "exp", "pow", "min", "max",
    "factorial", "nPr", "nCr",
    "gcd", "lcm", "sum", "avg", "average",
    "compound_interest", "mortgage_payment",
    "tc_to_frames", "frames_to_tc",
];

fn is_builtin_function(name: &str) -> bool {
    BUILTIN_FUNCTION_NAMES.contains(&name)
}

/// Result of loading one or more plugins into the registry.
#[derive(Debug, Default)]
pub struct LoadResult {
    /// Number of plugins that loaded successfully.
    pub loaded: usize,
    /// Errors from plugins that failed to load.
    pub errors: Vec<PluginError>,
    /// Non-fatal warnings (e.g., naming conflicts).
    pub warnings: Vec<PluginWarning>,
}

/// Manages the full set of loaded plugins and their merged contributions.
pub struct PluginRegistry {
    /// Loaded plugin instances (kept alive for shutdown).
    plugins: Vec<Box<dyn CalcPadPlugin>>,
    /// Merged function map: name -> callable.
    functions: HashMap<String, PluginFn>,
    /// Merged unit map: name -> conversion factor.
    units: HashMap<String, f64>,
    /// Merged variable map: name -> value.
    variables: HashMap<String, f64>,
    /// Accumulated warnings from all load operations.
    warnings: Vec<PluginWarning>,
}

impl PluginRegistry {
    /// Create an empty registry.
    pub fn new() -> Self {
        Self {
            plugins: Vec::new(),
            functions: HashMap::new(),
            units: HashMap::new(),
            variables: HashMap::new(),
            warnings: Vec::new(),
        }
    }

    /// Add a plugin (by value) to the registry.
    ///
    /// Calls `init()`, then the three `register_*()` methods.
    /// On init failure the plugin is **not** added and the error is returned.
    pub fn add<P: CalcPadPlugin + 'static>(&mut self, plugin: P) -> Result<(), PluginError> {
        self.add_boxed(Box::new(plugin))
    }

    /// Add a boxed plugin to the registry.
    pub fn add_boxed(&mut self, plugin: Box<dyn CalcPadPlugin>) -> Result<(), PluginError> {
        let name = plugin.name().to_string();

        // Lifecycle: init
        plugin.init().map_err(|msg| PluginError {
            plugin_name: name.clone(),
            message: msg,
        })?;

        // Collect functions
        let mut freg = PluginFunctionRegistry::new();
        plugin.register_functions(&mut freg);
        for (fname, func) in freg.functions {
            if is_builtin_function(&fname) {
                self.warnings.push(PluginWarning {
                    plugin_name: name.clone(),
                    message: format!(
                        "function '{}' conflicts with built-in -- built-in takes precedence",
                        fname
                    ),
                });
            } else if self.functions.contains_key(&fname) {
                self.warnings.push(PluginWarning {
                    plugin_name: name.clone(),
                    message: format!(
                        "function '{}' already registered by another plugin -- overwritten",
                        fname
                    ),
                });
                self.functions.insert(fname, func);
            } else {
                self.functions.insert(fname, func);
            }
        }

        // Collect units
        let mut ureg = PluginUnitRegistry::new();
        plugin.register_units(&mut ureg);
        for (uname, factor) in ureg.units {
            if self.units.contains_key(&uname) {
                self.warnings.push(PluginWarning {
                    plugin_name: name.clone(),
                    message: format!(
                        "unit '{}' already registered -- overwritten",
                        uname
                    ),
                });
            }
            self.units.insert(uname, factor);
        }

        // Collect variables
        let mut vreg = PluginVariableRegistry::new();
        plugin.register_variables(&mut vreg);
        for (vname, value) in vreg.variables {
            if self.variables.contains_key(&vname) {
                self.warnings.push(PluginWarning {
                    plugin_name: name.clone(),
                    message: format!(
                        "variable '{}' already registered -- overwritten",
                        vname
                    ),
                });
            }
            self.variables.insert(vname, value);
        }

        self.plugins.push(plugin);
        Ok(())
    }

    // -------------------------------------------------------------------
    // Queries
    // -------------------------------------------------------------------

    /// Number of loaded plugins.
    pub fn plugin_count(&self) -> usize {
        self.plugins.len()
    }

    /// Names of all loaded plugins.
    pub fn plugin_names(&self) -> Vec<&str> {
        self.plugins.iter().map(|p| p.name()).collect()
    }

    /// Look up a plugin-provided function by name.
    pub fn get_function(&self, name: &str) -> Option<&PluginFn> {
        self.functions.get(name)
    }

    /// Call a plugin function by name.
    pub fn call_function(
        &self,
        name: &str,
        args: &[PluginValue],
    ) -> Result<PluginValue, String> {
        let func = self
            .functions
            .get(name)
            .ok_or_else(|| format!("unknown plugin function: {}", name))?;
        func(args)
    }

    /// Check whether a plugin function with the given name exists.
    pub fn has_function(&self, name: &str) -> bool {
        self.functions.contains_key(name)
    }

    /// All registered plugin function names (sorted).
    pub fn function_names(&self) -> Vec<&str> {
        let mut names: Vec<&str> = self.functions.keys().map(|s| s.as_str()).collect();
        names.sort();
        names
    }

    /// Look up a plugin-provided unit's conversion factor.
    pub fn get_unit_factor(&self, name: &str) -> Option<f64> {
        self.units.get(name).copied()
    }

    /// Check whether a plugin unit with the given name exists.
    pub fn has_unit(&self, name: &str) -> bool {
        self.units.contains_key(name)
    }

    /// All registered plugin unit names (sorted).
    pub fn unit_names(&self) -> Vec<&str> {
        let mut names: Vec<&str> = self.units.keys().map(|s| s.as_str()).collect();
        names.sort();
        names
    }

    /// Look up a plugin-provided variable's value.
    pub fn get_variable(&self, name: &str) -> Option<f64> {
        self.variables.get(name).copied()
    }

    /// Check whether a plugin variable with the given name exists.
    pub fn has_variable(&self, name: &str) -> bool {
        self.variables.contains_key(name)
    }

    /// All registered plugin variable names (sorted).
    pub fn variable_names(&self) -> Vec<&str> {
        let mut names: Vec<&str> = self.variables.keys().map(|s| s.as_str()).collect();
        names.sort();
        names
    }

    /// Warnings accumulated during loading.
    pub fn warnings(&self) -> &[PluginWarning] {
        &self.warnings
    }

    // -------------------------------------------------------------------
    // Lifecycle: shutdown
    // -------------------------------------------------------------------

    /// Shut down all loaded plugins (calls `shutdown()` on each).
    pub fn shutdown_all(&self) {
        for plugin in &self.plugins {
            plugin.shutdown();
        }
    }
}

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

impl Drop for PluginRegistry {
    fn drop(&mut self) {
        self.shutdown_all();
    }
}

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

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

    // -- helpers --

    struct MathPlugin;
    impl CalcPadPlugin for MathPlugin {
        fn name(&self) -> &str { "math_extras" }

        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))
            });
            reg.register("cube", |args| {
                if args.len() != 1 {
                    return Err("cube() requires 1 argument".into());
                }
                let n = args[0].number;
                Ok(PluginValue::new(n * n * n))
            });
        }

        fn register_variables(&self, reg: &mut PluginVariableRegistry) {
            reg.register("golden_ratio", 1.618033988749);
        }
    }

    struct UnitsPlugin;
    impl CalcPadPlugin for UnitsPlugin {
        fn name(&self) -> &str { "custom_units" }

        fn register_units(&self, reg: &mut PluginUnitRegistry) {
            reg.register("stone", 6.35029);
            reg.register("furlong", 201.168);
        }
    }

    struct FailingPlugin;
    impl CalcPadPlugin for FailingPlugin {
        fn name(&self) -> &str { "failing" }
        fn init(&self) -> Result<(), String> {
            Err("cannot connect to service".into())
        }
    }

    struct ConflictPlugin;
    impl CalcPadPlugin for ConflictPlugin {
        fn name(&self) -> &str { "conflict" }
        fn register_functions(&self, reg: &mut PluginFunctionRegistry) {
            // "sin" is a built-in
            reg.register("sin", |_| Ok(PluginValue::new(0.0)));
            // "my_func" is not
            reg.register("my_func", |_| Ok(PluginValue::new(99.0)));
        }
    }

    // -- tests --

    #[test]
    fn test_empty_registry() {
        let reg = PluginRegistry::new();
        assert_eq!(reg.plugin_count(), 0);
        assert!(reg.function_names().is_empty());
        assert!(reg.unit_names().is_empty());
        assert!(reg.variable_names().is_empty());
        assert!(reg.warnings().is_empty());
    }

    #[test]
    fn test_add_plugin_functions_and_variables() {
        let mut reg = PluginRegistry::new();
        reg.add(MathPlugin).unwrap();

        assert_eq!(reg.plugin_count(), 1);
        assert_eq!(reg.plugin_names(), vec!["math_extras"]);

        // Functions
        assert!(reg.has_function("square"));
        assert!(reg.has_function("cube"));
        assert!(!reg.has_function("nonexistent"));

        let result = reg.call_function("square", &[PluginValue::new(7.0)]).unwrap();
        assert_eq!(result.number, 49.0);

        let result = reg.call_function("cube", &[PluginValue::new(3.0)]).unwrap();
        assert_eq!(result.number, 27.0);

        // Variables
        assert!(reg.has_variable("golden_ratio"));
        let gr = reg.get_variable("golden_ratio").unwrap();
        assert!((gr - 1.618033988749).abs() < 1e-10);
    }

    #[test]
    fn test_add_plugin_units() {
        let mut reg = PluginRegistry::new();
        reg.add(UnitsPlugin).unwrap();

        assert!(reg.has_unit("stone"));
        assert!(reg.has_unit("furlong"));
        assert_eq!(reg.get_unit_factor("stone"), Some(6.35029));
    }

    #[test]
    fn test_multiple_plugins() {
        let mut reg = PluginRegistry::new();
        reg.add(MathPlugin).unwrap();
        reg.add(UnitsPlugin).unwrap();

        assert_eq!(reg.plugin_count(), 2);
        assert!(reg.has_function("square"));
        assert!(reg.has_unit("stone"));
    }

    #[test]
    fn test_plugin_init_failure_does_not_add() {
        let mut reg = PluginRegistry::new();
        let err = reg.add(FailingPlugin).unwrap_err();
        assert_eq!(err.plugin_name, "failing");
        assert!(err.message.contains("cannot connect"));
        assert_eq!(reg.plugin_count(), 0);
    }

    #[test]
    fn test_builtin_conflict_generates_warning() {
        let mut reg = PluginRegistry::new();
        reg.add(ConflictPlugin).unwrap();

        // "sin" should NOT be registered (built-in conflict)
        assert!(!reg.has_function("sin"));
        // "my_func" should be registered
        assert!(reg.has_function("my_func"));

        assert_eq!(reg.warnings().len(), 1);
        assert!(reg.warnings()[0].message.contains("sin"));
        assert!(reg.warnings()[0].message.contains("built-in"));
    }

    #[test]
    fn test_duplicate_function_name_across_plugins() {
        struct PluginA;
        impl CalcPadPlugin for PluginA {
            fn name(&self) -> &str { "a" }
            fn register_functions(&self, reg: &mut PluginFunctionRegistry) {
                reg.register("shared_fn", |_| Ok(PluginValue::new(1.0)));
            }
        }
        struct PluginB;
        impl CalcPadPlugin for PluginB {
            fn name(&self) -> &str { "b" }
            fn register_functions(&self, reg: &mut PluginFunctionRegistry) {
                reg.register("shared_fn", |_| Ok(PluginValue::new(2.0)));
            }
        }

        let mut reg = PluginRegistry::new();
        reg.add(PluginA).unwrap();
        reg.add(PluginB).unwrap();

        // The second plugin's version should overwrite
        let result = reg.call_function("shared_fn", &[]).unwrap();
        assert_eq!(result.number, 2.0);

        // A warning should have been emitted
        assert_eq!(reg.warnings().len(), 1);
        assert!(reg.warnings()[0].message.contains("overwritten"));
    }

    #[test]
    fn test_call_unknown_function_returns_error() {
        let reg = PluginRegistry::new();
        let err = reg.call_function("nope", &[]).unwrap_err();
        assert!(err.contains("unknown plugin function"));
    }

    #[test]
    fn test_function_names_sorted() {
        let mut reg = PluginRegistry::new();
        struct SortPlugin;
        impl CalcPadPlugin for SortPlugin {
            fn name(&self) -> &str { "sort" }
            fn register_functions(&self, r: &mut PluginFunctionRegistry) {
                r.register("zebra", |_| Ok(PluginValue::new(0.0)));
                r.register("alpha", |_| Ok(PluginValue::new(0.0)));
                r.register("mid", |_| Ok(PluginValue::new(0.0)));
            }
        }
        reg.add(SortPlugin).unwrap();
        assert_eq!(reg.function_names(), vec!["alpha", "mid", "zebra"]);
    }

    #[test]
    fn test_shutdown_is_called() {
        use std::sync::atomic::{AtomicBool, Ordering};
        static SHUT_DOWN: AtomicBool = AtomicBool::new(false);

        struct ShutdownPlugin;
        impl CalcPadPlugin for ShutdownPlugin {
            fn name(&self) -> &str { "shutdown_test" }
            fn shutdown(&self) {
                SHUT_DOWN.store(true, Ordering::SeqCst);
            }
        }

        {
            let mut reg = PluginRegistry::new();
            reg.add(ShutdownPlugin).unwrap();
            reg.shutdown_all();
            assert!(SHUT_DOWN.load(Ordering::SeqCst));
        }
    }

    #[test]
    fn test_default_trait() {
        let reg = PluginRegistry::default();
        assert_eq!(reg.plugin_count(), 0);
    }
}
