//! Module handle for loaded modules.
//!
//! Provides a unified interface for both static and dynamic modules.
//! Static modules are `Box<dyn Module>`, dynamic modules use opaque
//! `*mut c_void` pointers with FFI trampolines generated by `declare_module!`.

use std::{ffi::c_void, path::PathBuf};

use {
    libloading::Library,
    reovim_kernel::api::v1::{
        Module, ModuleContext, ModuleError, ModuleId, ModuleProbe, ProbeResult, Version,
    },
};

// ============================================================================
// FFI Function Types
// ============================================================================

/// FFI function types matching symbols generated by `declare_module!` macro.
pub(crate) type ProbeFn = unsafe extern "C" fn() -> ModuleProbe;
pub(crate) type EntryFn = unsafe extern "C" fn() -> *mut c_void;
pub(crate) type InitFn = unsafe extern "C" fn(*mut c_void, *const c_void) -> i32;
pub(crate) type ExitFn = unsafe extern "C" fn(*mut c_void) -> i32;
pub(crate) type DestroyFn = unsafe extern "C" fn(*mut c_void);

// Hot reload FFI types
pub(crate) type SupportsHotReloadFn = unsafe extern "C" fn(*const c_void) -> i32;
pub(crate) type SaveStateFn = unsafe extern "C" fn(*const c_void, *mut *mut u8, *mut usize) -> i32;
pub(crate) type RestoreStateFn = unsafe extern "C" fn(*mut c_void, *const u8, usize) -> i32;
pub(crate) type FreeStateFn = unsafe extern "C" fn(*mut u8, usize);

/// Maximum allowed state size for hot reload (16 MiB).
///
/// Prevents memory exhaustion from malicious or buggy modules.
const MAX_STATE_SIZE: usize = 16 * 1024 * 1024;

// ============================================================================
// FfiSymbols
// ============================================================================

/// Cached FFI symbols for dynamic modules (avoids repeated dlsym lookup).
pub(crate) struct FfiSymbols {
    pub(crate) init: InitFn,
    pub(crate) exit: ExitFn,
    pub(crate) destroy: DestroyFn,
    pub(crate) supports_hot_reload: Option<SupportsHotReloadFn>,
    pub(crate) save_state: Option<SaveStateFn>,
    pub(crate) restore_state: Option<RestoreStateFn>,
    pub(crate) free_state: Option<FreeStateFn>,
}

// ============================================================================
// InitResult
// ============================================================================

/// Result of `init()` call.
#[derive(Debug, Clone)]
pub enum InitResult {
    /// Module initialized successfully.
    Success,
    /// Module deferred initialization (retry later).
    Defer(String),
}

// ============================================================================
// ModuleHandle
// ============================================================================

/// Handle to a loaded module (static or dynamic).
///
/// # Safety Invariants
///
/// For dynamic modules:
/// - `dynamic_ptr` is always a valid, non-null pointer from `reovim_module_entry()`
/// - `library` keeps the .so loaded for the lifetime of the handle
/// - `ffi` symbols are resolved once at load time, cached for the handle's lifetime
/// - Drop calls `reovim_module_destroy()` before Library is dropped
#[allow(unsafe_code)]
pub struct ModuleHandle {
    id: ModuleId,
    probe: ModuleProbe,
    static_module: Option<Box<dyn Module>>,
    dynamic_ptr: Option<*mut c_void>,
    #[allow(dead_code)] // Kept loaded for dynamic module lifetime
    library: Option<Library>,
    path: Option<PathBuf>,
    ffi: Option<FfiSymbols>,
}

// SAFETY: ModuleHandle is Send+Sync because:
// - static_module: Module trait requires Send + Sync
// - dynamic_ptr: only accessed through FFI trampolines (single-threaded access
//   guaranteed by registry's Mutex)
// - Library: thread-safe for keeping .so loaded
#[allow(unsafe_code)]
unsafe impl Send for ModuleHandle {}
#[allow(unsafe_code)]
unsafe impl Sync for ModuleHandle {}

