//! Adapter that wraps a loaded WASM module as a `LifecyclePlugin`.
//!
//! The adapter owns the wasmtime Store + CachedExports and delegates
//! lifecycle method calls to the WASM guest via fuel-budgeted invocations.

use bext_plugin_api::lifecycle::LifecyclePlugin;
use bext_plugin_api::types::PluginManifest;

// ---------------------------------------------------------------------------
// Real adapter (with wasmtime)
// ---------------------------------------------------------------------------

#[cfg(feature = "wasmtime-runtime")]
pub(crate) mod real {
    use super::*;
    use bext_plugin_api::types::fuel;
    use std::sync::Mutex;

    /// Holds the wasmtime Store + cached export handles for a single WASM plugin.
    /// Wrapped in Mutex because Store is !Sync but LifecyclePlugin requires Sync.
    pub(crate) struct WasmState {
        pub store: wasmtime::Store<crate::runtime::real::HostState>,
        pub exports: crate::runtime::real::CachedExports,
    }

    /// A loaded WASM lifecycle plugin adapter.
    ///
    /// Implements `LifecyclePlugin` by calling guest exports through wasmtime.
    /// Each call refuels the store, marshals JSON args to guest memory, calls
    /// the export, and reads back any error.
    pub struct WasmLifecycleAdapter {
        pub(crate) manifest: PluginManifest,
        priority: u32,
        state: Mutex<WasmState>,
    }

    // SAFETY: `wasmtime::Store<T>` is `Send` when `T: Send`, but it is `!Sync`.
    // Our `HostState` is `Send` (it contains only owned data and a `StoreLimits`).
    // Therefore `Store<HostState>` is already `Send`, and `Mutex<WasmState>` would
    // normally auto-derive `Send + Sync`. However, because `Store` is `!Sync`,
    // the auto-derive for `Sync` on the containing struct fails. We implement
    // `Sync` manually because the `Mutex` serializes all access — no two threads
    // can access the `Store` concurrently. The `unsafe impl Send` is technically
    // redundant (Store<HostState> is Send), but kept for clarity alongside Sync.
    //
    // Invariants that must hold:
    // - `WasmState` is never accessed outside the `Mutex` lock guard.
    // - `HostState` fields remain `Send` (no `Rc`, raw pointers, etc.).
    unsafe impl Send for WasmLifecycleAdapter {}
    unsafe impl Sync for WasmLifecycleAdapter {}

    impl WasmLifecycleAdapter {
        pub(crate) fn new(manifest: PluginManifest, priority: u32, state: WasmState) -> Self {
            Self {
                manifest,
                priority,
                state: Mutex::new(state),
            }
        }

        /// Refuel and set epoch deadline for a call.
        fn refuel(store: &mut wasmtime::Store<crate::runtime::real::HostState>, fuel_budget: u64) {
            let _ = store.set_fuel(fuel_budget);
            store.set_epoch_deadline(100); // 5s wall-clock
        }

        /// Read remaining fuel from the store (for logging consumed fuel).
        fn remaining_fuel(store: &wasmtime::Store<crate::runtime::real::HostState>) -> u64 {
            store.get_fuel().unwrap_or(0)
        }

        /// Write a string to guest memory via alloc export.
        /// Takes separate borrows to avoid borrow conflicts with WasmState.
        fn write_to_guest_raw(
            store: &mut wasmtime::Store<crate::runtime::real::HostState>,
            memory: &wasmtime::Memory,
            alloc: &wasmtime::TypedFunc<i32, i32>,
            s: &str,
        ) -> Result<(i32, i32), String> {
            let bytes = s.as_bytes();
            let len = bytes.len() as i32;
            let ptr = alloc
                .call(&mut *store, len)
                .map_err(|e| format!("alloc({len}): {e}"))?;
            let mem_data = memory.data_mut(&mut *store);
            let start = ptr as usize;
            let end = start + bytes.len();
            if end > mem_data.len() {
                return Err("alloc returned OOB pointer".into());
            }
            mem_data[start..end].copy_from_slice(bytes);
            Ok((ptr, len))
        }
    }

