//! `DigestAlg` — `EVP_MD` algorithm descriptor, and `DigestCtx` — stateful context.
//!
//! Phase 3.1 delivers `DigestAlg`; Phase 4.1 extends this module with `DigestCtx`.

use crate::error::ErrorStack;
use native_ossl_sys as sys;
use std::ffi::CStr;
use std::sync::Arc;

// ── DigestAlg — algorithm descriptor ─────────────────────────────────────────

/// An OpenSSL digest algorithm descriptor (`EVP_MD*`).
///
/// Fetched once and reused.  Implements `Clone` via `EVP_MD_up_ref`.
#[derive(Debug)]
pub struct DigestAlg {
    ptr: *mut sys::EVP_MD,
    /// Keeps the library context alive while this descriptor is in use.
    lib_ctx: Option<Arc<crate::lib_ctx::LibCtx>>,
}

impl DigestAlg {
    /// Fetch a digest algorithm from the global default library context.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the algorithm is not available.
    pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack> {
        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
        let ptr = unsafe { sys::EVP_MD_fetch(std::ptr::null_mut(), name.as_ptr(), props_ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(DigestAlg { ptr, lib_ctx: None })
    }

    /// Fetch a digest algorithm from an explicit library context.
    ///
    /// The `Arc` is cloned and held so the context outlives this descriptor.
    ///
    /// # Errors
    pub fn fetch_in(
        ctx: &Arc<crate::lib_ctx::LibCtx>,
        name: &CStr,
        props: Option<&CStr>,
    ) -> Result<Self, ErrorStack> {
        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
        let ptr = unsafe { sys::EVP_MD_fetch(ctx.as_ptr(), name.as_ptr(), props_ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(DigestAlg {
            ptr,
            lib_ctx: Some(Arc::clone(ctx)),
        })
    }

    /// Digest output size in bytes (e.g. 32 for SHA-256).
    #[must_use]
    pub fn output_len(&self) -> usize {
        usize::try_from(unsafe { sys::EVP_MD_get_size(self.ptr) }).unwrap_or(0)
    }

    /// Block size in bytes (e.g. 64 for SHA-256).
    #[must_use]
    pub fn block_size(&self) -> usize {
        usize::try_from(unsafe { sys::EVP_MD_get_block_size(self.ptr) }).unwrap_or(0)
    }

    /// NID (numeric identifier) of the digest algorithm.
    #[must_use]
    pub fn nid(&self) -> i32 {
        unsafe { sys::EVP_MD_get_type(self.ptr) }
    }

    /// Return the raw `EVP_MD*` pointer.  Valid for the lifetime of `self`.
    #[must_use]
    pub fn as_ptr(&self) -> *const sys::EVP_MD {
        self.ptr
    }
}

impl Clone for DigestAlg {
    fn clone(&self) -> Self {
        unsafe { sys::EVP_MD_up_ref(self.ptr) };
        DigestAlg {
            ptr: self.ptr,
            lib_ctx: self.lib_ctx.clone(),
        }
    }
}

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

// SAFETY: `EVP_MD*` is reference-counted and immutable after fetch.
unsafe impl Send for DigestAlg {}
unsafe impl Sync for DigestAlg {}

// ── DigestCtx — stateful context (Phase 4.1) ─────────────────────────────────

/// Stateful hash context (`EVP_MD_CTX*`).
///
/// `!Clone` — use `fork()` to duplicate mid-stream state.
/// All stateful operations require `&mut self` (exclusive ownership).
#[derive(Debug)]
pub struct DigestCtx {
    ptr: *mut sys::EVP_MD_CTX,
}

impl DigestCtx {
    /// Feed data into the ongoing hash computation.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `EVP_DigestUpdate` fails.
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
        crate::ossl_call!(sys::EVP_DigestUpdate(
            self.ptr,
            data.as_ptr().cast(),
            data.len()
        ))
    }

    /// Finalise the hash and write the result into `out`.
    ///
    /// `out` must be at least `alg.output_len()` bytes.
    /// Returns the number of bytes written.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `EVP_DigestFinal_ex` fails.
    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
        let mut len: u32 = 0;
        crate::ossl_call!(sys::EVP_DigestFinal_ex(
            self.ptr,
            out.as_mut_ptr(),
            std::ptr::addr_of_mut!(len)
        ))?;
        Ok(usize::try_from(len).unwrap_or(0))
    }

    /// Finalise with XOF (extendable-output) mode.
    ///
    /// Used by SHAKE-128, SHAKE-256.  `out.len()` determines the output length.
    ///
    /// # Errors
    pub fn finish_xof(&mut self, out: &mut [u8]) -> Result<(), ErrorStack> {
        crate::ossl_call!(sys::EVP_DigestFinalXOF(
            self.ptr,
            out.as_mut_ptr(),
            out.len()
        ))
    }

    /// Fork the current mid-stream state into a new context.
    ///
    /// Equivalent to `EVP_MD_CTX_copy_ex` — a deep copy of the context state.
    /// Named `fork` (not `clone`) to signal the operation is potentially expensive.
    ///
    /// # Errors
    pub fn fork(&self) -> Result<DigestCtx, ErrorStack> {
        let new_ctx = unsafe { sys::EVP_MD_CTX_new() };
        if new_ctx.is_null() {
            return Err(ErrorStack::drain());
        }
        crate::ossl_call!(sys::EVP_MD_CTX_copy_ex(new_ctx, self.ptr))?;
        Ok(DigestCtx { ptr: new_ctx })
    }

    /// Serialize a live mid-computation digest context to opaque bytes.
    ///
    /// Calls `EVP_MD_CTX_serialize`, which allocates a buffer via
    /// `OPENSSL_malloc` and writes the pointer and length to out-params.
    /// The buffer is copied into a `Vec<u8>` and then freed with
    /// `CRYPTO_free` (the real symbol behind the `OPENSSL_free` macro,
    /// which bindgen cannot bind directly).
    ///
    /// The bytes are version-specific to this OpenSSL build and cannot be
    /// exchanged between different OpenSSL versions.
    ///
    /// Only available when built against OpenSSL ≥ 4.0.
    ///
    /// # Errors
    ///
    /// Returns `Err` if serialization fails (e.g. the context has not been
    /// initialised or the algorithm does not support serialization).
    #[cfg(ossl_v400)]
    pub fn serialize(&self) -> Result<Vec<u8>, ErrorStack> {
        let mut len: usize = 0;
        // First call with NULL out to query the required buffer size.
        let rc = unsafe { sys::EVP_MD_CTX_serialize(self.ptr, std::ptr::null_mut(), &raw mut len) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        let mut buf = vec![0u8; len];
        // Second call with the allocated buffer to write the serialized state.
        let rc = unsafe { sys::EVP_MD_CTX_serialize(self.ptr, buf.as_mut_ptr(), &raw mut len) };
        if rc != 1 {
            return Err(ErrorStack::drain());
        }
        buf.truncate(len);
        Ok(buf)
    }

    /// Restore a digest context from bytes produced by [`DigestCtx::serialize`].
    ///
    /// The context must already be initialised with the same algorithm before
    /// calling `deserialize()`.  After a successful call the context is in
    /// exactly the same state as when `serialize()` was called.
    ///
    /// Only available when built against OpenSSL ≥ 4.0.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the data is invalid or the algorithm does not support
    /// deserialization.
    #[cfg(ossl_v400)]
    pub fn deserialize(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - data.as_ptr() is valid for data.len() bytes for the duration of this call
        // - &mut self ensures exclusive access to the context
        crate::ossl_call!(sys::EVP_MD_CTX_deserialize(
            self.ptr,
            data.as_ptr(),
            data.len()
        ))
    }

    /// Allocate an uninitialised `EVP_MD_CTX`.
    ///
    /// The context is not associated with any algorithm yet.  Use this when a
    /// raw context handle is needed before a higher-level init call (e.g.
    /// `EVP_DigestSignInit_ex`).
    ///
    /// # Errors
    ///
    /// Returns `Err` if `EVP_MD_CTX_new` fails.
    pub fn new_empty() -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::EVP_MD_CTX_new() };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(DigestCtx { ptr })
    }

    /// Reinitialise this context for reuse with the given algorithm.
    ///
    /// Calls `EVP_DigestInit_ex2`.  Pass `None` for `params` when no extra
    /// initialisation parameters are required.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `EVP_DigestInit_ex2` fails.
    pub fn reinit(
        &mut self,
        alg: &DigestAlg,
        params: Option<&crate::params::Params<'_>>,
    ) -> Result<(), ErrorStack> {
        crate::ossl_call!(sys::EVP_DigestInit_ex2(
            self.ptr,
            alg.ptr,
            params.map_or(std::ptr::null(), super::params::Params::as_ptr),
        ))
    }

    /// Construct a `DigestCtx` from a raw, owned `EVP_MD_CTX*`.
    ///
    /// # Safety
    ///
    /// `ptr` must be a valid, non-null `EVP_MD_CTX*` that the caller is giving up ownership of.
    /// The context need not be initialised with a digest algorithm yet.
    pub unsafe fn from_ptr(ptr: *mut sys::EVP_MD_CTX) -> Self {
        DigestCtx { ptr }
    }

    /// Return the algorithm associated with this context, if any.
    ///
    /// Returns `None` when the context was created with [`DigestCtx::new_empty`]
    /// and has not yet been initialised via [`DigestCtx::reinit`].
    ///
    /// The returned [`DigestAlg`] holds an independent reference (refcount bumped
    /// via `EVP_MD_up_ref`) and may safely outlive `self`.
    #[must_use]
    pub fn alg(&self) -> Option<DigestAlg> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant: Err is returned on null,
        //   null is never stored in DigestCtx)
        // - EVP_MD_CTX_get0_md borrows the algorithm pointer; it is valid for at
        //   least the lifetime of this context; we immediately bump the refcount
        //   before returning so the caller's DigestAlg owns an independent reference
        // - &self ensures no concurrent mutation of the context while we read it
        let md_ptr = unsafe { sys::EVP_MD_CTX_get0_md(self.ptr) };
        if md_ptr.is_null() {
            return None;
        }
        // Bump refcount so the returned DigestAlg is independently owned.
        // SAFETY: md_ptr is non-null (checked above); the cast to *mut is safe
        // because EVP_MD_up_ref only atomically increments a counter — it does
        // not mutate algorithm data — and OpenSSL's API requires a mutable pointer
        // for refcount operations even on logically immutable objects.
        unsafe { sys::EVP_MD_up_ref(md_ptr.cast_mut()) };
        Some(DigestAlg {
            ptr: md_ptr.cast_mut(),
            lib_ctx: None,
        })
    }

