//! `WasmtimeRuntime` — Cranelift JIT WASM plugin runtime (ADR-128 P2).
//!
//! # Design
//!
//! Each `.wasm` binary is compiled once per process by a shared [`Engine`].
//! Every call to [`WasmtimeRuntime::load_wasm`] creates a new [`Store`] so
//! plugins are fully isolated — one plugin cannot read another's linear memory.
//!
//! The 4 host imports the WASM module receives are registered via a [`Linker`]:
//!
//! | Import | Signature | Description |
//! |--------|-----------|-------------|
//! | `hc_state_get` | `(i32,i32,i32,i32)→i32` | Read entity state into guest buffer |
//! | `hc_state_set` | `(i32,i32,i32,i32,i32,i32)→i32` | Write entity state from guest buffer |
//! | `hc_state_subscribe` | `(i32,i32)→i32` | Subscribe to state-changed events |
//! | `hc_log` | `(i32,i32,i32)→()` | Structured log output from plugin |
//!
//! WASI is **not** imported — plugins have no filesystem or network access.
//!
//! # Memory convention
//!
//! The guest exports `alloc(size: i32) → i32` and `dealloc(ptr: i32, size: i32)`.
//! The host calls `alloc` before writing a buffer into guest memory, then calls
//! `dealloc` when done. See [`host_abi`] for the full ABI spec.

use std::sync::{Arc, Mutex};

use homecore::HomeCore;
use wasmtime::{Engine, Linker, Module, Store};

use crate::error::PluginError;
use crate::host_abi::{LogLevel, StateChangedEventJson, MAX_ABI_BUFFER_BYTES};

// ── Store data ─────────────────────────────────────────────────────────────

/// Per-plugin state stored inside the Wasmtime [`Store`].
///
/// Wasmtime's `Store<T>` exposes `T` to host functions via `caller.data()`.
/// We store the `HomeCore` handle and a list of subscribed entity IDs here.
pub struct PluginStoreData {
    pub hc: HomeCore,
    pub subscriptions: Vec<String>,
}

// ── WasmtimeRuntime ────────────────────────────────────────────────────────

/// Wasmtime-backed WASM plugin runtime (Cranelift JIT on Pi 5 and x86_64).
///
/// One `Engine` is shared across all plugins for module caching. Each plugin
/// gets its own isolated `Store`.
pub struct WasmtimeRuntime {
    engine: Engine,
}

impl WasmtimeRuntime {
    /// Create a new runtime with default Cranelift config.
    pub fn new() -> Result<Self, PluginError> {
        let engine = Engine::default();
        Ok(Self { engine })
    }

    /// Compile and instantiate a WASM plugin from raw bytes.
    ///
    /// Returns a [`WasmPlugin`] handle that owns the `Store` and the
    /// `Instance`. The handle can be used to call into the WASM module.
    pub fn load_wasm(
        &self,
        wasm_bytes: &[u8],
        hc: HomeCore,
    ) -> Result<WasmPlugin, PluginError> {
        let module = Module::new(&self.engine, wasm_bytes)
            .map_err(|e| PluginError::RuntimeError(format!("WASM compile: {e}")))?;

        let mut linker: Linker<PluginStoreData> = Linker::new(&self.engine);
        register_host_imports(&mut linker)?;

        let store_data = PluginStoreData {
            hc,
            subscriptions: Vec::new(),
        };
        let mut store = Store::new(&self.engine, store_data);

        let instance = linker
            .instantiate(&mut store, &module)
            .map_err(|e| PluginError::RuntimeError(format!("WASM instantiate: {e}")))?;

        Ok(WasmPlugin {
            inner: Arc::new(Mutex::new((store, instance))),
        })
    }
}

impl Default for WasmtimeRuntime {
    fn default() -> Self {
        Self::new().expect("default Wasmtime engine should not fail")
    }
}

// ── Host import registration ───────────────────────────────────────────────

/// Register the 4 host imports every HOMECORE plugin can call.
fn register_host_imports(
    linker: &mut Linker<PluginStoreData>,
) -> Result<(), PluginError> {
    register_hc_state_get(linker)?;
    register_hc_state_set(linker)?;
    register_hc_state_subscribe(linker)?;
    register_hc_log(linker)?;
    Ok(())
}

