lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Plugin manager for loading, unloading, and executing WASM plugins.
//!
//! This module provides the high-level API for managing plugins and their
//! association with datasets and hooks.

use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use lazy_static::lazy_static;
use spin::Mutex;

use super::error::PluginError;
use super::host::HostState;
use super::runtime::WasmPlugin;
use super::types::{HookType, PluginContext, PluginInfo, PluginLimits, PluginManifest};

// ═══════════════════════════════════════════════════════════════════════════════
// GLOBAL PLUGIN REGISTRY
// ═══════════════════════════════════════════════════════════════════════════════

lazy_static! {
    /// Global registry of loaded plugins.
    static ref PLUGINS: Mutex<BTreeMap<String, WasmPlugin>> = Mutex::new(BTreeMap::new());

    /// Mapping of datasets to their attached plugins and hooks.
    static ref DATASET_HOOKS: Mutex<BTreeMap<String, Vec<DatasetHook>>> = Mutex::new(BTreeMap::new());

    /// Plugin configuration per dataset.
    static ref PLUGIN_CONFIG: Mutex<BTreeMap<String, BTreeMap<String, String>>> = Mutex::new(BTreeMap::new());
}

/// A hook attached to a dataset.
#[derive(Debug, Clone)]
struct DatasetHook {
    /// Plugin name
    plugin_name: String,
    /// Hook type
    hook: HookType,
    /// Priority (lower = runs first)
    priority: i32,
}

// ═══════════════════════════════════════════════════════════════════════════════
// PLUGIN MANAGER
// ═══════════════════════════════════════════════════════════════════════════════

/// Manager for WASM plugins.
///
/// Provides a high-level API for:
/// - Loading and unloading plugins
/// - Attaching plugins to datasets
/// - Executing hooks in the I/O pipeline
pub struct PluginManager;

impl PluginManager {
    /// Load a plugin from WASM bytes.
    ///
    /// # Arguments
    /// * `name` - Unique name for the plugin
    /// * `wasm_bytes` - Raw WASM module bytes
    ///
    /// # Returns
    /// The plugin manifest on success.
    pub fn load_plugin(name: &str, wasm_bytes: &[u8]) -> Result<PluginManifest, PluginError> {
        Self::load_plugin_with_limits(name, wasm_bytes, PluginLimits::default())
    }

    /// Load a plugin with custom resource limits.
    pub fn load_plugin_with_limits(
        name: &str,
        wasm_bytes: &[u8],
        limits: PluginLimits,
    ) -> Result<PluginManifest, PluginError> {
        let mut plugins = PLUGINS.lock();

        // Check if already loaded
        if plugins.contains_key(name) {
            return Err(PluginError::AlreadyLoaded(name.into()));
        }

        // Load the plugin
        let plugin = WasmPlugin::load_with_limits(name, wasm_bytes, limits)?;
        let manifest = plugin.manifest().clone();

        plugins.insert(name.into(), plugin);

        Ok(manifest)
    }

    /// Unload a plugin.
    ///
    /// This will also detach the plugin from all datasets.
    pub fn unload_plugin(name: &str) -> Result<(), PluginError> {
        let mut plugins = PLUGINS.lock();

        let mut plugin = plugins
            .remove(name)
            .ok_or_else(|| PluginError::NotFound(name.into()))?;

        // Destroy the plugin
        plugin.destroy()?;

        // Remove from all dataset hooks
        let mut hooks = DATASET_HOOKS.lock();
        for (_, dataset_hooks) in hooks.iter_mut() {
            dataset_hooks.retain(|h| h.plugin_name != name);
        }

        Ok(())
    }

    /// List all loaded plugins.
    pub fn list_plugins() -> Vec<PluginInfo> {
        let plugins = PLUGINS.lock();
        plugins
            .values()
            .map(|p| {
                let mut info = PluginInfo::from(p.manifest());
                info.invocation_count = p.invocation_count();
                info
            })
            .collect()
    }

    /// Get plugin info by name.
    pub fn get_plugin(name: &str) -> Option<PluginInfo> {
        let plugins = PLUGINS.lock();
        plugins.get(name).map(|p| {
            let mut info = PluginInfo::from(p.manifest());
            info.invocation_count = p.invocation_count();
            info
        })
    }

    /// Attach a plugin to a dataset for specific hooks.
    ///
    /// # Arguments
    /// * `dataset` - Dataset name
    /// * `plugin_name` - Plugin to attach
    /// * `hooks` - List of hooks to attach
    /// * `config` - Configuration key-value pairs for the plugin
    pub fn attach(
        dataset: &str,
        plugin_name: &str,
        hooks: &[HookType],
        config: &BTreeMap<String, String>,
    ) -> Result<(), PluginError> {
        Self::attach_with_priority(dataset, plugin_name, hooks, config, 0)
    }

    /// Attach a plugin with priority.
    ///
    /// Lower priority values run first.
    pub fn attach_with_priority(
        dataset: &str,
        plugin_name: &str,
        hooks: &[HookType],
        config: &BTreeMap<String, String>,
        priority: i32,
    ) -> Result<(), PluginError> {
        // Verify plugin exists
        {
            let plugins = PLUGINS.lock();
            if !plugins.contains_key(plugin_name) {
                return Err(PluginError::NotFound(plugin_name.into()));
            }
        }

        // Store config
        if !config.is_empty() {
            let mut configs = PLUGIN_CONFIG.lock();
            let key = alloc::format!("{}:{}", dataset, plugin_name);
            configs.insert(key, config.clone());
        }

        // Add hooks
        let mut dataset_hooks = DATASET_HOOKS.lock();
        let entry = dataset_hooks.entry(dataset.into()).or_default();

        for hook in hooks {
            // Check if already attached for this hook
            let already_attached = entry
                .iter()
                .any(|h| h.plugin_name == plugin_name && h.hook == *hook);

            if !already_attached {
                entry.push(DatasetHook {
                    plugin_name: plugin_name.into(),
                    hook: *hook,
                    priority,
                });
            }
        }

        // Sort by priority
        entry.sort_by_key(|h| h.priority);

        Ok(())
    }