    /// Return the raw `EVP_MD_CTX*` pointer.  Valid for the lifetime of `self`.
    ///
    /// Used by `Signer`/`Verifier` which call `EVP_DigestSign*`
    /// on this context directly.  Returns a mutable pointer because most
    /// OpenSSL EVP functions require `EVP_MD_CTX*` even for logically
    /// read-only operations.
    #[must_use]
    pub fn as_ptr(&self) -> *mut sys::EVP_MD_CTX {
        self.ptr
    }
}

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

// `EVP_MD_CTX` has no `up_ref` — it is `!Clone` and exclusively owned.
// `Send` is safe because no thread-local state is stored in the context.
// `Sync` is safe because Rust's &self/&mut self discipline prevents concurrent
// mutation; read-only access from multiple threads is safe for EVP_MD_CTX.
unsafe impl Send for DigestCtx {}
unsafe impl Sync for DigestCtx {}

impl DigestAlg {
    /// Create a new digest context initialised with this algorithm.
    ///
    /// # Errors
    ///
    /// Returns `Err` if context allocation or init fails.
    pub fn new_context(&self) -> Result<DigestCtx, ErrorStack> {
        let ctx_ptr = unsafe { sys::EVP_MD_CTX_new() };
        if ctx_ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        crate::ossl_call!(sys::EVP_DigestInit_ex2(ctx_ptr, self.ptr, std::ptr::null())).map_err(
            |e| {
                unsafe { sys::EVP_MD_CTX_free(ctx_ptr) };
                e
            },
        )?;
        Ok(DigestCtx { ptr: ctx_ptr })
    }

