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

use async_trait::async_trait;
use tracing::{debug, info, warn};

use roboticus_agent::tools::{
    Tool, ToolContext, ToolError as AgentToolError, ToolRegistry, ToolResult as AgentToolResult,
};
use roboticus_core::config::PluginsConfig;
use roboticus_plugin_sdk::loader::discover_plugins;
use roboticus_plugin_sdk::registry::{PermissionPolicy, PluginRegistry};
use roboticus_plugin_sdk::script::ScriptPlugin;

/// Discover plugin manifests, instantiate `ScriptPlugin`s, and register them.
///
/// `plugin_env` is injected into every plugin script invocation. Callers should
/// populate it with workspace context (`ROBOTICUS_WORKSPACE`,
/// `ROBOTICUS_DELEGATION_DEPTH`, etc.) so plugins can discover allowed paths.
pub async fn init_plugin_registry(
    config: &PluginsConfig,
    plugin_env: HashMap<String, String>,
) -> Arc<PluginRegistry> {
    let registry = Arc::new(PluginRegistry::new(
        config.allow.clone(),
        config.deny.clone(),
        PermissionPolicy {
            strict: config.strict_permissions,
            allowed: config.allowed_permissions.clone(),
        },
    ));

    let plugins_dir = &config.dir;
    if !plugins_dir.exists() {
        debug!(dir = %plugins_dir.display(), "plugins directory does not exist, skipping discovery");
        return registry;
    }

    let discovered = match discover_plugins(plugins_dir) {
        Ok(d) => d,
        Err(e) => {
            warn!(error = %e, "failed to discover plugins");
            return registry;
        }
    };

    info!(count = discovered.len(), "discovered plugins");

    for dp in discovered {
        let name = dp.manifest.name.clone();
        let version = dp.manifest.version.clone();
        let tool_count = dp.manifest.tools.len();

        // ── Vet plugin integrity before registration ─────────────
        let report = dp.manifest.vet(&dp.dir);
        for w in &report.warnings {
            warn!(name = %name, warning = %w, "plugin vet warning");
        }
        if !report.is_ok() {
            for e in &report.errors {
                warn!(name = %name, error = %e, "plugin vet error");
            }
            warn!(
                name = %name,
                errors = report.errors.len(),
                "skipping plugin due to vet errors"
            );
            continue;
        }

        let timeout_secs = dp.manifest.timeout_seconds;
        let mut plugin = ScriptPlugin::new(dp.manifest, dp.dir).with_env(plugin_env.clone());
        if let Some(secs) = timeout_secs {
            plugin = plugin.with_timeout(std::time::Duration::from_secs(secs));
        }
        match registry.register(Box::new(plugin)).await {
            Ok(()) => {
                info!(
                    name = %name,
                    version = %version,
                    tools = tool_count,
                    "registered script plugin"
                );
            }
            Err(e) => {
                warn!(name = %name, error = %e, "failed to register plugin");
            }
        }
    }

    let init_errors = registry.init_all().await;
    if !init_errors.is_empty() {
        for err in &init_errors {
            warn!(error = %err, "plugin init error");
        }
    }

    let count = registry.plugin_count().await;
    info!(active = count, "plugin registry ready");

    registry
}

struct PluginTool {
    plugin_name: String,
    definition: roboticus_plugin_sdk::ToolDef,
    registry: Arc<PluginRegistry>,
}

impl PluginTool {
    fn new(
        plugin_name: String,
        definition: roboticus_plugin_sdk::ToolDef,
        registry: Arc<PluginRegistry>,
    ) -> Self {
        Self {
            plugin_name,
            definition,
            registry,
        }
    }
}

#[async_trait]
impl Tool for PluginTool {
    fn name(&self) -> &str {
        &self.definition.name
    }

    fn description(&self) -> &str {
        &self.definition.description
    }

    fn risk_level(&self) -> roboticus_core::RiskLevel {
        self.definition.risk_level
    }

    fn parameters_schema(&self) -> serde_json::Value {
        self.definition.parameters.clone()
    }

    fn paired_skill(&self) -> Option<&str> {
        self.definition.paired_skill.as_deref()
    }

    fn plugin_owner(&self) -> Option<&str> {
        Some(&self.plugin_name)
    }

