#![allow(unsafe_op_in_unsafe_fn)]
//! Structured logging interface for the NWEP C library.
//!
//! This module wraps the C library's logging subsystem and exposes it as an
//! idiomatic Rust API. The C library emits structured log entries (level,
//! timestamp, trace ID, component name, message) to one or more configured
//! sinks. The Rust bindings support the following sinks:
//!
//! - **Standard error** ([`set_stderr`]): the simplest option; routes all
//!   messages at or above the current level to `stderr`. Enable it with
//!   `set_stderr(true)` before constructing clients or servers to see library
//!   debug output.
//! - **JSON formatting** ([`set_json`]): when enabled, messages written to
//!   `stderr` are emitted as newline-delimited JSON objects instead of plain
//!   text.
//! - **Rust callback** ([`set_callback`]): installs a Rust closure that is
//!   invoked for every log entry at or above the current level. Only one
//!   callback can be active at a time; calling [`set_callback`] again replaces
//!   the previous closure. Use [`clear_callback`] to remove it.
//!
//! # Level filtering
//!
//! The global log level ([`set_level`] / [`get_level`]) acts as a threshold:
//! entries with a level below the threshold are discarded inside the C library
//! before any Rust code is invoked. The default level is determined by the C
//! library build configuration.
//!
//! # Writing log entries from Rust
//!
//! Use the level-specific convenience wrappers ([`trace`], [`debug`], [`info`],
//! [`warn`], [`error`]) or the generic [`write`] function to inject log entries
//! from Rust code into the same stream as C library messages. These functions
//! accept plain `&str` values; the binding handles NUL termination internally.
//!
//! # Example
//!
//! ```rust
//! use nwep::log::{self, LogLevel};
//!
//! // Print all messages at Debug level and above to stderr.
//! log::set_level(LogLevel::Debug);
//! log::set_stderr(true);
//!
//! let tid = [0u8; 16];
//! log::info(&tid, "myapp", "server started");
//! ```
//!
//! # Callback example
//!
//! ```rust
//! use nwep::log::{self, LogLevel, LogEntry};
//!
//! log::set_level(LogLevel::Info);
//! log::set_callback(|entry: LogEntry| {
//!     eprintln!("[{}] {}: {}", entry.level.as_str(), entry.component, entry.message);
//! });
//!
//! // Later, when done:
//! log::clear_callback();
//! ```

use crate::ffi;
use std::ffi::{CStr, CString};
use std::sync::Mutex;

// ---------------------------------------------------------------------------
// LogLevel
// ---------------------------------------------------------------------------

/// `LogLevel` controls the verbosity of the NWEP C library's internal logging.
///
/// Levels are ordered from most verbose ([`LogLevel::Trace`]) to least verbose
/// ([`LogLevel::Error`]). When a level is set with [`set_level`], only entries
/// at that level or higher are emitted.
///
/// The numeric discriminants match the C library's `NWEP_LOG_*` constants,
/// which allows the `PartialOrd`/`Ord` impls to behave correctly for threshold
/// comparisons.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
    /// `Trace` is the most verbose level, intended for fine-grained internal
    /// tracing (e.g., individual packet processing steps). Only enable this
    /// during active debugging; it generates very high log volume.
    Trace = 0,
    /// `Debug` is for developer-oriented diagnostic information such as
    /// connection state transitions and header values.
    Debug = 1,
    /// `Info` is for routine operational events such as connections established
    /// and requests completed.
    Info = 2,
    /// `Warn` is for recoverable anomalies that do not prevent the operation
    /// from completing but may indicate a misconfiguration or degraded state.
    Warn = 3,
    /// `Error` is for failures that require attention. At this level only
    /// conditions that cause an operation to fail are logged.
    Error = 4,
}