    /// Compute a digest in a single call (one-shot path).
    ///
    /// Zero-copy: reads from `data`, writes into `out`.
    /// `out` must be at least `self.output_len()` bytes.
    ///
    /// # Errors
    pub fn digest(&self, data: &[u8], out: &mut [u8]) -> Result<usize, ErrorStack> {
        let mut len: u32 = 0;
        crate::ossl_call!(sys::EVP_Digest(
            data.as_ptr().cast(),
            data.len(),
            out.as_mut_ptr(),
            std::ptr::addr_of_mut!(len),
            self.ptr,
            std::ptr::null_mut()
        ))?;
        Ok(usize::try_from(len).unwrap_or(0))
    }

    /// Compute a digest and return it in a freshly allocated `Vec<u8>`.
    ///
    /// # Errors
    pub fn digest_to_vec(&self, data: &[u8]) -> Result<Vec<u8>, ErrorStack> {
        let mut out = vec![0u8; self.output_len()];
        let len = self.digest(data, &mut out)?;
        out.truncate(len);
        Ok(out)
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::util::{hex_decode, hex_encode};

    #[test]
    fn fetch_sha256_properties() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        assert_eq!(alg.output_len(), 32);
        assert_eq!(alg.block_size(), 64);
    }

    #[test]
    fn fetch_nonexistent_fails() {
        assert!(DigestAlg::fetch(c"NONEXISTENT_DIGEST_XYZ", None).is_err());
    }

