//! `MacAlg` — `EVP_MAC` algorithm descriptor.
//!
//! Phase 3.3 delivers `MacAlg`; Phase 4.3 extends this module with
//! `MacCtx`, `HmacCtx`, and `CmacCtx`.

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

// ── MacAlg — algorithm descriptor ─────────────────────────────────────────────

/// An OpenSSL MAC algorithm descriptor (`EVP_MAC*`).
///
/// Fetched once and reused.  Implements `Clone` via `EVP_MAC_up_ref`.
pub struct MacAlg {
    ptr: *mut sys::EVP_MAC,
    /// Keeps the library context alive while this descriptor is in use.
    lib_ctx: Option<Arc<crate::lib_ctx::LibCtx>>,
}

impl MacAlg {
    /// Fetch a MAC 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_MAC_fetch(std::ptr::null_mut(), name.as_ptr(), props_ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(MacAlg { ptr, lib_ctx: None })
    }

    /// Fetch a MAC algorithm from an explicit library context.
    ///
    /// # 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_MAC_fetch(ctx.as_ptr(), name.as_ptr(), props_ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(MacAlg {
            ptr,
            lib_ctx: Some(Arc::clone(ctx)),
        })
    }

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

    /// Return the canonical name of this MAC algorithm (e.g. `"HMAC"`, `"CMAC"`).
    ///
    /// The returned reference is valid for the lifetime of `self`.
    #[must_use]
    pub fn name(&self) -> &CStr {
        // SAFETY: EVP_MAC_get0_name returns a pointer into the EVP_MAC object's
        // internal storage.  It is valid for the lifetime of the EVP_MAC*, which
        // is at least as long as &self.
        unsafe { CStr::from_ptr(sys::EVP_MAC_get0_name(self.ptr)) }
    }
}

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

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

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

// ── MacCtx — stateful context (Phase 4.3) ────────────────────────────────────

/// Stateful MAC context (`EVP_MAC_CTX*`).
///
/// `!Clone` — use `fork()` to duplicate mid-stream state.
/// All stateful operations require `&mut self`.
pub struct MacCtx {
    ptr: *mut sys::EVP_MAC_CTX,
}

impl MacCtx {
    /// Create a new (uninitialised) MAC context from an algorithm descriptor.
    ///
    /// Call [`MacCtx::init`] with a key before feeding data.
    ///
    /// # Errors
    pub fn new(alg: &MacAlg) -> Result<Self, ErrorStack> {
        let ptr = unsafe { sys::EVP_MAC_CTX_new(alg.ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(MacCtx { ptr })
    }

    /// Initialise (or re-initialise) with a key and optional parameters.
    ///
    /// **`key` is always copied** into the MAC context by `EVP_MAC_init`.
    /// HMAC additionally pads the key into IPAD/OPAD.  The copy is unavoidable.
    ///
    /// # Errors
    pub fn init(
        &mut self,
        key: &[u8],
        params: Option<&crate::params::Params<'_>>,
    ) -> Result<(), ErrorStack> {
        let params_ptr = params.map_or(crate::params::null_params(), crate::params::Params::as_ptr);
        crate::ossl_call!(sys::EVP_MAC_init(
            self.ptr,
            key.as_ptr(),
            key.len(),
            params_ptr
        ))
    }

    /// Feed data into the ongoing MAC computation.
    ///
    /// # Errors
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
        crate::ossl_call!(sys::EVP_MAC_update(self.ptr, data.as_ptr(), data.len()))
    }

    /// Finalise the MAC and write into `out`.
    ///
    /// `out` must be at least `self.mac_size()` bytes.
    /// Returns the number of bytes written.
    ///
    /// # Errors
    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
        let mut outl: usize = 0;
        crate::ossl_call!(sys::EVP_MAC_final(
            self.ptr,
            out.as_mut_ptr(),
            std::ptr::addr_of_mut!(outl),
            out.len()
        ))?;
        Ok(outl)
    }