    async fn execute(
        &self,
        params: serde_json::Value,
        _ctx: &ToolContext,
    ) -> std::result::Result<AgentToolResult, AgentToolError> {
        match self
            .registry
            .execute_plugin_tool(&self.plugin_name, &self.definition.name, &params)
            .await
        {
            Ok(result) if result.success => Ok(AgentToolResult {
                output: result.output,
                metadata: result.metadata,
            }),
            Ok(result) => Err(AgentToolError {
                message: if result.output.trim().is_empty() {
                    format!("plugin tool '{}' failed", self.definition.name)
                } else {
                    result.output
                },
            }),
            Err(e) => Err(AgentToolError {
                message: format!("plugin tool '{}' failed: {e}", self.definition.name),
            }),
        }
    }
}

/// Register all active plugin tools into the runtime ToolRegistry.
///
/// This bridges plugin-discovered tools into the same callable registry used by
/// the ReAct loop and tool schema export path. Duplicate tool names are skipped
/// so built-in tools retain precedence.
pub async fn register_plugin_tools(
    tool_registry: &mut ToolRegistry,
    plugin_registry: Arc<PluginRegistry>,
) {
    let plugin_tools = plugin_registry.list_all_tools().await;
    let discovered = plugin_tools.len();
    let mut registered = 0usize;
    let mut skipped = 0usize;

    for (plugin_name, tool_def) in plugin_tools {
        let tool_name = tool_def.name.clone();
        if tool_registry.get(&tool_name).is_some() {
            skipped += 1;
            warn!(
                plugin = %plugin_name,
                tool = %tool_name,
                "skipping plugin tool because tool name already exists"
            );
            continue;
        }
        tool_registry.register(Box::new(PluginTool::new(
            plugin_name,
            tool_def,
            Arc::clone(&plugin_registry),
        )));
        registered += 1;
    }

    info!(
        discovered,
        registered, skipped, "plugin tool bridge registration complete"
    );
}

#[cfg(test)]
mod tests {
    use super::*;
    use roboticus_agent::tools::ToolContext;
    use roboticus_core::InputAuthority;
    use std::fs;
    use std::path::PathBuf;

    #[tokio::test]
    async fn init_with_nonexistent_dir() {
        let config = PluginsConfig {
            dir: PathBuf::from("/nonexistent/plugins"),
            allow: vec![],
            deny: vec![],
            strict_permissions: false,
            allowed_permissions: vec![],
        };
        let registry = init_plugin_registry(&config, HashMap::new()).await;
        assert_eq!(registry.plugin_count().await, 0);
    }

    #[tokio::test]
    async fn init_with_empty_dir() {
        let dir = tempfile::tempdir().unwrap();
        let config = PluginsConfig {
            dir: dir.path().to_path_buf(),
            allow: vec![],
            deny: vec![],
            strict_permissions: false,
            allowed_permissions: vec![],
        };
        let registry = init_plugin_registry(&config, HashMap::new()).await;
        assert_eq!(registry.plugin_count().await, 0);
    }

    #[tokio::test]
    async fn init_discovers_and_registers_plugin() {
        let dir = tempfile::tempdir().unwrap();
        let plugin_dir = dir.path().join("hello-plugin");
        fs::create_dir(&plugin_dir).unwrap();
        fs::write(
            plugin_dir.join("plugin.toml"),
            r#"
name = "hello-plugin"
version = "0.1.0"
description = "A test plugin"

[[tools]]
name = "say_hello"
description = "Says hello"
"#,
        )
        .unwrap();
        fs::write(plugin_dir.join("say_hello.gosh"), "echo hello").unwrap();

        let config = PluginsConfig {
            dir: dir.path().to_path_buf(),
            allow: vec![],
            deny: vec![],
            strict_permissions: false,
            allowed_permissions: vec![],
        };
        let registry = init_plugin_registry(&config, HashMap::new()).await;
        assert_eq!(registry.plugin_count().await, 1);

        let plugins = registry.list_plugins().await;
        assert_eq!(plugins[0].name, "hello-plugin");
        assert_eq!(plugins[0].tools.len(), 1);
    }

    #[tokio::test]
    async fn deny_list_blocks_plugin() {
        let dir = tempfile::tempdir().unwrap();
        let plugin_dir = dir.path().join("blocked");
        fs::create_dir(&plugin_dir).unwrap();
        fs::write(
            plugin_dir.join("plugin.toml"),
            "name = \"blocked\"\nversion = \"1.0.0\"\n",
        )
        .unwrap();

        let config = PluginsConfig {
            dir: dir.path().to_path_buf(),
            allow: vec![],
            deny: vec!["blocked".into()],
            strict_permissions: false,
            allowed_permissions: vec![],
        };
        let registry = init_plugin_registry(&config, HashMap::new()).await;
        assert_eq!(registry.plugin_count().await, 0);
    }

