//! General-purpose utilities.

use native_ossl_sys as sys;

// ── Hex encoding ─────────────────────────────────────────────────────────────

/// Encode bytes as a lowercase hexadecimal string.
#[must_use]
pub fn hex_encode<T: AsRef<[u8]>>(b: T) -> String {
    b.as_ref().iter().fold(String::new(), |mut s, x| {
        use std::fmt::Write;
        write!(s, "{x:02x}").unwrap();
        s
    })
}

/// Decode a lowercase hexadecimal string into bytes.
///
/// # Panics
///
/// Panics if `s` has odd length or contains non-hex characters.
#[must_use]
pub fn hex_decode(s: &str) -> Vec<u8> {
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
        .collect()
}

// ── Constant-time comparison ──────────────────────────────────────────────────

/// Compare two byte slices in constant time, returning `true` iff they are equal.
///
/// The comparison time is proportional to the slice length and does not depend
/// on the data values, preventing timing side-channel attacks.
///
/// Slices of different lengths return `false` immediately (without constant-time
/// behaviour), since lengths are generally not considered secret.  If you need
/// to conceal the length, pad both inputs to the same size before calling.
///
/// Backed by `CRYPTO_memcmp` from OpenSSL.
#[must_use]
pub fn ct_eq(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    if a.is_empty() {
        return true;
    }
    // SAFETY: both slices are valid for `a.len()` bytes (guaranteed by Rust)
    // and are not mutated; CRYPTO_memcmp reads them in constant time.
    let rc = unsafe {
        sys::CRYPTO_memcmp(
            a.as_ptr().cast::<std::ffi::c_void>(),
            b.as_ptr().cast::<std::ffi::c_void>(),
            a.len(),
        )
    };
    rc == 0
}

// ── SecretBuf ─────────────────────────────────────────────────────────────────

/// A heap buffer that is securely zeroed via `OPENSSL_cleanse` on drop.
///
/// Use to hold key material, passwords, and other sensitive byte sequences.
/// The zeroing is performed by OpenSSL's `OPENSSL_cleanse`, which is the
/// FIPS-approved memory-clearing function and is not eliminated by the
/// compiler's dead-store optimiser.
///
/// # Example
///
/// ```ignore
/// use native_ossl::util::SecretBuf;
///
/// let mut key = SecretBuf::with_len(32);
/// native_ossl::rand::Rand::fill(key.as_mut_slice()).unwrap();
/// // key bytes are securely erased when `key` is dropped.
/// ```
pub struct SecretBuf {
    data: Vec<u8>,
}

impl SecretBuf {
    /// Wrap an existing allocation. Takes ownership; the buffer will be
    /// securely zeroed when the `SecretBuf` is dropped.
    #[must_use]
    pub fn new(data: Vec<u8>) -> Self {
        SecretBuf { data }
    }

    /// Allocate a zero-initialised buffer of `len` bytes.
    #[must_use]
    pub fn with_len(len: usize) -> Self {
        SecretBuf {
            data: vec![0u8; len],
        }
    }

    /// Copy `data` into a new secure buffer.
    #[must_use]
    pub fn from_slice(data: &[u8]) -> Self {
        SecretBuf {
            data: data.to_vec(),
        }
    }

    /// Number of bytes in the buffer.
    #[must_use]
    pub fn len(&self) -> usize {
        self.data.len()
    }

    /// `true` if the buffer holds no bytes.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.data.is_empty()
    }

    /// Expose the buffer as a mutable byte slice.
    ///
    /// Useful for writing derived key material directly into the buffer.
    pub fn as_mut_slice(&mut self) -> &mut [u8] {
        &mut self.data
    }
}

impl AsRef<[u8]> for SecretBuf {
    fn as_ref(&self) -> &[u8] {
        &self.data
    }
}

impl Drop for SecretBuf {
    fn drop(&mut self) {
        if !self.data.is_empty() {
            unsafe {
                sys::OPENSSL_cleanse(
                    self.data.as_mut_ptr().cast::<std::ffi::c_void>(),
                    self.data.len(),
                );
            }
        }
    }
}

// SAFETY: the buffer is owned; no aliasing across threads.
unsafe impl Send for SecretBuf {}
unsafe impl Sync for SecretBuf {}

#[cfg(test)]
mod tests {
    use super::{ct_eq, SecretBuf};

    #[test]
    fn ct_eq_equal_slices() {
        assert!(ct_eq(b"hello", b"hello"));
    }

    #[test]
    fn ct_eq_different_values() {
        assert!(!ct_eq(b"hello", b"world"));
    }

    #[test]
    fn ct_eq_different_lengths() {
        assert!(!ct_eq(b"hi", b"hii"));
    }

    #[test]
    fn ct_eq_empty_slices() {
        assert!(ct_eq(b"", b""));
    }

    #[test]
    fn ct_eq_one_byte_differ() {
        assert!(!ct_eq(&[0u8; 32], &{
            let mut v = [0u8; 32];
            v[31] = 1;
            v
        }));
    }

    #[test]
    fn with_len_creates_correct_size() {
        let buf = SecretBuf::with_len(32);
        assert_eq!(buf.len(), 32);
        assert!(!buf.is_empty());
    }

    #[test]
    fn from_slice_copies_data() {
        let src = b"secret key material";
        let buf = SecretBuf::from_slice(src);
        assert_eq!(buf.as_ref(), src);
    }

    #[test]
    fn new_wraps_ownership() {
        let v = vec![1u8, 2, 3];
        let buf = SecretBuf::new(v);
        assert_eq!(buf.as_ref(), &[1, 2, 3]);
    }

    #[test]
    fn empty_buf_is_empty() {
        let buf = SecretBuf::new(vec![]);
        assert!(buf.is_empty());
        // Drop must not call cleanse on a zero-length buffer (no panic).
    }

    #[test]
    fn as_mut_slice_writes_through() {
        let mut buf = SecretBuf::with_len(4);
        buf.as_mut_slice().copy_from_slice(&[10, 20, 30, 40]);
        assert_eq!(buf.as_ref(), &[10, 20, 30, 40]);
    }
}