    /// Finalise with variable-length XOF output (KMAC-128, KMAC-256).
    ///
    /// # Errors
    pub fn finish_xof(&mut self, out: &mut [u8]) -> Result<(), ErrorStack> {
        crate::ossl_call!(sys::EVP_MAC_finalXOF(self.ptr, out.as_mut_ptr(), out.len()))
    }

    /// Expected MAC output length in bytes.  Available after `init`.
    #[must_use]
    pub fn mac_size(&self) -> usize {
        unsafe { sys::EVP_MAC_CTX_get_mac_size(self.ptr) }
    }

    /// Block size of the underlying MAC algorithm in bytes.
    ///
    /// For HMAC, this is the block size of the underlying hash (e.g. 64 for
    /// SHA-256, 128 for SHA-384/SHA-512). Required by protocols that derive
    /// IPAD/OPAD or align buffers to the hash block size.
    ///
    /// Returns `0` before [`MacCtx::init`] is called.
    #[must_use]
    pub fn block_size(&self) -> usize {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - EVP_MAC_CTX_get_block_size reads algorithm metadata; no mutation
        // - &self ensures no concurrent mutable access
        unsafe { sys::EVP_MAC_CTX_get_block_size(self.ptr) }
    }

    /// Return the algorithm descriptor associated with this context.
    ///
    /// Bumps the algorithm's reference count so the returned [`MacAlg`] is
    /// independently owned and may outlive the context.
    ///
    /// Returns `None` if no algorithm is associated (e.g. context was just
    /// allocated but not yet bound to an algorithm).
    #[must_use]
    pub fn alg(&self) -> Option<MacAlg> {
        // SAFETY:
        // - self.ptr is non-null (constructor invariant)
        // - EVP_MAC_CTX_get0_mac returns a borrowed EVP_MAC* valid for the
        //   lifetime of the context; we immediately bump its refcount so the
        //   returned MacAlg owns an independent reference
        // - &self ensures the context is not mutated while we read it
        let ptr = unsafe { sys::EVP_MAC_CTX_get0_mac(self.ptr) };
        if ptr.is_null() {
            return None;
        }
        // SAFETY: ptr is non-null (checked above); EVP_MAC_up_ref only
        // increments an atomic refcount — it does not mutate algorithm data
        unsafe { sys::EVP_MAC_up_ref(ptr) };
        Some(MacAlg { ptr, lib_ctx: None })
    }

    /// Fork the current mid-stream state into a new context (`EVP_MAC_CTX_dup`).
    ///
    /// Named `fork` (not `clone`) to signal this is a potentially expensive deep copy.
    ///
    /// # Errors
    pub fn fork(&self) -> Result<MacCtx, ErrorStack> {
        let ptr = unsafe { sys::EVP_MAC_CTX_dup(self.ptr) };
        if ptr.is_null() {
            return Err(ErrorStack::drain());
        }
        Ok(MacCtx { ptr })
    }
}

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

unsafe impl Send for MacCtx {}

// ── HmacCtx — typed HMAC wrapper ─────────────────────────────────────────────

/// HMAC context bound to a specific digest algorithm.
pub struct HmacCtx(MacCtx);

impl HmacCtx {
    /// Create an HMAC context.
    ///
    /// The digest name is passed to `EVP_MAC_init` as an `OSSL_PARAM`.
    ///
    /// # Errors
    pub fn new(digest: &crate::digest::DigestAlg, key: &[u8]) -> Result<Self, ErrorStack> {
        // HMAC algorithm name — must pass the digest name as a parameter.
        let alg = MacAlg::fetch(c"HMAC", None)?;
        let mut ctx = MacCtx::new(&alg)?;

        // Build params: { digest = "<name>" }
        // We use push_utf8_string (copies) since the digest NID→name is dynamic.
        let nid = digest.nid();
        let name_cstr: std::ffi::CString = {
            let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
            if name_ptr.is_null() {
                return Err(crate::error::ErrorStack::drain());
            }
            unsafe { std::ffi::CStr::from_ptr(name_ptr) }.to_owned()
        };

        let params = crate::params::ParamBuilder::new()?
            .push_utf8_string(c"digest", &name_cstr)?
            .build()?;

        ctx.init(key, Some(&params))?;
        Ok(HmacCtx(ctx))
    }