// Dynamic FFI paths require a real .so with declare_module! exports.
// Static paths are covered by handle_tests.rs.
#[cfg_attr(coverage_nightly, coverage(off))]
impl ModuleHandle {
    /// Create a handle for a static (compile-time linked) module.
    #[must_use]
    pub fn from_static<M: Module>(module: M) -> Self {
        let probe = ModuleProbe::new(
            module.id().as_str(),
            module.name(),
            module.version(),
            module.api_version(),
        );
        let id = module.id();

        Self {
            id,
            probe,
            static_module: Some(Box::new(module)),
            dynamic_ptr: None,
            library: None,
            path: None,
            ffi: None,
        }
    }

    /// Create a handle from an already-boxed module.
    #[must_use]
    pub fn from_boxed(module: Box<dyn Module>) -> Self {
        let probe = ModuleProbe::new(
            module.id().as_str(),
            module.name(),
            module.version(),
            module.api_version(),
        );
        let id = module.id();

        Self {
            id,
            probe,
            static_module: Some(module),
            dynamic_ptr: None,
            library: None,
            path: None,
            ffi: None,
        }
    }

    /// Create a handle for a dynamic (runtime loaded) module.
    ///
    /// # Safety
    ///
    /// - The library must be ABI-compatible
    /// - `module_ptr` must be a valid pointer from `reovim_module_entry()`
    /// - `ffi` symbols must have been resolved from the same library
    #[allow(unsafe_code, dead_code, clippy::large_types_passed_by_value)]
    pub(crate) unsafe fn from_dynamic(
        library: Library,
        path: PathBuf,
        probe: ModuleProbe,
        module_ptr: *mut c_void,
        ffi: FfiSymbols,
    ) -> Self {
        let id = ModuleId::from_string(probe.id_str().to_owned());

        Self {
            id,
            probe,
            static_module: None,
            dynamic_ptr: Some(module_ptr),
            library: Some(library),
            path: Some(path),
            ffi: Some(ffi),
        }
    }

    /// Initialize the module.
    ///
    /// For static modules, calls `Module::init()` directly.
    /// For dynamic modules, calls the FFI init trampoline.
    ///
    /// # Errors
    ///
    /// Returns `ModuleError::InitFailed` if the module's init fails or panics.
    /// Returns `ModuleError::NotLoaded` if the handle has no module attached.
    #[allow(unsafe_code)]
    pub fn init(&mut self, ctx: &ModuleContext) -> Result<InitResult, ModuleError> {
        if let Some(ref mut module) = self.static_module {
            match module.init(ctx) {
                ProbeResult::Success => Ok(InitResult::Success),
                ProbeResult::Defer(reason) => Ok(InitResult::Defer(reason)),
                ProbeResult::Failed(err) => Err(err),
            }
        } else if let (Some(ptr), Some(ffi)) = (self.dynamic_ptr, &self.ffi) {
            if ptr.is_null() {
                return Err(ModuleError::InitFailed("module pointer is null".into()));
            }
            let ctx_ptr: *const c_void = std::ptr::from_ref(ctx).cast();
            // SAFETY: ptr is non-null (checked above), ffi.init is valid (resolved by loader)
            let result = unsafe { (ffi.init)(ptr, ctx_ptr) };
            match result {
                0 => Ok(InitResult::Success),
                1 => Ok(InitResult::Defer("deferred by module".into())),
                -1 => Err(ModuleError::InitFailed("init returned error".into())),
                -2 => Err(ModuleError::InitFailed("panic in module init".into())),
                code => Err(ModuleError::InitFailed(format!("unknown code: {code}"))),
            }
        } else {
            Err(ModuleError::NotLoaded(self.id.clone()))
        }
    }