/// `hc_state_get(key_ptr: i32, key_len: i32, out_ptr: i32, out_cap: i32) → i32`
///
/// Reads the current state for the entity whose UTF-8 ID is in the guest
/// buffer at `[key_ptr, key_ptr+key_len)`. Writes the JSON-encoded state
/// into `[out_ptr, out_ptr+out_cap)`. Returns the number of bytes written,
/// or -1 if the entity is not found, or -2 if `out_cap` is too small.
fn register_hc_state_get(
    linker: &mut Linker<PluginStoreData>,
) -> Result<(), PluginError> {
    linker
        .func_wrap(
            "env",
            "hc_state_get",
            |mut caller: wasmtime::Caller<'_, PluginStoreData>,
             key_ptr: i32,
             key_len: i32,
             out_ptr: i32,
             out_cap: i32|
             -> i32 {
                // Phase 1: read the entity key from guest memory.
                let key: String = {
                    let mem = match caller.get_export("memory") {
                        Some(wasmtime::Extern::Memory(m)) => m,
                        _ => return -1,
                    };
                    match read_str(mem.data(&caller), key_ptr, key_len) {
                        Some(k) => k.to_owned(),
                        None => return -1,
                    }
                };

                // Phase 2: look up state and build JSON (no borrow on caller).
                let entity_id = match homecore::EntityId::parse(&key) {
                    Ok(id) => id,
                    Err(_) => return -1,
                };
                let json_bytes: Vec<u8> = {
                    let state_arc = match caller.data().hc.states().get(&entity_id) {
                        Some(s) => s,
                        None => return -1,
                    };
                    match serde_json::to_vec(&*state_arc) {
                        Ok(v) => v,
                        Err(_) => return -1,
                    }
                };

                if json_bytes.len() > out_cap as usize {
                    return -2;
                }

                // Phase 3: write JSON back into guest memory.
                let mem = match caller.get_export("memory") {
                    Some(wasmtime::Extern::Memory(m)) => m,
                    _ => return -1,
                };
                let end = out_ptr as usize + json_bytes.len();
                let out = match mem.data_mut(&mut caller).get_mut(out_ptr as usize..end) {
                    Some(s) => s,
                    None => return -1,
                };
                out.copy_from_slice(&json_bytes);
                json_bytes.len() as i32
            },
        )
        .map_err(|e| PluginError::RuntimeError(format!("register hc_state_get: {e}")))?;
    Ok(())
}

/// `hc_state_set(eid_ptr,eid_len,state_ptr,state_len,attrs_ptr,attrs_len) → i32`
///
/// Sets the state for the entity whose UTF-8 ID is at `[eid_ptr,eid_ptr+eid_len)`.
/// The new state string is at `[state_ptr,state_ptr+state_len)`.
/// The attributes JSON is at `[attrs_ptr,attrs_ptr+attrs_len)`.
/// Returns 0 on success, negative on error.
fn register_hc_state_set(
    linker: &mut Linker<PluginStoreData>,
) -> Result<(), PluginError> {
    linker
        .func_wrap(
            "env",
            "hc_state_set",
            |mut caller: wasmtime::Caller<'_, PluginStoreData>,
             eid_ptr: i32,
             eid_len: i32,
             state_ptr: i32,
             state_len: i32,
             attrs_ptr: i32,
             attrs_len: i32|
             -> i32 {
                // Read all strings from guest memory in one borrow.
                let (eid, new_state, attrs_str) = {
                    let mem = match caller.get_export("memory") {
                        Some(wasmtime::Extern::Memory(m)) => m,
                        _ => return -1,
                    };
                    let data = mem.data(&caller);
                    let eid = match read_str(data, eid_ptr, eid_len) {
                        Some(s) => s.to_owned(),
                        None => return -1,
                    };
                    let new_state = match read_str(data, state_ptr, state_len) {
                        Some(s) => s.to_owned(),
                        None => return -1,
                    };
                    let attrs_str = read_str(data, attrs_ptr, attrs_len)
                        .unwrap_or("{}")
                        .to_owned();
                    (eid, new_state, attrs_str)
                };

                let entity_id = match homecore::EntityId::parse(&eid) {
                    Ok(id) => id,
                    Err(_) => return -2,
                };
                let attrs: serde_json::Value =
                    serde_json::from_str(&attrs_str).unwrap_or(serde_json::json!({}));

                caller
                    .data()
                    .hc
                    .states()
                    .set(entity_id, new_state, attrs, homecore::Context::new());
                0
            },
        )
        .map_err(|e| PluginError::RuntimeError(format!("register hc_state_set: {e}")))?;
    Ok(())
}