    /// Feed data into the HMAC computation.
    ///
    /// # Errors
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
        self.0.update(data)
    }

    /// Finalise the HMAC.
    ///
    /// # Errors
    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
        self.0.finish(out)
    }

    /// Finalise and return the HMAC in a freshly allocated `Vec<u8>`.
    ///
    /// # Errors
    pub fn finish_to_vec(&mut self) -> Result<Vec<u8>, ErrorStack> {
        let size = self.0.mac_size();
        let mut out = vec![0u8; size];
        let n = self.0.finish(&mut out)?;
        out.truncate(n);
        Ok(out)
    }

    /// Expected MAC size in bytes.
    #[must_use]
    pub fn mac_size(&self) -> usize {
        self.0.mac_size()
    }

    /// One-shot HMAC computation.
    ///
    /// # Errors
    pub fn oneshot(
        digest: &crate::digest::DigestAlg,
        key: &[u8],
        data: &[u8],
        out: &mut [u8],
    ) -> Result<usize, ErrorStack> {
        let mut ctx = HmacCtx::new(digest, key)?;
        ctx.update(data)?;
        ctx.finish(out)
    }
}

// ── CmacCtx — typed CMAC wrapper ─────────────────────────────────────────────

/// CMAC context bound to a specific block cipher.
pub struct CmacCtx(MacCtx);

impl CmacCtx {
    /// Create a CMAC context.
    ///
    /// # Errors
    pub fn new(cipher: &crate::cipher::CipherAlg, key: &[u8]) -> Result<Self, ErrorStack> {
        let alg = MacAlg::fetch(c"CMAC", None)?;
        let mut ctx = MacCtx::new(&alg)?;

        // For CMAC we need the cipher name. Use OBJ_nid2sn to get it.
        // Actually, CMAC needs the cipher name as a string parameter.
        // Use a simple string: AES-256-CBC etc. (caller chose the cipher).
        // We'll use push_octet_slice since we don't have a name accessor yet.
        // For now: use a known cipher name based on key_len and block_size.
        // Better: expose CipherAlg::name() when we have it.
        // Workaround: use OSSL_CIPHER_PARAM_NAME if available.
        // For the test, just hardcode based on the cipher's key_len.
        let key_len = cipher.key_len();
        // Map key length to the corresponding AES-CBC cipher name.
        // Return Err for any length that does not correspond to a known AES variant
        // rather than silently using the wrong algorithm.
        // TODO: replace with CipherAlg::name() via EVP_CIPHER_get0_name once available.
        let cipher_name: &std::ffi::CStr = match key_len {
            16 => c"AES-128-CBC",
            24 => c"AES-192-CBC",
            32 => c"AES-256-CBC",
            _ => return Err(ErrorStack::drain()),
        };

        let params = crate::params::ParamBuilder::new()?
            .push_utf8_string(c"cipher", cipher_name)?
            .build()?;

        ctx.init(key, Some(&params))?;
        Ok(CmacCtx(ctx))
    }