impl LogLevel {
    /// `as_str` returns the canonical lowercase string representation of this
    /// log level as defined by the C library (e.g., `"trace"`, `"debug"`).
    ///
    /// The returned string is a `'static` reference into the C library's string
    /// table. Falls back to `"unknown"` if the C library returns a null
    /// pointer.
    pub fn as_str(&self) -> &'static str {
        let level = match self {
            LogLevel::Trace => ffi::nwep_log_level_NWEP_LOG_TRACE,
            LogLevel::Debug => ffi::nwep_log_level_NWEP_LOG_DEBUG,
            LogLevel::Info => ffi::nwep_log_level_NWEP_LOG_INFO,
            LogLevel::Warn => ffi::nwep_log_level_NWEP_LOG_WARN,
            LogLevel::Error => ffi::nwep_log_level_NWEP_LOG_ERROR,
        };
        unsafe {
            let ptr = ffi::nwep_log_level_str(level);
            if ptr.is_null() { "unknown" } else { CStr::from_ptr(ptr).to_str().unwrap_or("unknown") }
        }
    }

    /// `to_ffi` converts this [`LogLevel`] to the raw `nwep_log_level`
    /// discriminant expected by C library functions.
    pub(crate) fn to_ffi(&self) -> ffi::nwep_log_level {
        match self {
            LogLevel::Trace => ffi::nwep_log_level_NWEP_LOG_TRACE,
            LogLevel::Debug => ffi::nwep_log_level_NWEP_LOG_DEBUG,
            LogLevel::Info => ffi::nwep_log_level_NWEP_LOG_INFO,
            LogLevel::Warn => ffi::nwep_log_level_NWEP_LOG_WARN,
            LogLevel::Error => ffi::nwep_log_level_NWEP_LOG_ERROR,
        }
    }
}

impl From<ffi::nwep_log_level> for LogLevel {
    fn from(l: ffi::nwep_log_level) -> Self {
        match l {
            ffi::nwep_log_level_NWEP_LOG_TRACE => LogLevel::Trace,
            ffi::nwep_log_level_NWEP_LOG_DEBUG => LogLevel::Debug,
            ffi::nwep_log_level_NWEP_LOG_INFO => LogLevel::Info,
            ffi::nwep_log_level_NWEP_LOG_WARN => LogLevel::Warn,
            ffi::nwep_log_level_NWEP_LOG_ERROR => LogLevel::Error,
            _ => LogLevel::Info,
        }
    }
}

// ---------------------------------------------------------------------------
// LogEntry
// ---------------------------------------------------------------------------

/// `LogEntry` is a single structured log record delivered to a Rust callback
/// registered with [`set_callback`].
///
/// The C library fills in all fields before invoking the callback. The Rust
/// binding copies the data out of the C struct and converts C strings to owned
/// `String` values, so the callback receives a fully owned, heap-allocated
/// entry that can be stored or forwarded without any lifetime constraints.
#[derive(Clone, Debug)]
pub struct LogEntry {
    /// `level` is the severity level of this log entry.
    pub level: LogLevel,

    /// `timestamp_ns` is the wall-clock time at which the entry was created,
    /// expressed as nanoseconds since the Unix epoch (see
    /// [`crate::types::Tstamp`]).
    pub timestamp_ns: u64,

    /// `trace_id` is the 16-byte distributed trace ID associated with the
    /// operation that produced this entry. An all-zero trace ID indicates that
    /// no trace context is available.
    pub trace_id: [u8; 16],

    /// `component` is the name of the C library subsystem that emitted this
    /// entry (e.g., `"quic"`, `"auth"`, `"trust"`).
    pub component: String,

    /// `message` is the human-readable log message text.
    pub message: String,
}

impl LogEntry {
    /// `format_json` serializes this log entry as a structured JSON object.
    ///
    /// The JSON is produced by the C library's `nwep_log_format_json` function
    /// into a 4096-byte internal buffer, then returned as an owned `String`.
    /// The output is a single-line JSON object without a trailing newline,
    /// suitable for newline-delimited JSON (NDJSON) log streams.
    ///
    /// # Example output
    ///
    /// ```json
    /// {"level":"info","timestamp_ns":1700000000000000000,"trace_id":"00000000000000000000000000000000","component":"myapp","message":"server started"}
    /// ```
    pub fn format_json(&self) -> String {
        let component = CString::new(self.component.as_str()).unwrap_or_default();
        let message = CString::new(self.message.as_str()).unwrap_or_default();
        let ffi_entry = ffi::nwep_log_entry {
            level: self.level.to_ffi(),
            timestamp_ns: self.timestamp_ns,
            trace_id: self.trace_id,
            component: component.as_ptr(),
            message: message.as_ptr(),
        };
        let mut buf = vec![0u8; 4096];
        let n = unsafe { ffi::nwep_log_format_json(buf.as_mut_ptr() as *mut i8, buf.len(), &ffi_entry) };
        String::from_utf8_lossy(&buf[..n]).into_owned()
    }
}

// ---------------------------------------------------------------------------
// Global log configuration
// ---------------------------------------------------------------------------