/// `hc_state_subscribe(eid_ptr: i32, eid_len: i32) → i32`
///
/// Records a subscription so the host will call `receive_event` on future
/// state changes for this entity. Returns 0 on success, -1 on invalid entity.
fn register_hc_state_subscribe(
    linker: &mut Linker<PluginStoreData>,
) -> Result<(), PluginError> {
    linker
        .func_wrap(
            "env",
            "hc_state_subscribe",
            |mut caller: wasmtime::Caller<'_, PluginStoreData>,
             eid_ptr: i32,
             eid_len: i32|
             -> i32 {
                let eid: String = {
                    let mem = match caller.get_export("memory") {
                        Some(wasmtime::Extern::Memory(m)) => m,
                        _ => return -1,
                    };
                    match read_str(mem.data(&caller), eid_ptr, eid_len) {
                        Some(s) => s.to_owned(),
                        None => return -1,
                    }
                };
                caller.data_mut().subscriptions.push(eid);
                0
            },
        )
        .map_err(|e| PluginError::RuntimeError(format!("register hc_state_subscribe: {e}")))?;
    Ok(())
}

/// `hc_log(level: i32, msg_ptr: i32, msg_len: i32) → ()`
///
/// Structured log output from the plugin. `level`: 0=debug 1=info 2=warn 3=error.
fn register_hc_log(
    linker: &mut Linker<PluginStoreData>,
) -> Result<(), PluginError> {
    linker
        .func_wrap(
            "env",
            "hc_log",
            |mut caller: wasmtime::Caller<'_, PluginStoreData>,
             level: i32,
             msg_ptr: i32,
             msg_len: i32| {
                let mem = match caller.get_export("memory") {
                    Some(wasmtime::Extern::Memory(m)) => m,
                    _ => return,
                };
                let msg = read_str(mem.data(&caller), msg_ptr, msg_len)
                    .unwrap_or("(invalid utf8)")
                    .to_owned();
                let lvl = LogLevel::from_i32(level);
                eprintln!("[PLUGIN {}] {}", lvl.as_str(), msg);
            },
        )
        .map_err(|e| PluginError::RuntimeError(format!("register hc_log: {e}")))?;
    Ok(())
}

// ── WasmPlugin ─────────────────────────────────────────────────────────────

/// A loaded WASM plugin instance. Wraps a Wasmtime `Store` + `Instance`.
///
/// The `Arc<Mutex<_>>` allows the handle to be `Clone` + `Send` while
/// maintaining exclusive access for calls into the WASM module.
pub struct WasmPlugin {
    pub inner: Arc<Mutex<(Store<PluginStoreData>, wasmtime::Instance)>>,
}

impl WasmPlugin {
    /// Return a snapshot of the entity IDs this plugin has subscribed to.
    pub fn subscriptions(&self) -> Vec<String> {
        self.inner
            .lock()
            .map(|g| g.0.data().subscriptions.clone())
            .unwrap_or_default()
    }

    /// Call the `plugin_setup` export with the given config-entry JSON.
    pub fn call_setup(&self, config_entry_json: &str) -> Result<i32, PluginError> {
        let mut guard = self
            .inner
            .lock()
            .map_err(|e| PluginError::RuntimeError(format!("lock: {e}")))?;
        let (store, instance) = &mut *guard;
        call_export_str(store, instance, "plugin_setup", config_entry_json)
    }

    /// Call `plugin_handle_state_changed` with a [`StateChangedEventJson`].
    pub fn call_state_changed(
        &self,
        event: &StateChangedEventJson,
    ) -> Result<i32, PluginError> {
        let json = serde_json::to_string(event)
            .map_err(|e| PluginError::RuntimeError(format!("serialize event: {e}")))?;
        let mut guard = self
            .inner
            .lock()
            .map_err(|e| PluginError::RuntimeError(format!("lock: {e}")))?;
        let (store, instance) = &mut *guard;
        call_export_str(store, instance, "plugin_handle_state_changed", &json)
    }
}

