use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

use serde::{Deserialize, Serialize};
use serde_json::Value;
use wasmtime::{Engine, Linker, Memory, Module, Store};

use crate::error::ButterflyBotError;
use crate::Result;

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ToolRuntime {
    #[default]
    Wasm,
}

impl ToolRuntime {
    pub fn as_str(&self) -> &'static str {
        "wasm"
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FilesystemPolicy {
    pub mode: Option<String>,
    #[serde(default)]
    pub allow: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NetworkPolicy {
    #[serde(default)]
    pub allow: Vec<String>,
    pub default_deny: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CapabilityPolicy {
    pub abi_version: Option<u32>,
    #[serde(default)]
    pub allow: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WasmToolConfig {
    pub module: Option<String>,
    pub entrypoint: Option<String>,
    pub timeout_ms: Option<u64>,
    pub fuel: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolSandboxConfig {
    #[serde(default)]
    pub wasm: WasmToolConfig,
    #[serde(default)]
    pub filesystem: FilesystemPolicy,
    #[serde(default)]
    pub network: NetworkPolicy,
    #[serde(default)]
    pub capabilities: CapabilityPolicy,
}

impl ToolSandboxConfig {
    pub fn is_capability_allowed(&self, capability: &str) -> bool {
        self.capabilities
            .allow
            .iter()
            .any(|allowed| allowed == capability)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SandboxSettings {
    #[serde(default)]
    pub tools: HashMap<String, ToolSandboxConfig>,
}

impl SandboxSettings {
    pub fn from_root_config(root: &Value) -> Self {
        let candidate = root
            .get("tools")
            .and_then(|v| v.get("settings"))
            .and_then(|v| v.get("sandbox"))
            .cloned();

        match candidate {
            Some(value) => serde_json::from_value(value).unwrap_or_default(),
            None => SandboxSettings::default(),
        }
    }

    pub fn execution_plan(&self, tool_name: &str) -> ExecutionPlan {
        let mut tool_config = self.tools.get(tool_name).cloned().unwrap_or_default();
        if tool_config.capabilities.allow.is_empty() {
            tool_config.capabilities.allow = Self::default_capabilities_for_tool(tool_name);
        }
        let reason = "wasm_only_policy".to_string();

        ExecutionPlan {
            runtime: ToolRuntime::Wasm,
            reason,
            tool_config,
        }
    }

    fn default_capabilities_for_tool(tool_name: &str) -> Vec<String> {
        match tool_name {
            "todo" => vec![
                "kv.sqlite.todo.create",
                "kv.sqlite.todo.create_many",
                "kv.sqlite.todo.list",
                "kv.sqlite.todo.complete",
                "kv.sqlite.todo.reopen",
                "kv.sqlite.todo.delete",
                "kv.sqlite.todo.clear",
                "kv.sqlite.todo.reorder",
            ],
            "tasks" => vec![
                "kv.sqlite.tasks.schedule",
                "kv.sqlite.tasks.list",
                "kv.sqlite.tasks.enable",
                "kv.sqlite.tasks.disable",
                "kv.sqlite.tasks.delete",
                "kv.sqlite.tasks.clear",
            ],
            "reminders" => vec![
                "kv.sqlite.reminders.create",
                "kv.sqlite.reminders.list",
                "kv.sqlite.reminders.complete",
                "kv.sqlite.reminders.delete",
                "kv.sqlite.reminders.snooze",
                "kv.sqlite.reminders.clear",
            ],
            "planning" => vec![
                "kv.sqlite.planning.create",
                "kv.sqlite.planning.list",
                "kv.sqlite.planning.get",
                "kv.sqlite.planning.update",
                "kv.sqlite.planning.delete",
                "kv.sqlite.planning.clear",
            ],
            "wakeup" => vec![
                "kv.sqlite.wakeup.create",
                "kv.sqlite.wakeup.list",
                "kv.sqlite.wakeup.enable",
                "kv.sqlite.wakeup.disable",
                "kv.sqlite.wakeup.delete",
            ],
            "coding" => vec!["coding.generate"],
            "mcp" => vec!["mcp.list_tools", "mcp.call"],
            "http_call" => vec!["http.request"],
            "github" => vec!["github.list_tools", "github.call_tool"],
            "zapier" => vec!["zapier.list_tools", "zapier.call_tool"],
            "search_internet" => vec!["search.internet"],
            "solana" => vec![
                "solana.wallet",
                "solana.balance",
                "solana.transfer",
                "solana.simulate_transfer",
                "solana.tx_status",
                "solana.tx_history",
            ],
            _ => Vec::new(),
        }
        .into_iter()
        .map(str::to_string)
        .collect()
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;
    use std::fs;

    use super::{SandboxSettings, ToolRuntime, ToolSandboxConfig, WasmRuntime};

    #[test]
    fn wasm_only_policy_routes_all_tools_to_wasm() {
        let settings = SandboxSettings::default();

        assert_eq!(settings.execution_plan("coding").runtime, ToolRuntime::Wasm);
        assert_eq!(settings.execution_plan("mcp").runtime, ToolRuntime::Wasm);
        assert_eq!(
            settings.execution_plan("http_call").runtime,
            ToolRuntime::Wasm
        );
        assert_eq!(settings.execution_plan("github").runtime, ToolRuntime::Wasm);
        assert_eq!(settings.execution_plan("zapier").runtime, ToolRuntime::Wasm);
        assert_eq!(
            settings.execution_plan("planning").runtime,
            ToolRuntime::Wasm
        );
        assert!(settings
            .execution_plan("solana")
            .tool_config
            .is_capability_allowed("solana.balance"));
        assert!(settings
            .execution_plan("reminders")
            .tool_config
            .is_capability_allowed("kv.sqlite.reminders.list"));
    }

    #[test]
    fn explicit_capability_allowlist_overrides_defaults() {
        let root = serde_json::json!({
            "tools": {
                "settings": {
                    "sandbox": {
                        "tools": {
                            "reminders": {
                                "capabilities": {
                                    "allow": ["clock.now_unix"]
                                }
                            }
                        }
                    }
                }
            }
        });

        let settings = SandboxSettings::from_root_config(&root);
        let plan = settings.execution_plan("reminders");
        assert!(plan.tool_config.is_capability_allowed("clock.now_unix"));
        assert!(!plan
            .tool_config
            .is_capability_allowed("kv.sqlite.reminders.list"));
    }

    #[test]
    fn explicit_runtime_override_cannot_bypass_wasm_only_policy() {
        let root = serde_json::json!({
            "tools": {
                "settings": {
                    "sandbox": {
                        "tools": {
                            "coding": {
                                "runtime": "native"
                            },
                            "github": {
                                "runtime": "wasm"
                            }
                        }
                    }
                }
            }
        });

        let settings = SandboxSettings::from_root_config(&root);
        assert_eq!(settings.execution_plan("coding").runtime, ToolRuntime::Wasm);
        assert_eq!(settings.execution_plan("github").runtime, ToolRuntime::Wasm);

        let root_all = serde_json::json!({
            "tools": {
                "settings": {
                    "sandbox": {
                        "tools": {
                            "github": {
                                "runtime": "wasm"
                            }
                        }
                    }
                }
            }
        });
        let settings_all = SandboxSettings::from_root_config(&root_all);
        assert_eq!(
            settings_all.execution_plan("github").runtime,
            ToolRuntime::Wasm
        );

        let root_off = serde_json::json!({
            "tools": {
                "settings": {
                    "sandbox": {}
                }
            }
        });
        let settings_off = SandboxSettings::from_root_config(&root_off);
        assert_eq!(
            settings_off.execution_plan("coding").runtime,
            ToolRuntime::Wasm
        );
        assert_eq!(
            settings_off.execution_plan("tasks").runtime,
            ToolRuntime::Wasm
        );
    }

    #[test]
    fn wasm_module_path_defaults_to_convention() {
        let cfg = ToolSandboxConfig::default();
        let path = WasmRuntime::resolve_module_path("coding", &cfg);
        assert!(
            path.ends_with("/coding_tool.wasm") || path.ends_with("\\coding_tool.wasm"),
            "unexpected module path: {path}"
        );
    }

    #[test]
    fn wasm_module_path_prefers_per_tool_when_generic_configured() {
        let generic = "./wasm/testdata/butterfly_bot_wasm_tool.wasm";
        let tool_name = "__test_reminders";
        let default = "./wasm/__test_reminders_tool.wasm";
        let _ = fs::create_dir_all("./wasm/testdata");
        let _ = fs::write(generic, b"generic");
        let _ = fs::write(default, b"default");

        let mut cfg = ToolSandboxConfig::default();
        cfg.wasm.module = Some(generic.to_string());

        let resolved = WasmRuntime::resolve_module_path(tool_name, &cfg);
        assert!(
            resolved.ends_with("/__test_reminders_tool.wasm")
                || resolved.ends_with("\\__test_reminders_tool.wasm"),
            "unexpected module path: {resolved}"
        );

        let _ = fs::remove_file(generic);
        let _ = fs::remove_file(default);
    }

    #[test]
    fn wasm_module_path_falls_back_to_default_when_configured_missing() {
        let tool_name = "__test_todo";
        let default = "./wasm/__test_todo_tool.wasm";
        let _ = fs::create_dir_all("./wasm");
        let _ = fs::write(default, b"default");

        let mut cfg = ToolSandboxConfig::default();
        cfg.wasm.module = Some("./wasm/does_not_exist.wasm".to_string());

        let resolved = WasmRuntime::resolve_module_path(tool_name, &cfg);
        assert!(
            resolved.ends_with("/__test_todo_tool.wasm")
                || resolved.ends_with("\\__test_todo_tool.wasm"),
            "unexpected module path: {resolved}"
        );

        let _ = fs::remove_file(default);
    }

    #[test]
    fn wasm_module_validation_rejects_non_wasm_bytes() {
        let path = "./wasm/__test_invalid_tool.wasm";
        let _ = fs::create_dir_all("./wasm");
        let _ = fs::write(path, b"default");

        let mut cfg = ToolSandboxConfig::default();
        cfg.wasm.module = Some(path.to_string());

        let err = WasmRuntime::validate_module_binary("__test_invalid", &cfg)
            .expect_err("expected invalid wasm header to fail");
        assert!(
            err.to_string().contains("missing wasm magic header"),
            "unexpected error: {err}"
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn wasm_zero_fuel_is_treated_as_unset() {
        let mut cfg = ToolSandboxConfig::default();
        cfg.wasm.fuel = Some(0);
        assert_eq!(WasmRuntime::resolve_fuel_limit(&cfg), None);

        cfg.wasm.fuel = Some(1);
        assert_eq!(WasmRuntime::resolve_fuel_limit(&cfg), Some(1));
    }

    #[test]
    fn wasm_module_validation_rejects_stub_marker() {
        let path = "./wasm/__test_stub_tool.wasm";
        let _ = fs::create_dir_all("./wasm");
        let mut bytes = vec![0x00, 0x61, 0x73, 0x6D];
        bytes.extend_from_slice(b"...stub responses...");
        let _ = fs::write(path, bytes);

        let mut cfg = ToolSandboxConfig::default();
        cfg.wasm.module = Some(path.to_string());

        let err = WasmRuntime::validate_module_binary("__test_stub", &cfg)
            .expect_err("expected stub module to fail");
        assert!(
            err.to_string().contains("placeholder stub"),
            "unexpected error: {err}"
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn reminders_wasm_execute_does_not_alloc_trap() {
        let cfg = ToolSandboxConfig::default();
        let params = json!({
            "action": "list",
            "status": "open",
            "user_id": "cli_user",
            "limit": 10
        });

        let result = WasmRuntime::execute_sync("reminders", &cfg, params);
        if let Err(err) = result {
            let msg = err.to_string();
            assert!(
                !msg.contains("WASM alloc failed"),
                "unexpected alloc trap from reminders wasm: {msg}"
            );
        }
    }

    #[test]
    fn capability_allowlist_parses_from_config() {
        let root = serde_json::json!({
            "tools": {
                "settings": {
                    "sandbox": {
                        "tools": {
                            "todo": {
                                "capabilities": {
                                    "abi_version": 1,
                                    "allow": ["kv.sqlite.todo.create", "clock.now_unix"]
                                }
                            }
                        }
                    }
                }
            }
        });

        let settings = SandboxSettings::from_root_config(&root);
        let plan = settings.execution_plan("todo");
        assert_eq!(plan.tool_config.capabilities.abi_version, Some(1));
        assert!(plan
            .tool_config
            .is_capability_allowed("kv.sqlite.todo.create"));
        assert!(!plan
            .tool_config
            .is_capability_allowed("kv.sqlite.todo.delete"));
    }

    #[test]
    fn capability_abi_version_mismatch_rejected() {
        let root = serde_json::json!({
            "tools": {
                "settings": {
                    "sandbox": {
                        "tools": {
                            "todo": {
                                "capabilities": {
                                    "abi_version": 2,
                                    "allow": ["kv.sqlite.todo.create"]
                                }
                            }
                        }
                    }
                }
            }
        });

        let settings = SandboxSettings::from_root_config(&root);
        let plan = settings.execution_plan("todo");
        let err = WasmRuntime::validate_capability_abi("todo", &plan.tool_config)
            .expect_err("expected abi_version mismatch to fail");
        assert!(err
            .to_string()
            .contains("unsupported capability ABI version"));
    }
}

#[derive(Debug, Clone)]
pub struct ExecutionPlan {
    pub runtime: ToolRuntime,
    pub reason: String,
    pub tool_config: ToolSandboxConfig,
}

#[derive(Debug, Default)]
pub struct WasmRuntime;

struct TimeoutCompletion {
    done: Arc<AtomicBool>,
}

impl Drop for TimeoutCompletion {
    fn drop(&mut self) {
        self.done.store(true, Ordering::Relaxed);
    }
}

impl WasmRuntime {
    const MAX_INPUT_BYTES: usize = 256 * 1024;
    const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6D];
    pub const SUPPORTED_CAPABILITY_ABI_VERSION: u32 = 1;

    fn module_filename(tool_name: &str) -> String {
        format!("{tool_name}_tool.wasm")
    }

    fn env_wasm_root() -> Option<PathBuf> {
        std::env::var("BUTTERFLY_BOT_WASM_DIR")
            .ok()
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty())
            .map(PathBuf::from)
    }

    #[cfg(target_os = "macos")]
    fn platform_wasm_roots() -> Vec<PathBuf> {
        let mut roots = Vec::new();

        if let Ok(home) = std::env::var("HOME") {
            let trimmed = home.trim();
            if !trimmed.is_empty() {
                roots.push(
                    PathBuf::from(trimmed)
                        .join("Library")
                        .join("Application Support")
                        .join("butterfly-bot")
                        .join("wasm"),
                );
            }
        }

        roots
    }

    #[cfg(not(target_os = "macos"))]
    fn platform_wasm_roots() -> Vec<PathBuf> {
        let mut roots = Vec::new();

        if let Ok(value) = std::env::var("XDG_DATA_HOME") {
            let trimmed = value.trim();
            if !trimmed.is_empty() {
                roots.push(PathBuf::from(trimmed).join("butterfly-bot").join("wasm"));
            }
        }

        if let Ok(home) = std::env::var("HOME") {
            let trimmed = home.trim();
            if !trimmed.is_empty() {
                roots.push(
                    PathBuf::from(trimmed)
                        .join(".local")
                        .join("share")
                        .join("butterfly-bot")
                        .join("wasm"),
                );
            }
        }

        roots
    }

    fn candidate_wasm_roots() -> Vec<PathBuf> {
        let mut roots = Vec::new();

        if let Some(root) = Self::env_wasm_root() {
            roots.push(root);
        }

        roots.extend(Self::platform_wasm_roots());

        let app_root = crate::runtime_paths::app_root();
        if !app_root.as_os_str().is_empty() {
            roots.push(app_root.join("wasm"));
        }

        roots.push(std::env::temp_dir().join("butterfly-bot").join("wasm"));

        if let Ok(exe) = std::env::current_exe() {
            if let Some(exe_dir) = exe.parent() {
                roots.push(exe_dir.join("wasm"));

                if let Some(parent) = exe_dir.parent() {
                    roots.push(parent.join("Resources").join("wasm"));
                    roots.push(parent.join("resources").join("wasm"));
                    roots.push(parent.join("share").join("butterfly-bot").join("wasm"));
                    roots.push(parent.join("lib").join("butterfly-bot").join("wasm"));
                }
            }
        }

        roots.push(PathBuf::from(".").join("wasm"));

        let mut deduped = Vec::new();
        for root in roots {
            if !deduped.contains(&root) {
                deduped.push(root);
            }
        }
        deduped
    }

    fn default_module_path(tool_name: &str) -> String {
        let file_name = Self::module_filename(tool_name);
        let fallback = PathBuf::from(".").join("wasm").join(&file_name);

        if let Ok(wasm_dir) = crate::wasm_bundle::ensure_bundled_wasm_tools() {
            let bundled = wasm_dir.join(&file_name);
            if bundled.exists() {
                return bundled.to_string_lossy().to_string();
            }
        }

        for root in Self::candidate_wasm_roots() {
            let candidate = root.join(&file_name);
            if candidate.exists() {
                return candidate.to_string_lossy().to_string();
            }
        }

        fallback.to_string_lossy().to_string()
    }

    fn resolve_module_path(tool_name: &str, config: &ToolSandboxConfig) -> String {
        let default_path = Self::default_module_path(tool_name);
        config
            .wasm
            .module
            .as_deref()
            .map(str::trim)
            .filter(|v| !v.is_empty())
            .map(|configured| {
                let configured_path = configured.to_string();
                let configured_file = Path::new(configured)
                    .file_name()
                    .and_then(|name| name.to_str())
                    .unwrap_or_default();

                if configured_file == "butterfly_bot_wasm_tool.wasm" {
                    return default_path.clone();
                }

                if Path::new(&configured_path).exists() {
                    configured_path
                } else if Path::new(&default_path).exists() {
                    default_path.clone()
                } else {
                    configured_path
                }
            })
            .unwrap_or(default_path)
    }

    fn resolve_entrypoint(config: &ToolSandboxConfig) -> String {
        config
            .wasm
            .entrypoint
            .as_deref()
            .map(str::trim)
            .filter(|v| !v.is_empty())
            .unwrap_or("execute")
            .to_string()
    }

    fn resolve_fuel_limit(config: &ToolSandboxConfig) -> Option<u64> {
        config.wasm.fuel.filter(|limit| *limit > 0)
    }

    pub fn validate_module_binary(tool_name: &str, config: &ToolSandboxConfig) -> Result<()> {
        let module_path = Self::resolve_module_path(tool_name, config);
        let path = Path::new(&module_path);

        if !path.exists() {
            return Err(ButterflyBotError::Runtime(format!(
                "WASM module path does not exist for tool '{tool_name}': {module_path}"
            )));
        }

        let mut file = File::open(path)
            .map_err(|e| ButterflyBotError::Runtime(format!("Failed to open wasm module: {e}")))?;
        let mut header = [0u8; 4];
        file.read_exact(&mut header).map_err(|e| {
            ButterflyBotError::Runtime(format!(
                "Failed to read wasm module header for tool '{tool_name}' ({module_path}): {e}"
            ))
        })?;

        if header != Self::WASM_MAGIC {
            return Err(ButterflyBotError::Runtime(format!(
                "Invalid wasm module for tool '{tool_name}' at {module_path}: missing wasm magic header"
            )));
        }

        let mut tail = Vec::new();
        file.read_to_end(&mut tail).map_err(|e| {
            ButterflyBotError::Runtime(format!(
                "Failed to inspect wasm module body for tool '{tool_name}' ({module_path}): {e}"
            ))
        })?;
        if tail
            .windows("stub responses".len())
            .any(|w| w == b"stub responses")
            || tail
                .windows("\"stub\":true".len())
                .any(|w| w == b"\"stub\":true")
        {
            return Err(ButterflyBotError::Runtime(format!(
                "WASM module for tool '{tool_name}' at {module_path} is a placeholder stub. Build/install a real WASM implementation before starting the daemon."
            )));
        }

        Ok(())
    }

    pub fn validate_capability_abi(tool_name: &str, config: &ToolSandboxConfig) -> Result<()> {
        if let Some(version) = config.capabilities.abi_version {
            if version != Self::SUPPORTED_CAPABILITY_ABI_VERSION {
                return Err(ButterflyBotError::Runtime(format!(
                    "Tool '{tool_name}' uses unsupported capability ABI version {version}; supported version is {}",
                    Self::SUPPORTED_CAPABILITY_ABI_VERSION
                )));
            }
        }

        Ok(())
    }

    fn split_ptr_len(packed: i64) -> Result<(i32, i32)> {
        let raw = packed as u64;
        let ptr = (raw >> 32) as u32;
        let len = (raw & 0xFFFF_FFFF) as u32;
        let ptr = i32::try_from(ptr).map_err(|_| {
            ButterflyBotError::Runtime("Invalid output pointer from wasm".to_string())
        })?;
        let len = i32::try_from(len).map_err(|_| {
            ButterflyBotError::Runtime("Invalid output length from wasm".to_string())
        })?;
        Ok((ptr, len))
    }

    fn ensure_range(memory: &Memory, store: &Store<()>, ptr: i32, len: i32) -> Result<()> {
        if ptr < 0 || len < 0 {
            return Err(ButterflyBotError::Runtime(
                "Negative pointer/length from wasm".to_string(),
            ));
        }
        let start = ptr as usize;
        let size = len as usize;
        let end = start
            .checked_add(size)
            .ok_or_else(|| ButterflyBotError::Runtime("WASM pointer overflow".to_string()))?;
        if end > memory.data_size(store) {
            return Err(ButterflyBotError::Runtime(
                "WASM memory range out of bounds".to_string(),
            ));
        }
        Ok(())
    }

    fn execute_sync(tool_name: &str, config: &ToolSandboxConfig, params: Value) -> Result<Value> {
        let mut module_path = Self::resolve_module_path(tool_name, config);

        if !Path::new(&module_path).exists()
            && (module_path.starts_with("./wasm/") || module_path.starts_with("wasm/"))
        {
            if let Ok(wasm_dir) = crate::wasm_bundle::ensure_bundled_wasm_tools() {
                let candidate = wasm_dir.join(Self::module_filename(tool_name));
                if candidate.exists() {
                    module_path = candidate.to_string_lossy().to_string();
                }
            }
        }

        tracing::info!(tool = %tool_name, module_path = %module_path, "Executing tool in WASM runtime");

        if !Path::new(&module_path).exists() {
            return Err(ButterflyBotError::Runtime(format!(
                "WASM module path does not exist for tool '{tool_name}': {module_path}"
            )));
        }

        let entrypoint = Self::resolve_entrypoint(config);
        let timeout_ms = config.wasm.timeout_ms.unwrap_or(0);
        let fuel_limit = Self::resolve_fuel_limit(config);

        let mut wasm_config = wasmtime::Config::new();
        if timeout_ms > 0 {
            wasm_config.epoch_interruption(true);
        }
        if fuel_limit.is_some() {
            wasm_config.consume_fuel(true);
        }

        let engine = Engine::new(&wasm_config).map_err(|e| {
            ButterflyBotError::Runtime(format!("Failed to initialize wasm engine: {e}"))
        })?;
        let module = Module::from_file(&engine, &module_path)
            .map_err(|e| ButterflyBotError::Runtime(format!("Failed to load wasm module: {e}")))?;
        let linker = Linker::new(&engine);
        let mut store = Store::new(&engine, ());

        if let Some(limit) = fuel_limit {
            store.set_fuel(limit).map_err(|e| {
                ButterflyBotError::Runtime(format!("Failed to apply wasm fuel limit: {e}"))
            })?;
        }

        let _timeout_guard = if timeout_ms > 0 {
            store.set_epoch_deadline(1);
            let done = Arc::new(AtomicBool::new(false));
            let done_for_thread = done.clone();
            let engine_for_thread = engine.clone();
            std::thread::spawn(move || {
                std::thread::sleep(Duration::from_millis(timeout_ms));
                if !done_for_thread.load(Ordering::Relaxed) {
                    engine_for_thread.increment_epoch();
                }
            });
            Some(TimeoutCompletion { done })
        } else {
            None
        };

        let instance = linker.instantiate(&mut store, &module).map_err(|e| {
            ButterflyBotError::Runtime(format!("Failed to instantiate wasm module: {e}"))
        })?;

        let memory = instance.get_memory(&mut store, "memory").ok_or_else(|| {
            ButterflyBotError::Runtime("WASM module missing exported memory".to_string())
        })?;

        let alloc = instance
            .get_typed_func::<i32, i32>(&mut store, "alloc")
            .map_err(|_| {
                ButterflyBotError::Runtime(
                    "WASM module missing `alloc(i32)->i32` export".to_string(),
                )
            })?;

        let dealloc = instance
            .get_typed_func::<(i32, i32), ()>(&mut store, "dealloc")
            .map_err(|_| {
                ButterflyBotError::Runtime(
                    "WASM module missing `dealloc(i32,i32)->()` export".to_string(),
                )
            })?;

        let exec = instance
            .get_typed_func::<(i32, i32), i64>(&mut store, &entrypoint)
            .map_err(|_| {
                ButterflyBotError::Runtime(format!(
                    "WASM module missing `{entrypoint}(i32,i32)->i64` export"
                ))
            })?;

        let input = serde_json::to_vec(&params)
            .map_err(|e| ButterflyBotError::Serialization(e.to_string()))?;
        if input.len() > Self::MAX_INPUT_BYTES {
            return Err(ButterflyBotError::Runtime(format!(
                "WASM tool input too large: {} bytes (max {})",
                input.len(),
                Self::MAX_INPUT_BYTES
            )));
        }

        let input_len = i32::try_from(input.len()).map_err(|_| {
            ButterflyBotError::Runtime("WASM input too large to pass as i32 length".to_string())
        })?;

        let input_ptr = alloc
            .call(&mut store, input_len)
            .map_err(|e| ButterflyBotError::Runtime(format!("WASM alloc failed: {e}")))?;

        Self::ensure_range(&memory, &store, input_ptr, input_len)?;
        memory
            .write(&mut store, input_ptr as usize, &input)
            .map_err(|e| ButterflyBotError::Runtime(format!("WASM memory write failed: {e}")))?;

        let packed = exec.call(&mut store, (input_ptr, input_len)).map_err(|e| {
            let msg = e.to_string();
            if timeout_ms > 0 && msg.to_ascii_lowercase().contains("interrupt") {
                ButterflyBotError::Runtime(format!(
                    "WASM tool '{tool_name}' timed out after {timeout_ms}ms"
                ))
            } else {
                ButterflyBotError::Runtime(format!("WASM tool execute failed: {msg}"))
            }
        })?;

        let (output_ptr, output_len) = Self::split_ptr_len(packed)?;
        Self::ensure_range(&memory, &store, output_ptr, output_len)?;

        let mut output = vec![0u8; output_len as usize];
        memory
            .read(&store, output_ptr as usize, &mut output)
            .map_err(|e| ButterflyBotError::Runtime(format!("WASM memory read failed: {e}")))?;

        let _ = dealloc.call(&mut store, (input_ptr, input_len));
        let _ = dealloc.call(&mut store, (output_ptr, output_len));

        if output.is_empty() {
            return Ok(serde_json::json!({}));
        }

        let value: Value = serde_json::from_slice(&output).map_err(|e| {
            ButterflyBotError::Runtime(format!("WASM output must be valid JSON: {e}"))
        })?;
        Ok(value)
    }

    pub async fn execute(
        &self,
        tool_name: &str,
        config: &ToolSandboxConfig,
        params: Value,
    ) -> Result<Value> {
        let tool_name = tool_name.to_string();
        let config = config.clone();
        tokio::task::spawn_blocking(move || Self::execute_sync(&tool_name, &config, params))
            .await
            .map_err(|e| ButterflyBotError::Runtime(format!("WASM task join error: {e}")))?
    }
}
