//! Plugin loading — loads a cdylib and exposes it as an OpRegistry.
//!
//! The cdylib must export three C functions (generated by the `hush_plugin!` macro):
//!   - `hush_call(name, inputs_json) -> json_str`
//!   - `hush_call_generator(name, inputs_json) -> json_array_str`
//!   - `hush_free_str(ptr)` — free a string returned by the above

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::path::Path;

use hush_icore::registry::OpRegistry;
use serde_json::Value;

/// FFI function types matching the cdylib contract.
type HushCallFn = unsafe extern "C" fn(*const c_char, *const c_char) -> *mut c_char;
type HushFreeStrFn = unsafe extern "C" fn(*mut c_char);

/// An OpRegistry backed by a dynamically loaded cdylib plugin.
pub struct PluginRegistry {
    _lib: libloading::Library,
    call_fn: HushCallFn,
    call_generator_fn: HushCallFn,
    free_fn: HushFreeStrFn,
}

// SAFETY: The cdylib functions are required to be thread-safe (no mutable global state).
// The hush_plugin! macro generates stateless dispatch functions.
unsafe impl Send for PluginRegistry {}
unsafe impl Sync for PluginRegistry {}

impl PluginRegistry {
    /// Load a plugin from a cdylib path (.dll / .so / .dylib).
    ///
    /// # Safety
    /// The library must export `hush_call`, `hush_call_generator`, and `hush_free_str`
    /// with the correct C ABI signatures.
    pub fn load(path: &Path) -> Result<Self, String> {
        let lib = unsafe { libloading::Library::new(path) }
            .map_err(|e| format!("Failed to load plugin '{}': {}", path.display(), e))?;

        let call_fn: HushCallFn = unsafe {
            *lib.get::<HushCallFn>(b"hush_call\0")
                .map_err(|e| format!("Plugin missing 'hush_call' symbol: {}", e))?
        };

        let call_generator_fn: HushCallFn = unsafe {
            *lib.get::<HushCallFn>(b"hush_call_generator\0")
                .map_err(|e| format!("Plugin missing 'hush_call_generator' symbol: {}", e))?
        };

        let free_fn: HushFreeStrFn = unsafe {
            *lib.get::<HushFreeStrFn>(b"hush_free_str\0")
                .map_err(|e| format!("Plugin missing 'hush_free_str' symbol: {}", e))?
        };

        Ok(PluginRegistry {
            _lib: lib,
            call_fn,
            call_generator_fn,
            free_fn,
        })
    }

    /// Call an FFI function and parse the returned JSON string.
    fn ffi_call(&self, ffi_fn: HushCallFn, name: &str, inputs: &Value) -> Option<Value> {
        let name_c = CString::new(name).ok()?;
        let inputs_str = serde_json::to_string(inputs).ok()?;
        let inputs_c = CString::new(inputs_str).ok()?;

        let result_ptr = unsafe { ffi_fn(name_c.as_ptr(), inputs_c.as_ptr()) };
        if result_ptr.is_null() {
            return None;
        }

        let result_str = unsafe { CStr::from_ptr(result_ptr) }
            .to_str()
            .ok()?
            .to_owned();

        // Free the string allocated by the plugin
        unsafe { (self.free_fn)(result_ptr) };

        serde_json::from_str(&result_str).ok()
    }
}

impl OpRegistry for PluginRegistry {
    fn call(&self, name: &str, inputs: &Value) -> Option<Value> {
        self.ffi_call(self.call_fn, name, inputs)
    }

    fn call_generator(&self, name: &str, inputs: &Value) -> Option<Vec<Value>> {
        let value = self.ffi_call(self.call_generator_fn, name, inputs)?;
        match value {
            Value::Array(items) => Some(items),
            other => Some(vec![other]),
        }
    }
}