// ── Memory helpers ─────────────────────────────────────────────────────────

/// Read a UTF-8 string from guest linear memory.
fn read_str(mem: &[u8], ptr: i32, len: i32) -> Option<&str> {
    if len < 0 || len as usize > MAX_ABI_BUFFER_BYTES {
        return None;
    }
    let ptr = ptr as usize;
    let len = len as usize;
    let slice = mem.get(ptr..ptr + len)?;
    std::str::from_utf8(slice).ok()
}

/// Allocate a guest buffer via `alloc`, write `payload`, call `export_fn(ptr, len)`,
/// then free via `dealloc`. Returns the i32 result of the guest export.
fn call_export_str(
    store: &mut Store<PluginStoreData>,
    instance: &wasmtime::Instance,
    export_fn: &str,
    payload: &str,
) -> Result<i32, PluginError> {
    let payload_bytes = payload.as_bytes().to_vec(); // owned copy avoids reborrow issues
    let payload_len = payload_bytes.len() as i32;

    // 1. Allocate guest buffer.
    let alloc = instance
        .get_typed_func::<i32, i32>(&mut *store, "alloc")
        .map_err(|e| PluginError::RuntimeError(format!("get alloc: {e}")))?;
    let ptr = alloc
        .call(&mut *store, payload_len)
        .map_err(|e| PluginError::RuntimeError(format!("call alloc: {e}")))?;

    // 2. Write payload into guest memory.
    {
        let mem = instance
            .get_memory(&mut *store, "memory")
            .ok_or_else(|| PluginError::RuntimeError("no memory export".into()))?;
        let guest_slice = mem
            .data_mut(&mut *store)
            .get_mut(ptr as usize..ptr as usize + payload_bytes.len())
            .ok_or_else(|| PluginError::RuntimeError("guest memory OOB".into()))?;
        guest_slice.copy_from_slice(&payload_bytes);
    }

    // 3. Call the guest export.
    let func = instance
        .get_typed_func::<(i32, i32), i32>(&mut *store, export_fn)
        .map_err(|e| PluginError::RuntimeError(format!("get {export_fn}: {e}")))?;
    let result = func
        .call(&mut *store, (ptr, payload_len))
        .map_err(|e| PluginError::RuntimeError(format!("call {export_fn}: {e}")))?;

    // 4. Free the guest buffer.
    let dealloc = instance
        .get_typed_func::<(i32, i32), ()>(&mut *store, "dealloc")
        .map_err(|e| PluginError::RuntimeError(format!("get dealloc: {e}")))?;
    dealloc
        .call(&mut *store, (ptr, payload_len))
        .map_err(|e| PluginError::RuntimeError(format!("call dealloc: {e}")))?;

    Ok(result)
}

// ── Unit tests (using inline WAT) ──────────────────────────────────────────

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

    /// A minimal WAT module that implements all host imports as no-ops and
    /// exports `alloc` / `dealloc` / `plugin_setup` /
    /// `plugin_handle_state_changed`. Compiled at test time via `wat::parse_str`.
    ///
    /// The `hc_state_set` call in the test plugin writes back a hard-coded
    /// entity via the host import (the host import will actually call back into
    /// the HomeCore state machine via `caller.data()`).
    const TEST_WAT: &str = r#"