    #[test]
    fn clone_then_drop_both() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let alg2 = alg.clone();
        // Drop both — must not double-free.
        drop(alg);
        drop(alg2);
    }

    /// SHA-256("abc") known-answer test (verified against OpenSSL CLI + Python hashlib).
    #[test]
    fn sha256_known_answer() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let mut ctx = alg.new_context().unwrap();
        ctx.update(b"abc").unwrap();
        let mut out = [0u8; 32];
        let n = ctx.finish(&mut out).unwrap();
        assert_eq!(n, 32);
        assert_eq!(
            hex_encode(out),
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }

    /// Same vector via oneshot path.
    #[test]
    fn sha256_oneshot() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let got = alg.digest_to_vec(b"abc").unwrap();
        let expected =
            hex_decode("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
        assert_eq!(got, expected);
    }

    /// Fork mid-stream — two independent suffix completions.
    #[test]
    fn fork_mid_stream() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let mut ctx = alg.new_context().unwrap();
        ctx.update(b"common prefix").unwrap();

        let mut fork = ctx.fork().unwrap();

        ctx.update(b" A").unwrap();
        fork.update(b" B").unwrap();

        let mut out_a = [0u8; 32];
        let mut out_b = [0u8; 32];
        ctx.finish(&mut out_a).unwrap();
        fork.finish(&mut out_b).unwrap();

        // Different suffixes → different digests.
        assert_ne!(out_a, out_b);
    }

    /// Verify that serialize/deserialize round-trip preserves mid-stream state.
    ///
    /// Strategy: hash "hello" in context A, serialize, restore into context B,
    /// then append " world" in both and confirm identical final digests.
    #[cfg(ossl_v400)]
    #[test]
    fn digest_ctx_serialize_roundtrip() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();

        // Init context A and hash "hello".
        let mut ctx_a = alg.new_context().unwrap();
        ctx_a.update(b"hello").unwrap();

        // Serialize mid-stream state into an owned Vec<u8>.
        let state = ctx_a.serialize().unwrap();
        assert!(!state.is_empty(), "serialized state must be non-empty");

        // Continue context A with " world" and finalize.
        ctx_a.update(b" world").unwrap();
        let mut out_a = [0u8; 32];
        ctx_a.finish(&mut out_a).unwrap();

        // Restore mid-stream state into a fresh context B, apply same suffix.
        let mut ctx_b = alg.new_context().unwrap();
        ctx_b.deserialize(&state).unwrap();
        ctx_b.update(b" world").unwrap();
        let mut out_b = [0u8; 32];
        ctx_b.finish(&mut out_b).unwrap();

        assert_eq!(out_a, out_b, "restored context produced different digest");
    }

    // ── DigestCtx::alg() tests ────────────────────────────────────────────────

    /// Initialised context returns the algorithm it was created with.
    #[test]
    fn digest_ctx_alg_returns_some_after_init() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let ctx = alg.new_context().unwrap();
        let retrieved = ctx
            .alg()
            .expect("alg() must return Some on an initialised context");
        // Properties must match the original algorithm.
        assert_eq!(retrieved.output_len(), alg.output_len());
        assert_eq!(retrieved.block_size(), alg.block_size());
        assert_eq!(retrieved.nid(), alg.nid());
    }

    /// Empty (uninitialised) context returns None.
    #[test]
    fn digest_ctx_alg_returns_none_before_init() {
        let ctx = DigestCtx::new_empty().unwrap();
        assert!(
            ctx.alg().is_none(),
            "alg() must return None on an empty context"
        );
    }

    /// The returned `DigestAlg` is independently owned and lives past the context.
    #[test]
    fn digest_ctx_alg_outlives_context() {
        let alg_ref = DigestAlg::fetch(c"SHA2-512", None).unwrap();
        let retrieved = {
            let ctx = alg_ref.new_context().unwrap();
            ctx.alg().unwrap()
        }; // ctx dropped here; retrieved must still be valid
        assert_eq!(retrieved.output_len(), 64);
    }

    /// `alg()` on a forked context returns the same algorithm.
    #[test]
    fn digest_ctx_alg_after_fork() {
        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let mut ctx = alg.new_context().unwrap();
        ctx.update(b"data").unwrap();
        let forked = ctx.fork().unwrap();
        let retrieved = forked
            .alg()
            .expect("alg() must return Some on a forked context");
        assert_eq!(retrieved.output_len(), alg.output_len());
    }
}