    /// Call module exit.
    ///
    /// # Errors
    ///
    /// Returns `ModuleError::InitFailed` if the module's exit fails or panics.
    /// Returns `ModuleError::NotLoaded` if the handle has no module attached.
    #[allow(unsafe_code)]
    pub fn exit(&mut self) -> Result<(), ModuleError> {
        if let Some(ref mut module) = self.static_module {
            module.exit()
        } else if let (Some(ptr), Some(ffi)) = (self.dynamic_ptr, &self.ffi) {
            if ptr.is_null() {
                return Err(ModuleError::InitFailed("module pointer is null".into()));
            }
            // SAFETY: ptr is non-null (checked above), ffi.exit is valid
            let result = unsafe { (ffi.exit)(ptr) };
            match result {
                0 => Ok(()),
                -1 => Err(ModuleError::InitFailed("exit returned error".into())),
                -2 => Err(ModuleError::InitFailed("panic in module exit".into())),
                code => Err(ModuleError::InitFailed(format!("unknown code: {code}"))),
            }
        } else {
            Err(ModuleError::NotLoaded(self.id.clone()))
        }
    }

    /// Call `on_all_loaded()` lifecycle hook.
    pub fn on_all_loaded(&mut self, ctx: &ModuleContext) {
        if let Some(ref mut module) = self.static_module {
            module.on_all_loaded(ctx);
        }
        // Dynamic modules: on_all_loaded is not exposed via FFI currently.
        // Future work: add reovim_module_on_all_loaded symbol.
    }

    // ========================================================================
    // Accessors
    // ========================================================================

    /// Get module ID.
    #[must_use]
    pub const fn id(&self) -> &ModuleId {
        &self.id
    }

    /// Get module name from probe.
    #[must_use]
    pub fn name(&self) -> &str {
        self.probe.name_str()
    }

    /// Get module version from probe.
    #[must_use]
    pub const fn version(&self) -> Version {
        self.probe.version
    }

    /// Get module API version from probe.
    #[must_use]
    pub const fn api_version(&self) -> Version {
        self.probe.api_version
    }

    /// Get module path (for dynamic modules).
    #[must_use]
    pub const fn path(&self) -> Option<&PathBuf> {
        self.path.as_ref()
    }

    /// Check if this is a static module.
    #[must_use]
    pub const fn is_static(&self) -> bool {
        self.static_module.is_some()
    }

    /// Check if this is a dynamic module.
    #[must_use]
    pub const fn is_dynamic(&self) -> bool {
        self.dynamic_ptr.is_some()
    }

    /// Get static module reference (for static modules only).
    #[must_use]
    pub fn as_module(&self) -> Option<&dyn Module> {
        self.static_module.as_deref()
    }

    /// Get mutable static module reference.
    #[must_use]
    pub fn as_module_mut(&mut self) -> Option<&mut dyn Module> {
        self.static_module.as_deref_mut()
    }

    /// Get module dependencies.
    #[must_use]
    pub fn dependencies(&self) -> Vec<ModuleId> {
        self.static_module
            .as_ref()
            .map_or_else(|| self.probe.required_deps(), |module| module.dependencies())
    }

    /// Get optional dependencies.
    #[must_use]
    pub fn optional_dependencies(&self) -> Vec<ModuleId> {
        self.static_module
            .as_ref()
            .map_or_else(|| self.probe.optional_deps(), |module| module.optional_dependencies())
    }