(module
  ;; Host imports
  (import "env" "hc_state_get"
    (func $hc_state_get (param i32 i32 i32 i32) (result i32)))
  (import "env" "hc_state_set"
    (func $hc_state_set (param i32 i32 i32 i32 i32 i32) (result i32)))
  (import "env" "hc_state_subscribe"
    (func $hc_state_subscribe (param i32 i32) (result i32)))
  (import "env" "hc_log"
    (func $hc_log (param i32 i32 i32)))

  ;; Linear memory: 1 page = 64 KiB
  (memory (export "memory") 1)

  ;; Simple bump allocator state
  (global $bump (mut i32) (i32.const 1024))

  ;; alloc(size) → ptr
  (func (export "alloc") (param $size i32) (result i32)
    (local $ptr i32)
    (local.set $ptr (global.get $bump))
    (global.set $bump (i32.add (global.get $bump) (local.get $size)))
    (local.get $ptr)
  )

  ;; dealloc(ptr, size) — no-op in bump allocator
  (func (export "dealloc") (param i32 i32))

  ;; plugin_setup(ptr, len) → 0
  (func (export "plugin_setup") (param i32 i32) (result i32)
    (i32.const 0)
  )

  ;; plugin_handle_state_changed(ptr, len) → 0
  ;; Calls hc_log with a fixed message so we can observe the import works.
  (func (export "plugin_handle_state_changed") (param i32 i32) (result i32)
    ;; log "ok" at INFO level — offset 0 in memory, write "ok" there first
    (i32.store8 (i32.const 0) (i32.const 111)) ;; 'o'
    (i32.store8 (i32.const 1) (i32.const 107)) ;; 'k'
    (call $hc_log (i32.const 1) (i32.const 0) (i32.const 2))
    (i32.const 0)
  )
)
"#;

    #[test]
    fn wasmtime_runtime_compiles_and_instantiates_wat() {
        let wasm_bytes = wat::parse_str(TEST_WAT).expect("WAT should parse");
        let rt = WasmtimeRuntime::new().expect("engine should init");
        let hc = HomeCore::new();
        let plugin = rt.load_wasm(&wasm_bytes, hc).expect("should instantiate");

        // call plugin_setup — expect 0
        let r = plugin
            .call_setup(r#"{"entry_id":"test","domain":"test","title":"test","data":{}}"#)
            .expect("setup should not error");
        assert_eq!(r, 0, "plugin_setup should return 0");
    }

    #[test]
    fn hc_state_set_round_trip_via_wat() {
        /// WAT plugin that calls hc_state_set to write "on" for binary_sensor.test_alert
        const SET_WAT: &str = r#"
(module
  (import "env" "hc_state_get"
    (func $hc_state_get (param i32 i32 i32 i32) (result i32)))
  (import "env" "hc_state_set"
    (func $hc_state_set (param i32 i32 i32 i32 i32 i32) (result i32)))
  (import "env" "hc_state_subscribe"
    (func $hc_state_subscribe (param i32 i32) (result i32)))
  (import "env" "hc_log"
    (func $hc_log (param i32 i32 i32)))

  (memory (export "memory") 1)
  (global $bump (mut i32) (i32.const 2048))

  (func (export "alloc") (param $size i32) (result i32)
    (local $ptr i32)
    (local.set $ptr (global.get $bump))
    (global.set $bump (i32.add (global.get $bump) (local.get $size)))
    (local.get $ptr)
  )
  (func (export "dealloc") (param i32 i32))

  ;; Strings stored at known offsets in memory:
  ;; offset 0: "binary_sensor.test_alert" (24 bytes)
  ;; offset 64: "on" (2 bytes)
  ;; offset 128: "{}" (2 bytes)
  (data (i32.const 0) "binary_sensor.test_alert")
  (data (i32.const 64) "on")
  (data (i32.const 128) "{}")

  ;; plugin_setup: call hc_state_set to write "on"
  (func (export "plugin_setup") (param i32 i32) (result i32)
    (call $hc_state_set
      (i32.const 0)   ;; eid_ptr
      (i32.const 24)  ;; eid_len  = len("binary_sensor.test_alert")
      (i32.const 64)  ;; state_ptr
      (i32.const 2)   ;; state_len = len("on")
      (i32.const 128) ;; attrs_ptr
      (i32.const 2)   ;; attrs_len = len("{}")
    )
    drop
    (i32.const 0)
  )

  (func (export "plugin_handle_state_changed") (param i32 i32) (result i32)
    (i32.const 0)
  )
)
"#;
        let wasm_bytes = wat::parse_str(SET_WAT).expect("WAT should parse");
        let rt = WasmtimeRuntime::new().expect("engine");
        let hc = HomeCore::new();
        let plugin = rt.load_wasm(&wasm_bytes, hc.clone()).expect("instantiate");

        // Call plugin_setup — the WAT calls hc_state_set inside.
        plugin.call_setup("{}").expect("setup");

        // Verify the host state machine saw the write.
        let eid = homecore::EntityId::parse("binary_sensor.test_alert").unwrap();
        let state = hc.states().get(&eid).expect("state should exist");
        assert_eq!(
            state.state, "on",
            "hc_state_set via host import should write 'on'"
        );
    }
}
