//! Reference quicproquo server plugin: payload-size rate limiter.
//!
//! Rejects any single message whose payload exceeds `MAX_PAYLOAD_BYTES`.  In a
//! real deployment you would extend this with per-sender token-bucket logic,
//! but this example intentionally stays simple so the plugin API surface is
//! easy to follow.
//!
//! # Building
//!
//! ```bash
//! cargo build --release -p rate_limit_plugin
//! # Output: target/release/librate_limit_plugin.so  (Linux)
//! #         target/release/librate_limit_plugin.dylib (macOS)
//! ```
//!
//! # Deploying
//!
//! Copy the resulting `.so` / `.dylib` into your plugin directory and start
//! the server with `--plugin-dir /path/to/plugins`.
//!
//! # Config (via TOML)
//!
//! ```toml
//! plugin_dir = "/etc/qpq/plugins"
//! ```

use std::ffi::c_void;

use quicproquo_plugin_api::{CMessageEvent, HookVTable, HOOK_CONTINUE, HOOK_REJECT, PLUGIN_OK};

/// Maximum allowed encrypted payload size in bytes.
const MAX_PAYLOAD_BYTES: usize = 512 * 1024; // 512 KiB

/// Plugin-private state, heap-allocated and passed as `user_data`.
struct PluginState {
    /// Last rejection reason as a null-terminated C string owned by us.
    last_error: Option<std::ffi::CString>,
}

impl PluginState {
    fn new() -> *mut Self {
        Box::into_raw(Box::new(Self { last_error: None }))
    }

    fn set_error(&mut self, msg: &str) {
        // Replace previous error (if any); CString silently truncates interior NUL bytes,
        // but our format strings never contain them.
        self.last_error = std::ffi::CString::new(msg).ok();
    }
}

// ── Hook callbacks ────────────────────────────────────────────────────────────

unsafe extern "C" fn on_message_enqueue(
    user_data: *mut c_void,
    event: *const CMessageEvent,
) -> i32 {
    let state = &mut *(user_data as *mut PluginState);
    let payload_len = (*event).payload_len;

    if payload_len > MAX_PAYLOAD_BYTES {
        state.set_error(&format!(
            "payload {} bytes exceeds limit {} bytes",
            payload_len, MAX_PAYLOAD_BYTES
        ));
        return HOOK_REJECT;
    }

    HOOK_CONTINUE
}

unsafe extern "C" fn error_message(user_data: *mut c_void) -> *const u8 {
    let state = &*(user_data as *mut PluginState);
    match &state.last_error {
        Some(s) => s.as_ptr() as *const u8,
        None => std::ptr::null(),
    }
}

unsafe extern "C" fn destroy(user_data: *mut c_void) {
    if !user_data.is_null() {
        drop(Box::from_raw(user_data as *mut PluginState));
    }
}

// ── Plugin entry point ────────────────────────────────────────────────────────

/// Called by the server once at startup. Fill `vtable` with function pointers
/// and return `PLUGIN_OK` (0) on success.
///
/// # Safety
///
/// `vtable` must be a valid pointer to a zeroed `HookVTable` as provided by
/// `qpq-server`. Do not call from any other context.
#[no_mangle]
pub unsafe extern "C" fn qpq_plugin_init(vtable: *mut HookVTable) -> i32 {
    if vtable.is_null() {
        return -1;
    }
    let vtable = &mut *vtable;

    vtable.user_data = PluginState::new() as *mut c_void;
    vtable.on_message_enqueue = Some(on_message_enqueue);
    vtable.error_message = Some(error_message);
    vtable.destroy = Some(destroy);

    PLUGIN_OK
}