    #[tokio::test]
    async fn init_respects_timeout_seconds() {
        let dir = tempfile::tempdir().unwrap();
        let plugin_dir = dir.path().join("slow-plugin");
        fs::create_dir(&plugin_dir).unwrap();
        fs::write(
            plugin_dir.join("plugin.toml"),
            r#"
name = "slow-plugin"
version = "0.1.0"
timeout_seconds = 300

[[tools]]
name = "slow_task"
description = "A long-running task"
"#,
        )
        .unwrap();
        fs::write(plugin_dir.join("slow_task.sh"), "#!/bin/sh\necho done").unwrap();

        let config = PluginsConfig {
            dir: dir.path().to_path_buf(),
            allow: vec![],
            deny: vec![],
            strict_permissions: false,
            allowed_permissions: vec![],
        };
        let registry = init_plugin_registry(&config, HashMap::new()).await;
        assert_eq!(registry.plugin_count().await, 1);
        // Verify the plugin registered and its tool works
        let result = registry
            .execute_tool("slow_task", &serde_json::json!({}))
            .await
            .unwrap();
        assert!(result.success);
    }

    #[tokio::test]
    async fn plugin_tool_execution() {
        let dir = tempfile::tempdir().unwrap();
        let plugin_dir = dir.path().join("echo-plugin");
        fs::create_dir(&plugin_dir).unwrap();
        fs::write(
            plugin_dir.join("plugin.toml"),
            r#"
name = "echo-plugin"
version = "0.1.0"
[[tools]]
name = "echo"
description = "Echoes input"
"#,
        )
        .unwrap();
        fs::write(
            plugin_dir.join("echo.sh"),
            "#!/bin/sh\necho $ROBOTICUS_INPUT",
        )
        .unwrap();

        let config = PluginsConfig {
            dir: dir.path().to_path_buf(),
            allow: vec![],
            deny: vec![],
            strict_permissions: false,
            allowed_permissions: vec![],
        };
        let registry = init_plugin_registry(&config, HashMap::new()).await;

        let result = registry
            .execute_tool("echo", &serde_json::json!({"msg": "hi"}))
            .await
            .unwrap();
        assert!(result.success);
        assert!(result.output.contains("msg"));
    }

    #[tokio::test]
    async fn register_plugin_tools_bridges_active_plugin_tool_into_runtime_registry() {
        let dir = tempfile::tempdir().unwrap();
        let plugin_dir = dir.path().join("bridge-plugin");
        fs::create_dir(&plugin_dir).unwrap();
        fs::write(
            plugin_dir.join("plugin.toml"),
            r#"
name = "bridge-plugin"
version = "0.1.0"
[[tools]]
name = "bridge_echo"
description = "Echoes ROBOTICUS_INPUT from plugin bridge"
"#,
        )
        .unwrap();
        fs::write(
            plugin_dir.join("bridge_echo.sh"),
            "#!/bin/sh\necho $ROBOTICUS_INPUT",
        )
        .unwrap();

        let config = PluginsConfig {
            dir: dir.path().to_path_buf(),
            allow: vec![],
            deny: vec![],
            strict_permissions: false,
            allowed_permissions: vec![],
        };
        let plugin_registry = init_plugin_registry(&config, HashMap::new()).await;
        let mut runtime_tools = ToolRegistry::new();

        register_plugin_tools(&mut runtime_tools, Arc::clone(&plugin_registry)).await;

        let bridged = runtime_tools.get("bridge_echo");
        assert!(
            bridged.is_some(),
            "plugin tool should be bridged into registry"
        );
        let ctx = ToolContext {
            session_id: "test-session".to_string(),
            agent_id: "test-agent".to_string(),
            agent_name: "Test Agent".to_string(),
            authority: InputAuthority::Creator,
            workspace_root: dir.path().to_path_buf(),
            tool_allowed_paths: vec![],
            channel: None,
            db: None,
            sandbox: roboticus_agent::tools::ToolSandboxSnapshot::default(),
        };
        let result = bridged
            .unwrap()
            .execute(serde_json::json!({"hello":"world"}), &ctx)
            .await
            .expect("bridged plugin tool should execute");
        assert!(result.output.contains("hello"));
        assert!(result.output.contains("world"));
    }
}