    impl LifecyclePlugin for WasmLifecycleAdapter {
        fn name(&self) -> &str {
            &self.manifest.name
        }

        fn priority(&self) -> u32 {
            self.priority
        }

        fn on_server_start(&self, config_json: &str) -> Result<(), String> {
            let start = std::time::Instant::now();
            let mut guard = self
                .state
                .lock()
                .map_err(|e| format!("lock poisoned: {e}"))?;
            let ex = guard.exports.clone(); // Copy the whole struct (all fields are Copy)
            let budget = fuel::ON_SERVER_START;
            Self::refuel(&mut guard.store, budget);
            if let Some(f) = ex.on_server_start {
                let (ptr, len) =
                    Self::write_to_guest_raw(&mut guard.store, &ex.memory, &ex.alloc, config_json)?;
                f.call(&mut guard.store, (ptr, len))
                    .map_err(|e| format!("on_server_start: {e}"))?;
            }
            let consumed = budget.saturating_sub(Self::remaining_fuel(&guard.store));
            tracing::debug!(
                plugin = %self.manifest.name,
                hook = "on_server_start",
                elapsed_us = start.elapsed().as_micros() as u64,
                fuel_consumed = consumed,
                "lifecycle hook completed"
            );
            Ok(())
        }

        fn on_server_stop(&self) -> Result<(), String> {
            let start = std::time::Instant::now();
            let mut guard = self
                .state
                .lock()
                .map_err(|e| format!("lock poisoned: {e}"))?;
            let ex = guard.exports.clone();
            let budget = fuel::CLEANUP;
            Self::refuel(&mut guard.store, budget);
            if let Some(f) = ex.on_server_stop {
                f.call(&mut guard.store, ())
                    .map_err(|e| format!("on_server_stop: {e}"))?;
            }
            let consumed = budget.saturating_sub(Self::remaining_fuel(&guard.store));
            tracing::debug!(
                plugin = %self.manifest.name,
                hook = "on_server_stop",
                elapsed_us = start.elapsed().as_micros() as u64,
                fuel_consumed = consumed,
                "lifecycle hook completed"
            );
            Ok(())
        }

        fn on_request_complete(&self, event_json: &str) -> Result<(), String> {
            let start = std::time::Instant::now();
            let mut guard = self
                .state
                .lock()
                .map_err(|e| format!("lock poisoned: {e}"))?;
            let ex = guard.exports.clone();
            let budget = fuel::ON_REQUEST_COMPLETE;
            Self::refuel(&mut guard.store, budget);
            if let Some(f) = ex.on_request_complete {
                let (ptr, len) =
                    Self::write_to_guest_raw(&mut guard.store, &ex.memory, &ex.alloc, event_json)?;
                f.call(&mut guard.store, (ptr, len))
                    .map_err(|e| format!("on_request_complete: {e}"))?;
            }
            let consumed = budget.saturating_sub(Self::remaining_fuel(&guard.store));
            tracing::debug!(
                plugin = %self.manifest.name,
                hook = "on_request_complete",
                elapsed_us = start.elapsed().as_micros() as u64,
                fuel_consumed = consumed,
                "lifecycle hook completed"
            );
            Ok(())
        }

