//! Dynamic plugin loader for server-side hook extensions.
//!
//! Loads shared libraries (`*.so` / `*.dylib`) from a directory at server
//! startup. Each library must export:
//!
//! ```c
//! extern "C" int32_t qpq_plugin_init(HookVTable *vtable);
//! ```
//!
//! The server creates a zeroed [`HookVTable`], passes it to `qpq_plugin_init`,
//! and wraps the resulting vtable in a [`PluginHooks`] that implements
//! [`ServerHooks`].  Multiple plugins are chained via [`ChainedHooks`].
//!
//! # Safety model
//!
//! Dynamic loading is inherently unsafe.  The plugin binary MUST:
//! - be compiled against the same `quicproquo-plugin-api` version
//! - not store the event-struct pointers beyond the callback duration
//! - be `Send + Sync` (the wrapper is put behind an `Arc`)
//!
//! The server operator is responsible for only loading trusted plugin binaries.

use std::path::Path;

use libloading::{Library, Symbol};
use quicproquo_plugin_api::{
    CAuthEvent, CChannelEvent, CFetchEvent, CMessageEvent, HookVTable, HOOK_CONTINUE, PLUGIN_OK,
};

use crate::hooks::{AuthEvent, ChannelEvent, FetchEvent, HookAction, MessageEvent, ServerHooks};

// ── PluginHooks ───────────────────────────────────────────────────────────────

/// A [`ServerHooks`] implementation backed by a dynamically loaded plugin vtable.
///
/// Holds the [`Library`] alive alongside the vtable so that the loaded code
/// is not unmapped while the vtable function pointers are still reachable.
pub struct PluginHooks {
    /// The vtable filled by `qpq_plugin_init`.
    vtable: HookVTable,
    /// Keeps the shared library mapped. Must be dropped after `vtable`.
    _lib: Library,
    /// Name of the plugin file, for diagnostics.
    name: String,
}

impl PluginHooks {
    /// Load a plugin from `path` and call `qpq_plugin_init`.
    ///
    /// Returns `Err` if the library cannot be opened, the symbol is missing,
    /// or `qpq_plugin_init` returns a non-zero error code.
    pub fn load(path: &Path) -> anyhow::Result<Self> {
        let name = path
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| path.display().to_string());

        // Safety: loading arbitrary shared libraries is inherently unsafe.
        // The server operator is responsible for only loading trusted plugins.
        let lib = unsafe { Library::new(path) }
            .map_err(|e| anyhow::anyhow!("plugin '{}': load failed: {}", name, e))?;

        // Zero-initialise the vtable so unused slots are null.
        let mut vtable = HookVTable {
            user_data: core::ptr::null_mut(),
            on_message_enqueue: None,
            on_batch_enqueue: None,
            on_auth: None,
            on_channel_created: None,
            on_fetch: None,
            on_user_registered: None,
            error_message: None,
            destroy: None,
        };

        // Safety: the symbol must have the exact signature declared in the API crate.
        let init: Symbol<unsafe extern "C" fn(*mut HookVTable) -> i32> =
            unsafe { lib.get(b"qpq_plugin_init\0") }.map_err(|e| {
                anyhow::anyhow!("plugin '{}': missing qpq_plugin_init: {}", name, e)
            })?;

        let rc = unsafe { init(&mut vtable) };
        if rc != PLUGIN_OK {
            anyhow::bail!("plugin '{}': qpq_plugin_init returned error {}", name, rc);
        }

        tracing::info!(plugin = %name, "loaded plugin");
        Ok(Self { vtable, _lib: lib, name })
    }

    /// Human-readable plugin name (filename).
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Retrieve the rejection reason from the plugin, falling back to a generic string.
    fn rejection_reason(&self) -> String {
        if let Some(f) = self.vtable.error_message {
            let ptr = unsafe { f(self.vtable.user_data) };
            if !ptr.is_null() {
                // Safety: plugin must return a valid null-terminated UTF-8 (or ASCII) string.
                let cstr = unsafe { std::ffi::CStr::from_ptr(ptr as *const core::ffi::c_char) };
                return cstr.to_string_lossy().into_owned();
            }
        }
        "rejected by plugin".to_string()
    }
}

impl Drop for PluginHooks {
    fn drop(&mut self) {
        if let Some(destroy) = self.vtable.destroy {
            // Safety: destroy must be safe to call at any time after init.
            unsafe { destroy(self.vtable.user_data) };
        }
    }
}

impl ServerHooks for PluginHooks {
    fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
        let f = match self.vtable.on_message_enqueue {
            Some(f) => f,
            None => return HookAction::Continue,
        };

        let sender_ptr = event
            .sender_identity
            .as_deref()
            .map(|s| s.as_ptr())
            .unwrap_or(core::ptr::null());
        let sender_len = event.sender_identity.as_deref().map_or(0, |s| s.len());

        let c_event = CMessageEvent {
            sender_identity: sender_ptr,
            sender_identity_len: sender_len,
            recipient_key: event.recipient_key.as_ptr(),
            recipient_key_len: event.recipient_key.len(),
            channel_id: event.channel_id.as_ptr(),
            channel_id_len: event.channel_id.len(),
            payload_len: event.payload_len,
            seq: event.seq,
        };