    /// Get extension kinds declared by this module.
    #[must_use]
    pub fn extension_kinds(&self) -> &[&'static str] {
        self.static_module
            .as_ref()
            .map_or(&[] as &[&'static str], |module| module.extension_kinds())
    }

    /// Get capabilities provided by this module (#618).
    #[must_use]
    pub fn provides(&self) -> &[&'static str] {
        self.static_module
            .as_ref()
            .map_or(&[] as &[&'static str], |module| module.provides())
    }

    /// Get capabilities required by this module (#618).
    #[must_use]
    pub fn requires(&self) -> &[&'static str] {
        self.static_module
            .as_ref()
            .map_or(&[] as &[&'static str], |module| module.requires())
    }

    /// Get version constraints on dependencies (#619).
    #[must_use]
    pub fn version_constraints(&self) -> Vec<(ModuleId, &'static str)> {
        self.static_module
            .as_ref()
            .map_or_else(Vec::new, |module| module.version_constraints())
    }

    /// Check if module supports hot reload.
    #[must_use]
    #[allow(unsafe_code)]
    pub fn supports_hot_reload(&self) -> bool {
        if let Some(ref module) = self.static_module {
            return module.supports_hot_reload();
        }
        if let (Some(ptr), Some(ffi)) = (self.dynamic_ptr, &self.ffi) {
            ffi.supports_hot_reload.is_some_and(|supports_fn| {
                // SAFETY: ptr is valid, supports_fn is from loader
                let result = unsafe { supports_fn(ptr) };
                result == 1
            })
        } else {
            false
        }
    }

    /// Save module state (for hot reload).
    ///
    /// State buffers larger than 16 MiB are rejected.
    #[must_use]
    #[allow(unsafe_code)]
    pub fn save_state(&self) -> Option<Box<[u8]>> {
        if let Some(ref module) = self.static_module {
            let state = module.save_state()?;
            if state.len() > MAX_STATE_SIZE {
                tracing::warn!(
                    module_id = %self.id,
                    size = state.len(),
                    limit = MAX_STATE_SIZE,
                    "state exceeds size limit, discarding"
                );
                return None;
            }
            return Some(state);
        }
        if let (Some(ptr), Some(ffi)) = (self.dynamic_ptr, &self.ffi) {
            let save_fn = ffi.save_state?;
            let mut out_ptr: *mut u8 = std::ptr::null_mut();
            let mut out_len: usize = 0;

            // SAFETY: ptr is valid, out_ptr/out_len are valid stack addresses
            let result = unsafe { save_fn(ptr, &raw mut out_ptr, &raw mut out_len) };

            if result == 0 && !out_ptr.is_null() && out_len > 0 {
                if out_len > MAX_STATE_SIZE {
                    tracing::warn!(
                        module_id = %self.id,
                        size = out_len,
                        limit = MAX_STATE_SIZE,
                        "state exceeds size limit, freeing and discarding"
                    );
                    // Free via FFI (module's allocator, not ours)
                    if let Some(free_fn) = ffi.free_state {
                        // SAFETY: out_ptr was allocated by save_fn
                        unsafe { free_fn(out_ptr, out_len) };
                    }
                    return None;
                }

                // SAFETY: save_fn allocated this buffer
                let slice =
                    unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(out_ptr, out_len)) };
                Some(slice)
            } else {
                None
            }
        } else {
            None
        }
    }

    /// Restore module state (for hot reload).
    ///
    /// # Errors
    ///
    /// Returns `ModuleError::InitFailed` if restore fails, panics, or is unsupported.
    /// Returns `ModuleError::NotLoaded` if the handle has no module attached.
    #[allow(unsafe_code)]
    pub fn restore_state(&mut self, state: &[u8]) -> Result<(), ModuleError> {
        if let Some(ref mut module) = self.static_module {
            return module.restore_state(state);
        }
        if let (Some(ptr), Some(ffi)) = (self.dynamic_ptr, &self.ffi) {
            ffi.restore_state.map_or_else(
                || {
                    Err(ModuleError::InitFailed(
                        "state restore not supported for this dynamic module".into(),
                    ))
                },
                |restore_fn| {
                    // SAFETY: ptr is valid, state is a valid slice
                    let result = unsafe { restore_fn(ptr, state.as_ptr(), state.len()) };
                    match result {
                        0 => Ok(()),
                        -1 => Err(ModuleError::InitFailed("restore_state failed".into())),
                        -2 => Err(ModuleError::InitFailed("panic in restore_state".into())),
                        code => Err(ModuleError::InitFailed(format!("unknown code: {code}"))),
                    }
                },
            )
        } else {
            Err(ModuleError::NotLoaded(self.id.clone()))
        }
    }
}

#[allow(unsafe_code)]
#[cfg_attr(coverage_nightly, coverage(off))]
impl Drop for ModuleHandle {
    fn drop(&mut self) {
        if let (Some(ptr), Some(ffi)) = (self.dynamic_ptr.take(), &self.ffi)
            && !ptr.is_null()
        {
            // SAFETY: ptr is non-null (checked), ffi.destroy is valid (from loader),
            // called exactly once (take() ensures no double-drop)
            unsafe { (ffi.destroy)(ptr) };
        }
        // Library is dropped automatically, unloading the .so
    }
}