/// `set_level` sets the global minimum log level.
///
/// Log entries with a level below `level` are discarded inside the C library
/// before any sink (stderr, callback) is invoked. This setting is global and
/// affects all clients and servers in the process.
///
/// # Example
///
/// ```rust
/// use nwep::log::{set_level, LogLevel};
///
/// set_level(LogLevel::Warn); // only Warn and Error messages will be shown
/// ```
pub fn set_level(level: LogLevel) {
    unsafe { ffi::nwep_log_set_level(level.to_ffi()) }
}

/// `get_level` returns the current global minimum log level.
///
/// # Example
///
/// ```rust
/// use nwep::log::{get_level, set_level, LogLevel};
///
/// set_level(LogLevel::Debug);
/// assert_eq!(get_level(), LogLevel::Debug);
/// ```
pub fn get_level() -> LogLevel {
    LogLevel::from(unsafe { ffi::nwep_log_get_level() })
}

/// `set_json` enables or disables structured JSON formatting for the stderr
/// sink.
///
/// When `enabled` is `true`, each log line written to stderr is a
/// newline-delimited JSON object produced by the same logic as
/// [`LogEntry::format_json`]. When `false`, a human-readable plain-text format
/// is used. This setting has no effect if the stderr sink is disabled (see
/// [`set_stderr`]).
pub fn set_json(enabled: bool) {
    unsafe { ffi::nwep_log_set_json(enabled as i32) }
}

/// `set_stderr` enables or disables writing log entries to standard error.
///
/// This is the simplest way to observe C library debug output during
/// development. Pass `true` to enable and `false` to disable. The stderr sink
/// and the Rust callback sink ([`set_callback`]) operate independently; both
/// can be active simultaneously.
///
/// # Example
///
/// ```rust
/// use nwep::log::{set_stderr, set_level, LogLevel};
///
/// set_level(LogLevel::Debug);
/// set_stderr(true); // all Debug+ messages will appear on stderr
/// ```
pub fn set_stderr(enabled: bool) {
    unsafe { ffi::nwep_log_set_stderr(enabled as i32) }
}

// ---------------------------------------------------------------------------
// Callback sink
// ---------------------------------------------------------------------------

/// The type of a user-supplied log callback. The closure receives a fully owned
/// [`LogEntry`] for each log message emitted by the C library at or above the
/// current level.
type LogCallbackFn = Box<dyn Fn(LogEntry) + Send + Sync + 'static>;

/// `LOG_CALLBACK` is the global slot that holds the active Rust log callback.
///
/// A `Mutex` is used because the C library may invoke the trampoline from any
/// thread. The `Option` allows the slot to be empty when no callback is
/// installed.
static LOG_CALLBACK: Mutex<Option<LogCallbackFn>> = Mutex::new(None);

/// `log_trampoline` is the `extern "C"` function passed to the C library as
/// the log callback. It converts the raw `nwep_log_entry` pointer into a safe
/// Rust [`LogEntry`] and invokes the user closure stored in [`LOG_CALLBACK`].
///
/// If `entry` is null the trampoline returns immediately without invoking the
/// closure. The `user_data` parameter is unused; all state is carried through
/// the global `LOG_CALLBACK`.
unsafe extern "C" fn log_trampoline(entry: *const ffi::nwep_log_entry, _user_data: *mut std::ffi::c_void) {
    if entry.is_null() { return; }
    let e = &*entry;
    let component = if e.component.is_null() {
        String::new()
    } else {
        CStr::from_ptr(e.component).to_string_lossy().into_owned()
    };
    let message = if e.message.is_null() {
        String::new()
    } else {
        CStr::from_ptr(e.message).to_string_lossy().into_owned()
    };
    let log_entry = LogEntry {
        level: LogLevel::from(e.level),
        timestamp_ns: e.timestamp_ns,
        trace_id: e.trace_id,
        component,
        message,
    };
    if let Ok(guard) = LOG_CALLBACK.lock() {
        if let Some(cb) = guard.as_ref() {
            cb(log_entry);
        }
    }
}

/// `set_callback` installs a Rust closure as the global log callback.
///
/// The closure is called on whatever thread the C library uses to emit the log
/// entry, so it must be `Send + Sync`. Only one callback can be active at a
/// time; calling `set_callback` again atomically replaces the previous closure
/// before registering the new one with the C library.
///
/// The callback receives a fully owned [`LogEntry`] value. The closure may
/// block briefly (e.g., to acquire a lock on an output buffer) but should not
/// perform long-running I/O on the calling thread, as this may stall the C
/// library's event loop.
///
/// Use [`clear_callback`] to uninstall the callback when it is no longer
/// needed.
///
/// # Example
///
/// ```rust
/// use nwep::log::{set_callback, LogEntry};
///
/// set_callback(|entry: LogEntry| {
///     println!("[{}] {}", entry.level.as_str(), entry.message);
/// });
/// ```
pub fn set_callback<F: Fn(LogEntry) + Send + Sync + 'static>(cb: F) {
    let mut guard = LOG_CALLBACK.lock().unwrap();
    *guard = Some(Box::new(cb));
    drop(guard);
    unsafe { ffi::nwep_log_set_callback(Some(log_trampoline), std::ptr::null_mut()) }
}