        fn on_cache_write(&self, key: &str, tags_json: &str) -> Result<(), String> {
            let start = std::time::Instant::now();
            let mut guard = self
                .state
                .lock()
                .map_err(|e| format!("lock poisoned: {e}"))?;
            let ex = guard.exports.clone();
            let budget = fuel::ON_CACHE_WRITE;
            Self::refuel(&mut guard.store, budget);
            if let Some(f) = ex.on_cache_write {
                let (kp, kl) =
                    Self::write_to_guest_raw(&mut guard.store, &ex.memory, &ex.alloc, key)?;
                let (tp, tl) =
                    Self::write_to_guest_raw(&mut guard.store, &ex.memory, &ex.alloc, tags_json)?;
                f.call(&mut guard.store, (kp, kl, tp, tl))
                    .map_err(|e| format!("on_cache_write: {e}"))?;
            }
            let consumed = budget.saturating_sub(Self::remaining_fuel(&guard.store));
            tracing::debug!(
                plugin = %self.manifest.name,
                hook = "on_cache_write",
                elapsed_us = start.elapsed().as_micros() as u64,
                fuel_consumed = consumed,
                "lifecycle hook completed"
            );
            Ok(())
        }

        fn on_cache_invalidate(&self, pattern: &str, count: u32) -> Result<(), String> {
            let start = std::time::Instant::now();
            let mut guard = self
                .state
                .lock()
                .map_err(|e| format!("lock poisoned: {e}"))?;
            let ex = guard.exports.clone();
            let budget = fuel::ON_CACHE_INVALIDATE;
            Self::refuel(&mut guard.store, budget);
            if let Some(f) = ex.on_cache_invalidate {
                let (ptr, len) =
                    Self::write_to_guest_raw(&mut guard.store, &ex.memory, &ex.alloc, pattern)?;
                f.call(&mut guard.store, (ptr, len, count as i32))
                    .map_err(|e| format!("on_cache_invalidate: {e}"))?;
            }
            let consumed = budget.saturating_sub(Self::remaining_fuel(&guard.store));
            tracing::debug!(
                plugin = %self.manifest.name,
                hook = "on_cache_invalidate",
                elapsed_us = start.elapsed().as_micros() as u64,
                fuel_consumed = consumed,
                "lifecycle hook completed"
            );
            Ok(())
        }

        fn on_reload(&self) -> Result<(), String> {
            let start = std::time::Instant::now();
            let mut guard = self
                .state
                .lock()
                .map_err(|e| format!("lock poisoned: {e}"))?;
            let ex = guard.exports.clone();
            let budget = fuel::ON_RELOAD;
            Self::refuel(&mut guard.store, budget);
            if let Some(f) = ex.on_reload {
                f.call(&mut guard.store, ())
                    .map_err(|e| format!("on_reload: {e}"))?;
            }
            let consumed = budget.saturating_sub(Self::remaining_fuel(&guard.store));
            tracing::debug!(
                plugin = %self.manifest.name,
                hook = "on_reload",
                elapsed_us = start.elapsed().as_micros() as u64,
                fuel_consumed = consumed,
                "lifecycle hook completed"
            );
            Ok(())
        }

        fn cleanup(&self) -> Result<(), String> {
            let start = std::time::Instant::now();
            let mut guard = self
                .state
                .lock()
                .map_err(|e| format!("lock poisoned: {e}"))?;
            let ex = guard.exports.clone();
            let budget = fuel::CLEANUP;
            Self::refuel(&mut guard.store, budget);
            if let Some(f) = ex.cleanup {
                f.call(&mut guard.store, ())
                    .map_err(|e| format!("cleanup: {e}"))?;
            }
            let consumed = budget.saturating_sub(Self::remaining_fuel(&guard.store));
            tracing::debug!(
                plugin = %self.manifest.name,
                hook = "cleanup",
                elapsed_us = start.elapsed().as_micros() as u64,
                fuel_consumed = consumed,
                "lifecycle hook completed"
            );
            Ok(())
        }
    }
}

// ---------------------------------------------------------------------------
// Stub adapter (when wasmtime-runtime feature is not enabled)
// ---------------------------------------------------------------------------

#[cfg(not(feature = "wasmtime-runtime"))]
pub(crate) mod stub {
    use super::*;

    pub struct WasmLifecycleAdapter {
        pub(crate) manifest: PluginManifest,
        priority: u32,
    }