    /// Detach a plugin from a dataset.
    pub fn detach(dataset: &str, plugin_name: &str) -> Result<(), PluginError> {
        let mut hooks = DATASET_HOOKS.lock();

        if let Some(dataset_hooks) = hooks.get_mut(dataset) {
            dataset_hooks.retain(|h| h.plugin_name != plugin_name);
        }

        // Remove config
        let mut configs = PLUGIN_CONFIG.lock();
        let key = alloc::format!("{}:{}", dataset, plugin_name);
        configs.remove(&key);

        Ok(())
    }

    /// List hooks attached to a dataset.
    pub fn list_dataset_hooks(dataset: &str) -> Vec<(String, HookType)> {
        let hooks = DATASET_HOOKS.lock();
        hooks
            .get(dataset)
            .map(|hs| hs.iter().map(|h| (h.plugin_name.clone(), h.hook)).collect())
            .unwrap_or_default()
    }

    /// Execute all plugins for a specific hook.
    ///
    /// Data is passed through each plugin in sequence (chain execution).
    /// The output of one plugin becomes the input of the next.
    pub fn execute_hook(
        dataset: &str,
        hook: HookType,
        ctx: &PluginContext,
        data: &[u8],
    ) -> Result<Vec<u8>, PluginError> {
        let hooks = DATASET_HOOKS.lock();
        let dataset_hooks = match hooks.get(dataset) {
            Some(h) => h.clone(),
            None => return Ok(data.to_vec()),
        };
        drop(hooks);

        let mut result = data.to_vec();

        for dh in dataset_hooks.iter().filter(|h| h.hook == hook) {
            let mut plugins = PLUGINS.lock();

            if let Some(plugin) = plugins.get_mut(&dh.plugin_name) {
                // Load config for this plugin+dataset
                {
                    let configs = PLUGIN_CONFIG.lock();
                    let key = alloc::format!("{}:{}", dataset, dh.plugin_name);
                    if let Some(config) = configs.get(&key) {
                        let state = plugin.host_state_mut();
                        for (k, v) in config {
                            state.config.insert(k.clone(), v.clone());
                        }
                    }
                }

                // Execute plugin
                result = plugin.process(ctx, &result)?;
            }
        }

        Ok(result)
    }

    /// Execute a hook that doesn't modify data (notification only).
    ///
    /// Used for PostWrite, OnDelete, etc.
    pub fn execute_notify_hook(
        dataset: &str,
        hook: HookType,
        ctx: &PluginContext,
    ) -> Result<(), PluginError> {
        let _ = Self::execute_hook(dataset, hook, ctx, &[])?;
        Ok(())
    }

    /// Check if any plugins are attached for a hook.
    pub fn has_hooks(dataset: &str, hook: HookType) -> bool {
        let hooks = DATASET_HOOKS.lock();
        hooks
            .get(dataset)
            .map(|hs| hs.iter().any(|h| h.hook == hook))
            .unwrap_or(false)
    }

    /// Clear all plugins and hooks (for testing).
    #[cfg(test)]
    pub fn clear_all() {
        let mut plugins = PLUGINS.lock();
        // Clear plugins - can't drain() on MutexGuard, so just clear
        plugins.clear();

        let mut hooks = DATASET_HOOKS.lock();
        hooks.clear();

        let mut configs = PLUGIN_CONFIG.lock();
        configs.clear();
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// TESTS
// ═══════════════════════════════════════════════════════════════════════════════

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

    #[test]
    fn test_dataset_hook_struct() {
        let hook = DatasetHook {
            plugin_name: "test".into(),
            hook: HookType::PreWrite,
            priority: 0,
        };
        assert_eq!(hook.plugin_name, "test");
        assert_eq!(hook.hook, HookType::PreWrite);
    }

    #[test]
    fn test_list_plugins_empty() {
        // Note: This test may be affected by other tests running in parallel
        let plugins = PluginManager::list_plugins();
        // Just verify it doesn't panic
        let _ = plugins;
    }

    #[test]
    fn test_attach_nonexistent_plugin() {
        let result = PluginManager::attach(
            "dataset",
            "nonexistent",
            &[HookType::PreWrite],
            &BTreeMap::new(),
        );
        assert!(matches!(result, Err(PluginError::NotFound(_))));
    }

    #[test]
    fn test_has_hooks_empty() {
        assert!(!PluginManager::has_hooks(
            "nonexistent_dataset",
            HookType::PreWrite
        ));
    }

    #[test]
    fn test_list_dataset_hooks_empty() {
        let hooks = PluginManager::list_dataset_hooks("nonexistent");
        assert!(hooks.is_empty());
    }

    #[test]
    fn test_execute_hook_no_plugins() {
        let ctx = PluginContext::new("/path", "dataset", "write");
        let data = b"hello world";

        let result = PluginManager::execute_hook("empty_dataset", HookType::PreWrite, &ctx, data);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), data);
    }
}