//! Pure Rust primitives backing the `node:crypto` / `globalThis.crypto` shims.
//!
//! See `docs/captured-effects-vfs-crypto-timers.md`. Hashing/HMAC are
//! deterministic functions of their input and run inline (flagged `CryptoHash`,
//! never call-logged). Randomness is nondeterministic and is *captured*: the
//! caller draws bytes via [`random_bytes`] (real CSPRNG) or [`seeded_bytes`]
//! (reproducible from the run's deterministic seed) and records them so a
//! resume replays the exact bytes.

use anyhow::{bail, Result};
use hmac::{Hmac, Mac};
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};

/// Compute a digest over `data` for a Node-style algorithm name. Supported:
/// `sha1`, `sha256`, `sha512` (case-insensitive, with or without dashes).
pub fn hash(algorithm: &str, data: &[u8]) -> Result<Vec<u8>> {
    match normalize_alg(algorithm).as_str() {
        "sha1" => Ok(Sha1::digest(data).to_vec()),
        "sha256" => Ok(Sha256::digest(data).to_vec()),
        "sha512" => Ok(Sha512::digest(data).to_vec()),
        other => bail!("unsupported hash algorithm `{other}` (supported: sha1, sha256, sha512)"),
    }
}

/// Compute an HMAC over `data` with `key` for a Node-style algorithm name.
pub fn hmac(algorithm: &str, key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
    match normalize_alg(algorithm).as_str() {
        "sha1" => {
            let mut mac = Hmac::<Sha1>::new_from_slice(key).expect("HMAC accepts any key length");
            mac.update(data);
            Ok(mac.finalize().into_bytes().to_vec())
        }
        "sha256" => {
            let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts any key length");
            mac.update(data);
            Ok(mac.finalize().into_bytes().to_vec())
        }
        "sha512" => {
            let mut mac = Hmac::<Sha512>::new_from_slice(key).expect("HMAC accepts any key length");
            mac.update(data);
            Ok(mac.finalize().into_bytes().to_vec())
        }
        other => bail!("unsupported hmac algorithm `{other}` (supported: sha1, sha256, sha512)"),
    }
}

/// Draw `n` cryptographically strong random bytes from the host CSPRNG. Used as
/// the first-run entropy source under `CryptoPolicy::Captured`/`Host`; the bytes
/// are then recorded so replay reproduces them.
pub fn random_bytes(n: usize) -> Vec<u8> {
    use rand::RngCore;
    let mut bytes = vec![0u8; n];
    rand::thread_rng().fill_bytes(&mut bytes);
    bytes
}

/// Produce `n` deterministic bytes from `(seed, nonce)` for
/// `CryptoPolicy::Seeded`. Reproducible across runs and resumes: the same seed
/// and nonce always yield the same bytes. Bytes are generated by hashing
/// `seed || nonce || counter` with SHA-256 and concatenating blocks — a simple
/// hash-based DRBG, sufficient for reproducible-but-not-secure test entropy.
pub fn seeded_bytes(seed: &str, nonce: u64, n: usize) -> Vec<u8> {
    let mut out = Vec::with_capacity(n);
    let mut counter: u64 = 0;
    while out.len() < n {
        let mut hasher = Sha256::new();
        hasher.update(seed.as_bytes());
        hasher.update(nonce.to_le_bytes());
        hasher.update(counter.to_le_bytes());
        out.extend_from_slice(&hasher.finalize());
        counter += 1;
    }
    out.truncate(n);
    out
}

fn normalize_alg(algorithm: &str) -> String {
    algorithm.to_ascii_lowercase().replace('-', "")
}

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

    #[test]
    fn sha256_matches_known_vector() {
        // echo -n "abc" | sha256sum
        let digest = hash("sha256", b"abc").unwrap();
        assert_eq!(
            hex::encode(digest),
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }

    #[test]
    fn sha256_accepts_dashed_and_uppercase_name() {
        assert_eq!(
            hash("SHA-256", b"abc").unwrap(),
            hash("sha256", b"abc").unwrap()
        );
    }

    #[test]
    fn hmac_sha256_matches_known_vector() {
        // HMAC-SHA256(key="key", "The quick brown fox jumps over the lazy dog")
        let mac = hmac(
            "sha256",
            b"key",
            b"The quick brown fox jumps over the lazy dog",
        )
        .unwrap();
        assert_eq!(
            hex::encode(mac),
            "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"
        );
    }

    #[test]
    fn unsupported_algorithm_errors() {
        assert!(hash("md5", b"x").is_err());
    }

    #[test]
    fn seeded_bytes_are_reproducible_and_length_correct() {
        let a = seeded_bytes("seed", 7, 40);
        let b = seeded_bytes("seed", 7, 40);
        assert_eq!(a, b);
        assert_eq!(a.len(), 40);
        // Different nonce → different stream.
        assert_ne!(seeded_bytes("seed", 8, 40), a);
    }
}