    impl WasmLifecycleAdapter {
        pub fn new(manifest: PluginManifest, priority: u32) -> Self {
            Self { manifest, priority }
        }
    }

    impl LifecyclePlugin for WasmLifecycleAdapter {
        fn name(&self) -> &str {
            &self.manifest.name
        }
        fn priority(&self) -> u32 {
            self.priority
        }
        // All methods use default Ok(()) — stub mode
    }
}

#[cfg(feature = "wasmtime-runtime")]
pub use real::WasmLifecycleAdapter;
#[cfg(not(feature = "wasmtime-runtime"))]
pub use stub::WasmLifecycleAdapter;

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

#[cfg(all(test, not(feature = "wasmtime-runtime")))]
mod tests {
    use super::*;
    use bext_plugin_api::types::PluginCapability;

    #[test]
    fn stub_adapter_name_and_priority() {
        let manifest = PluginManifest {
            name: "test-plugin".into(),
            version: "1.0.0".into(),
            description: "A test plugin".into(),
            capabilities: vec![PluginCapability::Lifecycle],
            provides_capabilities: Vec::new(),
            requires_capabilities: Vec::new(),
        };
        let adapter = WasmLifecycleAdapter::new(manifest, 500);
        assert_eq!(adapter.name(), "test-plugin");
        assert_eq!(adapter.priority(), 500);
    }

    #[test]
    fn stub_hooks_are_noop() {
        let manifest = PluginManifest {
            name: "stub-test".into(),
            version: "0.1.0".into(),
            description: String::new(),
            capabilities: vec![],
            provides_capabilities: Vec::new(),
            requires_capabilities: Vec::new(),
        };
        let adapter = WasmLifecycleAdapter::new(manifest, 1000);

        // All hooks should return Ok(())
        assert!(adapter.on_server_start("{}").is_ok());
        assert!(adapter.on_server_stop().is_ok());
        assert!(adapter.on_request_complete("{}").is_ok());
        assert!(adapter.on_cache_write("/page/1", "[]").is_ok());
        assert!(adapter.on_cache_invalidate("/page/*", 5).is_ok());
        assert!(adapter.on_reload().is_ok());
        assert!(adapter.cleanup().is_ok());
    }
}

#[cfg(all(test, feature = "wasmtime-runtime"))]
mod wasmtime_tests {
    use super::real::*;
    use crate::runtime::real::{CachedExports, HostState};
    use crate::sandbox::PluginSandbox;
    use bext_plugin_api::lifecycle::LifecyclePlugin;
    use bext_plugin_api::types::{PluginManifest, WasmPluginPermissions};

