//! `LibCtx` — `OSSL_LIB_CTX` wrapper and `Provider` — `OSSL_PROVIDER` wrapper.
//!
//! Most callers use the global default library context and never need this module.
//! Create an explicit context when:
//! - FIPS mode is required (load `"fips"` + `"base"` providers).
//! - Per-process isolation of algorithm state is needed.
//!
//! ## `Arc<LibCtx>` ownership
//!
//! OpenSSL does not expose `OSSL_LIB_CTX_up_ref`.  Rust manages the lifetime
//! via `Arc<LibCtx>`.  Algorithm descriptors that use an explicit context store
//! `Option<Arc<LibCtx>>` to extend its lifetime.

use crate::error::ErrorStack;
use native_ossl_sys as sys;
use std::ffi::CString;
use std::sync::atomic::{AtomicU64, Ordering};

static PKCS11_CFG_COUNTER: AtomicU64 = AtomicU64::new(0);

// ── LibCtx ────────────────────────────────────────────────────────────────────

/// An OpenSSL library context (`OSSL_LIB_CTX*`).
///
/// Wrap in `Arc<LibCtx>` before passing to algorithm descriptors; they will
/// clone the `Arc` to keep the context alive.
#[derive(Debug)]
pub struct LibCtx {
    ptr: *mut sys::OSSL_LIB_CTX,
    /// When `false` this `LibCtx` was created from an externally-owned pointer
    /// and must **not** call `OSSL_LIB_CTX_free` on drop.
    owned: bool,
}

impl LibCtx {
    /// Create a new, empty library context.
    ///
    /// No providers are loaded by default.  Call `load_provider` at least once
    /// before using algorithms.
    ///
    /// # Errors
    ///
    /// Returns `Err` if OpenSSL cannot allocate the context.
    pub fn new() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::OSSL_LIB_CTX_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(LibCtx { ptr, owned: true })
    }

    /// Load a provider into this library context.
    ///
    /// Common provider names:
    /// - `c"default"` — standard algorithms.
    /// - `c"fips"` — FIPS 140-3 validated algorithms (requires OpenSSL FIPS module).
    /// - `c"base"` — must be loaded alongside `"fips"` for PEM/DER encoders.
    ///
    /// The returned `Provider` keeps the provider loaded until dropped.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the provider cannot be loaded.
    pub fn load_provider(&self, name: &std::ffi::CStr) -> Result<Provider, ErrorStack> {
        let ptr = unsafe { sys::OSSL_PROVIDER_load(self.ptr, name.as_ptr()) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(Provider { ptr })
    }

    /// Return the raw `OSSL_LIB_CTX*` pointer.  Valid while `self` is alive.
    #[must_use]
    pub fn as_ptr(&self) -> *mut sys::OSSL_LIB_CTX {
        self.ptr
    }

    /// Load an `openssl.cnf`-format configuration file into this context.
    ///
    /// Providers and algorithm configuration declared in the file are activated
    /// within this context only — the global default context is not affected.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `path` contains a null byte, the file cannot be read,
    /// or the configuration is syntactically invalid.
    pub fn load_config(&self, path: &std::path::Path) -> Result<(), ErrorStack> {
        let path_str = path.to_str().ok_or_else(ErrorStack::drain)?;
        let path_cs = CString::new(path_str).map_err(|_| ErrorStack::drain())?;
        // SAFETY: self.ptr is valid for the duration of this call;
        // path_cs is NUL-terminated and lives through the FFI boundary.
        let rc = unsafe { sys::OSSL_LIB_CTX_load_config(self.ptr, path_cs.as_ptr()) };
        if rc == 1 {
            Ok(())
        } else {
            Err(ErrorStack::drain())
        }
    }

    /// Load and activate the `pkcs11` provider, pointing it at `module`.
    ///
    /// Writes a minimal `openssl.cnf` snippet to a temporary file, calls
    /// [`load_config`](Self::load_config) on it, then loads the `pkcs11`
    /// provider into this context.  The temporary file is deleted before this
    /// function returns.
    ///
    /// The returned [`Provider`] keeps the provider active until dropped.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `module` cannot be represented as a valid path string,
    /// if the temporary file cannot be written, or if the provider fails to load.
    pub fn load_pkcs11_provider(&self, module: &std::path::Path) -> Result<Provider, ErrorStack> {
        let module_str = module.to_str().ok_or_else(ErrorStack::drain)?;
        let config = format!(
            "openssl_conf = openssl_init\n\
             [openssl_init]\n\
             providers = provider_sect\n\
             [provider_sect]\n\
             pkcs11 = pkcs11_sect\n\
             default = default_sect\n\
             [pkcs11_sect]\n\
             module = pkcs11-provider.so\n\
             pkcs11-module-path = {module_str}\n\
             activate = 1\n\
             [default_sect]\n\
             activate = 1\n"
        );
        // Write config to a uniquely-named temp file, load it, then remove it.
        // Using process ID + a counter ensures uniqueness in concurrent tests.
        let seq = PKCS11_CFG_COUNTER.fetch_add(1, Ordering::Relaxed);
        let tmp_path = std::env::temp_dir().join(format!(
            "native-ossl-pkcs11-{}-{seq}.cnf",
            std::process::id()
        ));
        std::fs::write(&tmp_path, config.as_bytes()).map_err(|_| ErrorStack::drain())?;
        let result = self.load_config(&tmp_path);
        let _ = std::fs::remove_file(&tmp_path);
        result?;
        self.load_provider(c"pkcs11")
    }

    /// Wrap a raw `OSSL_LIB_CTX*` that is owned and managed externally.
    ///
    /// The resulting `LibCtx` will NOT call `OSSL_LIB_CTX_free` when dropped.
    /// Use this only when the raw pointer's lifetime is guaranteed to exceed the
    /// `LibCtx` (e.g. a context received from a FIPS provider callback).
    ///
    /// # Safety
    ///
    /// The caller must ensure that `ptr` is a valid, non-null `OSSL_LIB_CTX*`
    /// that remains valid for as long as the returned `LibCtx` is alive.
    pub unsafe fn from_raw_unowned(ptr: *mut sys::OSSL_LIB_CTX) -> Self {
        LibCtx { ptr, owned: false }
    }
}