/// `clear_callback` removes the active log callback and unregisters the
/// trampoline from the C library.
///
/// After this call the C library will no longer invoke any Rust code for log
/// entries. The previously installed closure is dropped.
///
/// It is safe to call `clear_callback` even if no callback is currently
/// installed.
pub fn clear_callback() {
    let mut guard = LOG_CALLBACK.lock().unwrap();
    *guard = None;
    drop(guard);
    unsafe { ffi::nwep_log_set_callback(None, std::ptr::null_mut()) }
}

// ---------------------------------------------------------------------------
// Log writing functions
// ---------------------------------------------------------------------------

/// `write` emits a log entry at the specified `level` through the C library's
/// logging subsystem.
///
/// The `trace_id` is a 16-byte distributed trace ID (pass `&[0u8; 16]` if no
/// trace context is available). The `component` string identifies the source of
/// the message (e.g., `"myapp::server"`). The `msg` string is the log message
/// text.
///
/// Internally the C library's `nwep_log_write` is called with a `"%s"` format
/// string to avoid interpreting any printf-style sequences in `msg`.
///
/// # Example
///
/// ```rust
/// use nwep::log::{write, LogLevel};
///
/// let tid = [0u8; 16];
/// write(LogLevel::Info, &tid, "myapp", "starting up");
/// ```
pub fn write(level: LogLevel, trace_id: &[u8; 16], component: &str, msg: &str) {
    let comp = CString::new(component).unwrap_or_default();
    let m = CString::new(msg).unwrap_or_default();
    let fmt = CString::new("%s").unwrap();
    unsafe {
        ffi::nwep_log_write(level.to_ffi(), trace_id.as_ptr(), comp.as_ptr(), fmt.as_ptr(), m.as_ptr())
    }
}

macro_rules! log_fn {
    ($name:ident, $ffi_fn:ident, $doc:expr) => {
        #[doc = $doc]
        pub fn $name(trace_id: &[u8; 16], component: &str, msg: &str) {
            let comp = CString::new(component).unwrap_or_default();
            let m = CString::new(msg).unwrap_or_default();
            let fmt = CString::new("%s").unwrap();
            unsafe { ffi::$ffi_fn(trace_id.as_ptr(), comp.as_ptr(), fmt.as_ptr(), m.as_ptr()) }
        }
    };
}

log_fn!(
    trace,
    nwep_log_trace,
    "`trace` emits a log entry at `LogLevel::Trace` level.

`trace_id` is a 16-byte distributed trace ID (use `&[0u8; 16]` when no trace
context is available). `component` identifies the source of the message and
`msg` is the log text. Internally the C library is called with a `\"%s\"` format
string so no printf-style sequences in `msg` are interpreted.

This function should only be called when the current level is
`LogLevel::Trace`; at higher levels the C library discards the entry before
any work is done.

# Example

```rust
use nwep::log::trace;
let tid = [0u8; 16];
trace(&tid, \"myapp\", \"entered request handler\");
```"
);

log_fn!(
    debug,
    nwep_log_debug,
    "`debug` emits a log entry at `LogLevel::Debug` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::debug;
let tid = [0u8; 16];
debug(&tid, \"myapp\", \"connection established\");
```"
);

log_fn!(
    info,
    nwep_log_info,
    "`info` emits a log entry at `LogLevel::Info` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::info;
let tid = [0u8; 16];
info(&tid, \"myapp\", \"server listening on :6937\");
```"
);

log_fn!(
    warn,
    nwep_log_warn,
    "`warn` emits a log entry at `LogLevel::Warn` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::warn;
let tid = [0u8; 16];
warn(&tid, \"myapp\", \"retrying after transient error\");
```"
);

log_fn!(
    error,
    nwep_log_error,
    "`error` emits a log entry at `LogLevel::Error` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::error;
let tid = [0u8; 16];
error(&tid, \"myapp\", \"fatal: failed to bind socket\");
```"
);