    /// Build a minimal WASM module (WAT) that exports alloc, init, memory,
    /// and optionally lifecycle hooks. The module uses a simple bump allocator.
    fn minimal_wat(hooks: &[&str]) -> String {
        // The module has 1 page of memory and a bump allocator.
        // init returns a pointer to a hardcoded JSON manifest.
        // Each lifecycle hook is a no-op (just returns).
        let manifest_json = r#"{"name":"test-wasm","version":"0.1.0","description":"test","capabilities":["lifecycle"]}"#;
        let manifest_bytes: Vec<u8> = manifest_json.as_bytes().to_vec();
        let manifest_len = manifest_bytes.len();
        // We'll store the manifest at offset 1024 and the bump pointer at offset 0..3
        let manifest_offset = 1024;
        let initial_bump = manifest_offset + manifest_len;

        let mut data_hex = String::new();
        for b in &manifest_bytes {
            data_hex.push_str(&format!("\\{b:02x}"));
        }

        // Build bump pointer initial value (i32 LE) at offset 0
        let bump_bytes = (initial_bump as u32).to_le_bytes();
        let mut bump_hex = String::new();
        for b in &bump_bytes {
            bump_hex.push_str(&format!("\\{b:02x}"));
        }

        let mut hook_exports = String::new();
        for hook in hooks {
            match *hook {
                "on_server_start" => {
                    hook_exports.push_str(
                        "  (func $on_server_start (param i32 i32))\n  (export \"on_server_start\" (func $on_server_start))\n"
                    );
                }
                "on_server_stop" => {
                    hook_exports.push_str(
                        "  (func $on_server_stop)\n  (export \"on_server_stop\" (func $on_server_stop))\n"
                    );
                }
                "on_request_complete" => {
                    hook_exports.push_str(
                        "  (func $on_request_complete (param i32 i32))\n  (export \"on_request_complete\" (func $on_request_complete))\n"
                    );
                }
                "on_cache_write" => {
                    hook_exports.push_str(
                        "  (func $on_cache_write (param i32 i32 i32 i32))\n  (export \"on_cache_write\" (func $on_cache_write))\n"
                    );
                }
                "on_cache_invalidate" => {
                    hook_exports.push_str(
                        "  (func $on_cache_invalidate (param i32 i32 i32))\n  (export \"on_cache_invalidate\" (func $on_cache_invalidate))\n"
                    );
                }
                "on_reload" => {
                    hook_exports.push_str(
                        "  (func $on_reload)\n  (export \"on_reload\" (func $on_reload))\n",
                    );
                }
                "cleanup" => {
                    hook_exports
                        .push_str("  (func $cleanup)\n  (export \"cleanup\" (func $cleanup))\n");
                }
                _ => {}
            }
        }

        format!(
            r#"(module
  (memory (export "memory") 1)
  ;; Bump pointer at offset 0
  (data (i32.const 0) "{bump_hex}")
  ;; Manifest JSON at offset {manifest_offset}
  (data (i32.const {manifest_offset}) "{data_hex}")

  ;; Bump allocator: read ptr from offset 0, advance by n, store back
  (func $alloc (param $n i32) (result i32)
    (local $ptr i32)
    (local.set $ptr (i32.load (i32.const 0)))
    (i32.store (i32.const 0) (i32.add (local.get $ptr) (local.get $n)))
    (local.get $ptr)
  )
  (export "alloc" (func $alloc))

  ;; init() -> (ptr, len) returning manifest
  (func $init (result i32 i32)
    (i32.const {manifest_offset})
    (i32.const {manifest_len})
  )
  (export "init" (func $init))

{hook_exports})"#
        )
    }

    /// Create a WasmLifecycleAdapter from WAT source with given hooks.
    fn create_adapter(hooks: &[&str]) -> WasmLifecycleAdapter {
        use bext_plugin_api::types::fuel;
        use wasmtime::*;

        let wat = minimal_wat(hooks);

        let mut config = Config::new();
        config.consume_fuel(true);
        config.epoch_interruption(true);
        let engine = Engine::new(&config).expect("engine");

        let mut linker: Linker<HostState> = Linker::new(&engine);
        // Register a minimal bext.log so the module can link if needed
        linker
            .func_wrap(
                "bext",
                "log",
                |_: Caller<'_, HostState>, _: i32, _: i32, _: i32, _: i32| {},
            )
            .unwrap();

        let module = Module::new(&engine, &wat).expect("compile WAT");

        let sandbox = PluginSandbox::new(
            "test-wasm".into(),
            WasmPluginPermissions::default(),
            std::path::Path::new("/tmp/bext-test-plugins"),
        );
        let host_state = HostState {
            sandbox,
            plugin_config: serde_json::Value::Null,
            memory: None,
            limiter: StoreLimitsBuilder::new()
                .memory_size(64 * 1024 * 1024)
                .table_elements(10_000)
                .instances(1)
                .build(),
        };

        let mut store = Store::new(&engine, host_state);
        store.limiter(|state| &mut state.limiter);
        store.set_fuel(fuel::ON_SERVER_START).unwrap();
        store.set_epoch_deadline(200);

        let instance = linker
            .instantiate(&mut store, &module)
            .expect("instantiate");
        let exports = CachedExports::resolve(&instance, &mut store).expect("resolve exports");
        store.data_mut().memory = Some(exports.memory);

        // Call init to verify it works
        let (ptr, len) = exports.init.call(&mut store, ()).expect("init");
        let data = exports.memory.data(&store);
        let json =
            std::str::from_utf8(&data[ptr as usize..(ptr as usize + len as usize)]).expect("utf8");
        let manifest: PluginManifest = serde_json::from_str(json).expect("manifest");

        let wasm_state = WasmState { store, exports };
        WasmLifecycleAdapter::new(manifest, 500, wasm_state)
    }

    #[test]
    fn adapter_name_and_priority() {
        let adapter = create_adapter(&[]);
        assert_eq!(adapter.name(), "test-wasm");
        assert_eq!(adapter.priority(), 500);
    }

    #[test]
    fn hooks_without_exports_are_noop() {
        // Create adapter with NO lifecycle hook exports
        let adapter = create_adapter(&[]);

        // All hooks should succeed (no-op when export doesn't exist)
        assert!(adapter.on_server_start("{}").is_ok());
        assert!(adapter.on_server_stop().is_ok());
        assert!(adapter
            .on_request_complete(r#"{"path":"/","status":200}"#)
            .is_ok());
        assert!(adapter.on_cache_write("/page/1", r#"["tag1"]"#).is_ok());
        assert!(adapter.on_cache_invalidate("/page/*", 3).is_ok());
        assert!(adapter.on_reload().is_ok());
        assert!(adapter.cleanup().is_ok());
    }

    #[test]
    fn hooks_with_exports_succeed() {
        // Create adapter WITH all lifecycle hook exports
        let adapter = create_adapter(&[
            "on_server_start",
            "on_server_stop",
            "on_request_complete",
            "on_cache_write",
            "on_cache_invalidate",
            "on_reload",
            "cleanup",
        ]);

        assert!(adapter.on_server_start(r#"{"debug":true}"#).is_ok());
        assert!(adapter.on_server_stop().is_ok());
        assert!(adapter
            .on_request_complete(r#"{"path":"/test","status":200}"#)
            .is_ok());
        assert!(adapter
            .on_cache_write("/page/1", r#"["blog","page"]"#)
            .is_ok());
        assert!(adapter.on_cache_invalidate("/page/*", 5).is_ok());
        assert!(adapter.on_reload().is_ok());
        assert!(adapter.cleanup().is_ok());
    }

    #[test]
    fn fuel_is_consumed_by_hook_calls() {
        // Create adapter with a hook and verify fuel is consumed
        let adapter = create_adapter(&["on_request_complete"]);

        // Call the hook — the no-op WASM function consumes minimal fuel
        // but alloc + memory write do consume some
        assert!(adapter.on_request_complete(r#"{"path":"/"}"#).is_ok());

        // We can't directly inspect fuel from outside, but the hook
        // succeeded within budget — that's the key assertion.
        // A second call also works (refueling happens each time).
        assert!(adapter.on_request_complete(r#"{"path":"/second"}"#).is_ok());
    }

    #[test]
    fn multiple_hooks_called_sequentially() {
        let adapter = create_adapter(&[
            "on_server_start",
            "on_request_complete",
            "on_cache_write",
            "cleanup",
        ]);

        // Simulate a full lifecycle
        assert!(adapter.on_server_start(r#"{"port":3000}"#).is_ok());
        for i in 0..10 {
            assert!(adapter
                .on_request_complete(&format!(r#"{{"path":"/page/{i}","status":200}}"#))
                .is_ok());
        }
        assert!(adapter.on_cache_write("/page/5", r#"["page"]"#).is_ok());
        assert!(adapter.cleanup().is_ok());
    }
}