        let rc = unsafe { f(self.vtable.user_data, &c_event) };
        if rc == HOOK_CONTINUE {
            HookAction::Continue
        } else {
            HookAction::Reject(self.rejection_reason())
        }
    }

    fn on_batch_enqueue(&self, events: &[MessageEvent]) {
        let f = match self.vtable.on_batch_enqueue {
            Some(f) => f,
            None => return,
        };

        let c_events: Vec<CMessageEvent> = events
            .iter()
            .map(|e| {
                let sender_ptr = e
                    .sender_identity
                    .as_deref()
                    .map(|s| s.as_ptr())
                    .unwrap_or(core::ptr::null());
                let sender_len = e.sender_identity.as_deref().map_or(0, |s| s.len());
                CMessageEvent {
                    sender_identity: sender_ptr,
                    sender_identity_len: sender_len,
                    recipient_key: e.recipient_key.as_ptr(),
                    recipient_key_len: e.recipient_key.len(),
                    channel_id: e.channel_id.as_ptr(),
                    channel_id_len: e.channel_id.len(),
                    payload_len: e.payload_len,
                    seq: e.seq,
                }
            })
            .collect();

        unsafe { f(self.vtable.user_data, c_events.as_ptr(), c_events.len()) };
    }

    fn on_auth(&self, event: &AuthEvent) {
        let f = match self.vtable.on_auth {
            Some(f) => f,
            None => return,
        };
        let c_event = CAuthEvent {
            username: event.username.as_ptr(),
            username_len: event.username.len(),
            success: if event.success { 1 } else { 0 },
            failure_reason: event.failure_reason.as_ptr(),
            failure_reason_len: event.failure_reason.len(),
        };
        unsafe { f(self.vtable.user_data, &c_event) };
    }

    fn on_channel_created(&self, event: &ChannelEvent) {
        let f = match self.vtable.on_channel_created {
            Some(f) => f,
            None => return,
        };
        let c_event = CChannelEvent {
            channel_id: event.channel_id.as_ptr(),
            channel_id_len: event.channel_id.len(),
            initiator_key: event.initiator_key.as_ptr(),
            initiator_key_len: event.initiator_key.len(),
            peer_key: event.peer_key.as_ptr(),
            peer_key_len: event.peer_key.len(),
            was_new: if event.was_new { 1 } else { 0 },
        };
        unsafe { f(self.vtable.user_data, &c_event) };
    }

    fn on_fetch(&self, event: &FetchEvent) {
        let f = match self.vtable.on_fetch {
            Some(f) => f,
            None => return,
        };
        let c_event = CFetchEvent {
            recipient_key: event.recipient_key.as_ptr(),
            recipient_key_len: event.recipient_key.len(),
            channel_id: event.channel_id.as_ptr(),
            channel_id_len: event.channel_id.len(),
            message_count: event.message_count,
        };
        unsafe { f(self.vtable.user_data, &c_event) };
    }

    fn on_user_registered(&self, username: &str, identity_key: &[u8]) {
        let f = match self.vtable.on_user_registered {
            Some(f) => f,
            None => return,
        };
        unsafe {
            f(
                self.vtable.user_data,
                username.as_ptr(),
                username.len(),
                identity_key.as_ptr(),
                identity_key.len(),
            )
        };
    }
}

// ── ChainedHooks ─────────────────────────────────────────────────────────────

/// Composes multiple [`ServerHooks`] implementations into one.
///
/// For filtering hooks (`on_message_enqueue`), the first rejection short-circuits
/// the chain. For fire-and-forget hooks, all plugins are called in order.
pub struct ChainedHooks {
    hooks: Vec<Box<dyn ServerHooks>>,
}

impl ChainedHooks {
    pub fn new(hooks: Vec<Box<dyn ServerHooks>>) -> Self {
        Self { hooks }
    }
}

impl ServerHooks for ChainedHooks {
    fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
        for h in &self.hooks {
            match h.on_message_enqueue(event) {
                HookAction::Continue => {}
                reject => return reject,
            }
        }
        HookAction::Continue
    }

    fn on_batch_enqueue(&self, events: &[MessageEvent]) {
        for h in &self.hooks {
            h.on_batch_enqueue(events);
        }
    }

    fn on_auth(&self, event: &AuthEvent) {
        for h in &self.hooks {
            h.on_auth(event);
        }
    }

    fn on_channel_created(&self, event: &ChannelEvent) {
        for h in &self.hooks {
            h.on_channel_created(event);
        }
    }

    fn on_fetch(&self, event: &FetchEvent) {
        for h in &self.hooks {
            h.on_fetch(event);
        }
    }

    fn on_user_registered(&self, username: &str, identity_key: &[u8]) {
        for h in &self.hooks {
            h.on_user_registered(username, identity_key);
        }
    }
}

// ── load_plugins_from_dir ─────────────────────────────────────────────────────

/// Load all `*.so` / `*.dylib` files from `dir` as plugins.
///
/// Non-fatal errors (unreadable files, init failures) are logged as warnings
/// and skipped; the server continues with the plugins that did load.
/// Returns the full list of successfully loaded plugins.
pub fn load_plugins_from_dir(dir: &Path) -> Vec<PluginHooks> {
    let mut plugins = Vec::new();

    let entries = match std::fs::read_dir(dir) {
        Ok(e) => e,
        Err(e) => {
            tracing::warn!(dir = %dir.display(), error = %e, "plugin_dir unreadable; no plugins loaded");
            return plugins;
        }
    };

    for entry in entries.flatten() {
        let path = entry.path();
        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
        if ext != "so" && ext != "dylib" {
            continue;
        }

        match PluginHooks::load(&path) {
            Ok(p) => {
                tracing::info!(plugin = %p.name(), "plugin loaded successfully");
                plugins.push(p);
            }
            Err(e) => {
                tracing::warn!(path = %path.display(), error = %e, "failed to load plugin; skipping");
            }
        }
    }

    plugins
}