    /// Feed data into the CMAC computation.
    ///
    /// # Errors
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
        self.0.update(data)
    }

    /// Finalise the CMAC.
    ///
    /// # Errors
    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
        self.0.finish(out)
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::digest::DigestAlg;
    use crate::util::hex_encode;

    #[test]
    fn fetch_hmac_succeeds() {
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        drop(alg);
    }

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

    #[test]
    fn clone_then_drop_both() {
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        let alg2 = alg.clone();
        drop(alg);
        drop(alg2);
    }

    /// HMAC-SHA256 RFC 4231 test vector #1.
    /// Key  = 0b0b...0b (20 bytes)
    /// Data = "Hi There"
    /// MAC  = b0344c61...
    #[test]
    fn hmac_sha256_rfc4231_tv1() {
        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let key = [0x0b_u8; 20];
        let data = b"Hi There";

        let mut ctx = HmacCtx::new(&digest, &key).unwrap();
        ctx.update(data).unwrap();
        let mac = ctx.finish_to_vec().unwrap();

        assert_eq!(
            hex_encode(&mac),
            "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
        );
    }

    /// Oneshot HMAC — same vector.
    #[test]
    fn hmac_sha256_oneshot() {
        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let key = [0x0b_u8; 20];
        let mut out = [0u8; 32];
        let n = HmacCtx::oneshot(&digest, &key, b"Hi There", &mut out).unwrap();
        assert_eq!(n, 32);
        assert_eq!(
            hex_encode(out),
            "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
        );
    }

    #[test]
    fn mac_ctx_block_size_before_init_is_zero() {
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        let ctx = MacCtx::new(&alg).unwrap();
        // EVP_MAC_CTX_get_block_size returns 0 when the sub-algorithm (digest)
        // has not been set yet via init.
        assert_eq!(ctx.block_size(), 0);
    }

    #[test]
    fn mac_ctx_block_size_hmac_sha256() {
        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        let mut ctx = MacCtx::new(&alg).unwrap();
        let nid = digest.nid();
        let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
        let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
        let params = crate::params::ParamBuilder::new()
            .unwrap()
            .push_utf8_string(c"digest", name)
            .unwrap()
            .build()
            .unwrap();
        ctx.init(&[0u8; 32], Some(&params)).unwrap();
        // SHA-256 uses 512-bit (64-byte) blocks.
        assert_eq!(ctx.block_size(), 64);
    }

    #[test]
    fn mac_ctx_block_size_hmac_sha512() {
        let digest = DigestAlg::fetch(c"SHA2-512", None).unwrap();
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        let mut ctx = MacCtx::new(&alg).unwrap();
        let nid = digest.nid();
        let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
        let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
        let params = crate::params::ParamBuilder::new()
            .unwrap()
            .push_utf8_string(c"digest", name)
            .unwrap()
            .build()
            .unwrap();
        ctx.init(&[0u8; 64], Some(&params)).unwrap();
        // SHA-512 uses 1024-bit (128-byte) blocks.
        assert_eq!(ctx.block_size(), 128);
    }

    #[test]
    fn mac_ctx_alg_returns_hmac_name() {
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        let ctx = MacCtx::new(&alg).unwrap();
        let retrieved = ctx.alg().expect("alg() should return Some after new()");
        assert_eq!(retrieved.name().to_bytes(), b"HMAC");
    }

    #[test]
    fn mac_ctx_alg_outlives_context() {
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        let ctx = MacCtx::new(&alg).unwrap();
        let retrieved = ctx.alg().unwrap();
        // Drop ctx; retrieved should still be valid (independent refcount).
        drop(ctx);
        assert_eq!(retrieved.name().to_bytes(), b"HMAC");
        drop(retrieved);
    }

    /// `MacCtx::fork` mid-stream — two different suffixes produce different MACs.
    #[test]
    fn mac_ctx_fork_mid_stream() {
        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
        let nid = digest.nid();
        let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
        let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
        let params = crate::params::ParamBuilder::new()
            .unwrap()
            .push_utf8_string(c"digest", name)
            .unwrap()
            .build()
            .unwrap();

        let mut ctx = MacCtx::new(&alg).unwrap();
        ctx.init(&[0u8; 32], Some(&params)).unwrap();
        ctx.update(b"common").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();

        assert_ne!(out_a, out_b);
    }
}