impl Drop for LibCtx {
    fn drop(&mut self) {
        if self.owned {
            unsafe { sys::OSSL_LIB_CTX_free(self.ptr) };
        }
    }
}

// SAFETY: `OSSL_LIB_CTX` is designed to be shared across threads after setup.
unsafe impl Send for LibCtx {}
unsafe impl Sync for LibCtx {}

// ── Provider ──────────────────────────────────────────────────────────────────

/// A loaded OpenSSL provider (`OSSL_PROVIDER*`).
///
/// The provider remains loaded until this value is dropped.  Keep it alive for
/// the lifetime of any algorithm descriptors that use the associated `LibCtx`.
pub struct Provider {
    ptr: *mut sys::OSSL_PROVIDER,
}

impl Drop for Provider {
    fn drop(&mut self) {
        unsafe { sys::OSSL_PROVIDER_unload(self.ptr) };
    }
}

// SAFETY: `OSSL_PROVIDER*` is managed by the library context thread-safely.
unsafe impl Send for Provider {}
unsafe impl Sync for Provider {}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    #[test]
    fn create_and_drop() {
        let ctx = LibCtx::new().unwrap();
        drop(ctx);
    }

    #[test]
    fn load_default_provider() {
        let ctx = LibCtx::new().unwrap();
        let _prov = ctx.load_provider(c"default").unwrap();
        // Provider unloaded when `_prov` drops.
    }

    #[test]
    fn arc_shared_context() {
        let ctx = Arc::new(LibCtx::new().unwrap());
        let _prov = ctx.load_provider(c"default").unwrap();
        let ctx2 = Arc::clone(&ctx);
        drop(ctx);
        // ctx2 keeps the context alive; prov is still valid.
        drop(ctx2);
    }
}
