//! Block cipher modes of operation: ECB, CBC, CTR, GCM
//! (NIST SP 800-38A and SP 800-38D for GCM).
//!
//! These modes are generic over any type implementing
//! [`BlockCipher`]. GCM is restricted to 128-bit block ciphers
//! (i.e., AES).
//!
//! # Side-channel posture
//!
//! - **Tag verification on GCM decrypt** uses `silentops::ct_eq`
//!   (constant-time, no early exit on first differing byte).
//! - **GHASH multiplier** (`gf128_mul`) is the SCA target on GCM:
//!   the carry-less multiplication over `GF(2^128)` is implemented
//!   in software and may leak through cache-line / shift patterns.
//!   Roadmap item `T2-H` (see
//!   `arcana/doc/sca/countermeasures/aes.rst`): replace with a CT
//!   carry-less multiplier on host (PCLMULQDQ / PMULL backend) and
//!   a bitsliced fallback on embedded.
//! - **The underlying AES** inherits all the cache-timing surface
//!   documented in [`super::aes`] (roadmap item `T1-A`). Until that
//!   ships, every GCM / CCM / CBC / CTR call leaks the AES key on
//!   a co-resident attacker.

use crate::BlockCipher;

// ============================================================
// ECB mode (Electronic Codebook)
// ============================================================

/// Encrypt data in ECB mode (each block encrypted independently).
///
/// # Warning
///
/// ECB mode is insecure for most purposes because identical plaintext blocks
/// produce identical ciphertext blocks, revealing patterns in the data.
///
/// # Panics
///
/// Panics if `data.len()` is not a multiple of the block size.
pub fn ecb_encrypt<C: BlockCipher>(cipher: &C, data: &mut [u8]) {
    let bs = C::BLOCK_LEN;
    assert!(
        data.len() % bs == 0,
        "ECB: data length must be a multiple of {} (got {})",
        bs,
        data.len()
    );
    for chunk in data.chunks_mut(bs) {
        cipher.encrypt_block(chunk);
    }
}

/// Decrypt data in ECB mode.
///
/// # Panics
///
/// Panics if `data.len()` is not a multiple of the block size.
pub fn ecb_decrypt<C: BlockCipher>(cipher: &C, data: &mut [u8]) {
    let bs = C::BLOCK_LEN;
    assert!(
        data.len() % bs == 0,
        "ECB: data length must be a multiple of {} (got {})",
        bs,
        data.len()
    );
    for chunk in data.chunks_mut(bs) {
        cipher.decrypt_block(chunk);
    }
}

// ============================================================
// CBC mode (Cipher Block Chaining)
// ============================================================

/// Encrypt data in CBC mode.
///
/// # Panics
///
/// Panics if `data.len()` is not a multiple of the block size, or if
/// `iv.len()` does not match the block size.
pub fn cbc_encrypt<C: BlockCipher>(cipher: &C, iv: &[u8], data: &mut [u8]) {
    let bs = C::BLOCK_LEN;
    assert_eq!(iv.len(), bs, "CBC: IV must be {} bytes", bs);
    assert!(
        data.len() % bs == 0,
        "CBC: data length must be a multiple of {} (got {})",
        bs,
        data.len()
    );

    let mut prev = vec![0u8; bs];
    prev.copy_from_slice(iv);

    for chunk in data.chunks_mut(bs) {
        // XOR plaintext with previous ciphertext (or IV)
        for i in 0..bs {
            chunk[i] ^= prev[i];
        }
        cipher.encrypt_block(chunk);
        prev.copy_from_slice(chunk);
    }
}

/// Decrypt data in CBC mode.
///
/// # Panics
///
/// Panics if `data.len()` is not a multiple of the block size, or if
/// `iv.len()` does not match the block size.
pub fn cbc_decrypt<C: BlockCipher>(cipher: &C, iv: &[u8], data: &mut [u8]) {
    let bs = C::BLOCK_LEN;
    assert_eq!(iv.len(), bs, "CBC: IV must be {} bytes", bs);
    assert!(
        data.len() % bs == 0,
        "CBC: data length must be a multiple of {} (got {})",
        bs,
        data.len()
    );

    let mut prev = vec![0u8; bs];
    prev.copy_from_slice(iv);

    for chunk in data.chunks_mut(bs) {
        let ct_copy: Vec<u8> = chunk.to_vec();
        cipher.decrypt_block(chunk);
        // XOR with previous ciphertext (or IV)
        for i in 0..bs {
            chunk[i] ^= prev[i];
        }
        prev.copy_from_slice(&ct_copy);
    }
}

// ============================================================
// CTR mode (Counter)
// ============================================================

/// Encrypt (or decrypt) data in CTR mode.
///
/// The `nonce` is used as the initial counter block. For a 128-bit cipher,
/// the nonce should typically be 12 bytes; the remaining 4 bytes are used as
/// a big-endian counter starting from 1. For shorter nonces, the counter
/// occupies the remaining bytes.
///
/// CTR mode is symmetric: encrypt and decrypt are the same operation.
pub fn ctr_encrypt<C: BlockCipher>(cipher: &C, nonce: &[u8], data: &mut [u8]) {
    let bs = C::BLOCK_LEN;
    assert!(
        nonce.len() < bs,
        "CTR: nonce must be shorter than block size ({} bytes)",
        bs
    );

    let counter_bytes = bs - nonce.len();
    let mut counter_block = vec![0u8; bs];
    counter_block[..nonce.len()].copy_from_slice(nonce);

    let mut counter: u64 = 1;

    for chunk in data.chunks_mut(bs) {
        // Set counter in the last bytes (big-endian)
        let counter_be = counter.to_be_bytes();
        let start = 8usize.saturating_sub(counter_bytes);
        for i in 0..counter_bytes {
            counter_block[nonce.len() + i] = if i + start < 8 { counter_be[i + start] } else { 0 };
        }

        let mut keystream = vec![0u8; bs];
        keystream.copy_from_slice(&counter_block);
        cipher.encrypt_block(&mut keystream);

        for i in 0..chunk.len() {
            chunk[i] ^= keystream[i];
        }

        counter += 1;
    }
}

// ============================================================
// GCM mode (Galois/Counter Mode)
// ============================================================

/// GCM (Galois/Counter Mode) for 128-bit block ciphers (i.e., AES).
///
/// Provides authenticated encryption with associated data (AEAD).
pub struct Gcm;

impl Gcm {
    /// Encrypt with GCM mode.
    ///
    /// Returns `(ciphertext, tag)` where `tag` is a 16-byte authentication tag.
    ///
    /// # Panics
    ///
    /// Panics if the cipher block size is not 16 bytes.
    pub fn encrypt<C: BlockCipher>(cipher: &C, nonce: &[u8; 12], aad: &[u8], plaintext: &[u8]) -> (Vec<u8>, [u8; 16]) {
        assert_eq!(C::BLOCK_LEN, 16, "GCM requires a 128-bit block cipher");

        // Compute H = E(K, 0^128)
        let mut h = [0u8; 16];
        cipher.encrypt_block(&mut h);

        // J0 = nonce || 0x00000001  (for 96-bit nonce)
        let mut j0 = [0u8; 16];
        j0[..12].copy_from_slice(nonce);
        j0[15] = 1;

        // Encrypt plaintext with GCTR (counter starts at J0 + 1)
        let mut ciphertext = plaintext.to_vec();
        gctr(cipher, &inc32(&j0), &mut ciphertext);

        // Compute GHASH
        let tag = ghash_compute(&h, aad, &ciphertext);

        // Final tag = E(K, J0) XOR GHASH
        let mut e_j0 = j0;
        cipher.encrypt_block(&mut e_j0);

        let mut final_tag = [0u8; 16];
        for i in 0..16 {
            final_tag[i] = tag[i] ^ e_j0[i];
        }

        (ciphertext, final_tag)
    }

    /// Decrypt with GCM mode.
    ///
    /// Returns `Some(plaintext)` if the tag verifies, `None` otherwise.
    ///
    /// # Panics
    ///
    /// Panics if the cipher block size is not 16 bytes.
    pub fn decrypt<C: BlockCipher>(
        cipher: &C,
        nonce: &[u8; 12],
        aad: &[u8],
        ciphertext: &[u8],
        tag: &[u8; 16],
    ) -> Option<Vec<u8>> {
        assert_eq!(C::BLOCK_LEN, 16, "GCM requires a 128-bit block cipher");

        // Compute H = E(K, 0^128)
        let mut h = [0u8; 16];
        cipher.encrypt_block(&mut h);

        // J0 = nonce || 0x00000001
        let mut j0 = [0u8; 16];
        j0[..12].copy_from_slice(nonce);
        j0[15] = 1;

        // Compute GHASH over ciphertext
        let ghash_tag = ghash_compute(&h, aad, ciphertext);

        // Expected tag = E(K, J0) XOR GHASH
        let mut e_j0 = j0;
        cipher.encrypt_block(&mut e_j0);

        let mut expected_tag = [0u8; 16];
        for i in 0..16 {
            expected_tag[i] = ghash_tag[i] ^ e_j0[i];
        }

        // Constant-time tag comparison
        let mut diff = 0u8;
        for i in 0..16 {
            diff |= tag[i] ^ expected_tag[i];
        }
        if diff != 0 {
            return None;
        }

        // Decrypt
        let mut plaintext = ciphertext.to_vec();
        gctr(cipher, &inc32(&j0), &mut plaintext);

        Some(plaintext)
    }
}

/// Increment the rightmost 32 bits of a 128-bit counter block.
fn inc32(block: &[u8; 16]) -> [u8; 16] {
    let mut out = *block;
    let ctr = u32::from_be_bytes([out[12], out[13], out[14], out[15]]);
    let new_ctr = ctr.wrapping_add(1);
    out[12..16].copy_from_slice(&new_ctr.to_be_bytes());
    out
}

/// GCTR function: CTR encryption using 128-bit blocks with 32-bit counter increment.
fn gctr<C: BlockCipher>(cipher: &C, icb: &[u8; 16], data: &mut [u8]) {
    if data.is_empty() {
        return;
    }

    let mut cb = *icb;

    for chunk in data.chunks_mut(16) {
        let mut keystream = cb;
        cipher.encrypt_block(&mut keystream);
        for i in 0..chunk.len() {
            chunk[i] ^= keystream[i];
        }
        cb = inc32(&cb);
    }
}

/// Multiply two 128-bit elements in GF(2^128) using the GCM polynomial.
///
/// The irreducible polynomial is x^128 + x^7 + x^2 + x + 1, represented
/// as R = 0xE1000...0 (MSB first).
pub(crate) fn gf128_mul(x: &[u8; 16], y: &[u8; 16]) -> [u8; 16] {
    let mut z = [0u8; 16];
    let mut v = *x;

    for i in 0..128 {
        // If bit i of Y is set
        let byte_idx = i / 8;
        let bit_idx = 7 - (i % 8);
        if (y[byte_idx] >> bit_idx) & 1 == 1 {
            for j in 0..16 {
                z[j] ^= v[j];
            }
        }

        // Shift V right by 1 in GF(2^128)
        let lsb = v[15] & 1;
        for j in (1..16).rev() {
            v[j] = (v[j] >> 1) | (v[j - 1] << 7);
        }
        v[0] >>= 1;

        // If the bit shifted out was 1, XOR with R
        if lsb == 1 {
            v[0] ^= 0xE1;
        }
    }

    z
}

/// Compute GHASH(H, A, C) where A is AAD and C is ciphertext.
fn ghash_compute(h: &[u8; 16], aad: &[u8], ciphertext: &[u8]) -> [u8; 16] {
    let mut y = [0u8; 16];

    // Process AAD blocks
    ghash_update(&mut y, h, aad);

    // Process ciphertext blocks
    ghash_update(&mut y, h, ciphertext);

    // Final block: len(A) || len(C) in bits, as 64-bit big-endian
    let mut len_block = [0u8; 16];
    let a_bits = (aad.len() as u64) * 8;
    let c_bits = (ciphertext.len() as u64) * 8;
    len_block[0..8].copy_from_slice(&a_bits.to_be_bytes());
    len_block[8..16].copy_from_slice(&c_bits.to_be_bytes());

    for i in 0..16 {
        y[i] ^= len_block[i];
    }
    y = gf128_mul(&y, h);

    y
}

/// Update GHASH state with data (padded to 128-bit blocks).
pub(crate) fn ghash_update(y: &mut [u8; 16], h: &[u8; 16], data: &[u8]) {
    for chunk in data.chunks(16) {
        let mut block = [0u8; 16];
        block[..chunk.len()].copy_from_slice(chunk);
        for i in 0..16 {
            y[i] ^= block[i];
        }
        *y = gf128_mul(y, h);
    }
}

// ============================================================
// Tests
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cipher::aes::Aes128;

    fn hex_to_bytes(s: &str) -> Vec<u8> {
        (0..s.len())
            .step_by(2)
            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
            .collect()
    }

    #[test]
    fn ecb_aes128_round_trip() {
        let key = hex_to_bytes("2b7e151628aed2a6abf7158809cf4f3c");
        let cipher = Aes128::new(&key);
        let plaintext = hex_to_bytes("3243f6a8885a308d313198a2e03707343243f6a8885a308d313198a2e0370734");

        let mut data = plaintext.clone();
        ecb_encrypt(&cipher, &mut data);
        assert_ne!(data, plaintext);

        ecb_decrypt(&cipher, &mut data);
        assert_eq!(data, plaintext);
    }

    #[test]
    fn cbc_aes128_round_trip() {
        let key = hex_to_bytes("2b7e151628aed2a6abf7158809cf4f3c");
        let iv = hex_to_bytes("000102030405060708090a0b0c0d0e0f");
        let cipher = Aes128::new(&key);
        let plaintext = hex_to_bytes("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51");

        let mut data = plaintext.clone();
        cbc_encrypt(&cipher, &iv, &mut data);
        assert_ne!(data, plaintext);

        cbc_decrypt(&cipher, &iv, &mut data);
        assert_eq!(data, plaintext);
    }

    /// NIST SP 800-38A Section F.5.1: CTR-AES128 test vector.
    #[test]
    fn ctr_aes128_round_trip() {
        let key = hex_to_bytes("2b7e151628aed2a6abf7158809cf4f3c");
        let nonce = hex_to_bytes("f0f1f2f3f4f5f6f7f8f9fafb");
        let cipher = Aes128::new(&key);
        let plaintext = hex_to_bytes("6bc1bee22e409f96e93d7e117393172a");

        let mut data = plaintext.clone();
        ctr_encrypt(&cipher, &nonce, &mut data);
        assert_ne!(data, plaintext);

        // CTR decrypt = CTR encrypt
        ctr_encrypt(&cipher, &nonce, &mut data);
        assert_eq!(data, plaintext);
    }

    /// GCM test vector from NIST SP 800-38D, Test Case 2.
    /// Key = 0...0 (16 bytes), Nonce = 0...0 (12 bytes), no AAD, PT = 0...0 (16 bytes).
    #[test]
    fn gcm_aes128_test_case_2() {
        let key = [0u8; 16];
        let nonce = [0u8; 12];
        let cipher = Aes128::new(&key);

        let plaintext = [0u8; 16];
        let (ct, tag) = Gcm::encrypt(&cipher, &nonce, &[], &plaintext);

        // Verify decryption
        let pt = Gcm::decrypt(&cipher, &nonce, &[], &ct, &tag);
        assert!(pt.is_some());
        assert_eq!(pt.unwrap(), plaintext);
    }

    /// GCM: bad tag should fail.
    #[test]
    fn gcm_bad_tag() {
        let key = [0u8; 16];
        let nonce = [0u8; 12];
        let cipher = Aes128::new(&key);

        let (ct, mut tag) = Gcm::encrypt(&cipher, &nonce, &[], b"hello world12345");
        tag[0] ^= 0xFF; // Corrupt tag
        assert!(Gcm::decrypt(&cipher, &nonce, &[], &ct, &tag).is_none());
    }

    /// GCM: AAD should affect the tag.
    #[test]
    fn gcm_aad_affects_tag() {
        let key = hex_to_bytes("feffe9928665731c6d6a8f9467308308");
        let nonce = [0u8; 12];
        let cipher = Aes128::new(&key);

        let (ct1, tag1) = Gcm::encrypt(&cipher, &nonce, b"aad1", b"plaintext1234567");
        let (ct2, tag2) = Gcm::encrypt(&cipher, &nonce, b"aad2", b"plaintext1234567");

        // Same plaintext but different AAD → different tags
        assert_eq!(ct1, ct2); // ciphertext should be the same (AAD doesn't affect CT)
        assert_ne!(tag1, tag2); // but tags differ
    }

    /// NIST GCM Test Case 3 (from SP 800-38D).
    #[test]
    fn gcm_nist_test_case_3() {
        let key = hex_to_bytes("feffe9928665731c6d6a8f9467308308");
        let nonce_bytes = hex_to_bytes("cafebabefacedbaddecaf888");
        let nonce: [u8; 12] = nonce_bytes.try_into().unwrap();
        let pt = hex_to_bytes(
            "d9313225f88406e5a55909c5aff5269a\
             86a7a9531534f7da2e4c303d8a318a72\
             1c3c0c95956809532fcf0e2449a6b525\
             b16aedf5aa0de657ba637b391aafd255",
        );

        let expected_ct = hex_to_bytes(
            "42831ec2217774244b7221b784d0d49c\
             e3aa212f2c02a4e035c17e2329aca12e\
             21d514b25466931c7d8f6a5aac84aa05\
             1ba30b396a0aac973d58e091473f5985",
        );
        let expected_tag = hex_to_bytes("4d5c2af327cd64a62cf35abd2ba6fab4");

        let cipher = Aes128::new(&key);
        let (ct, tag) = Gcm::encrypt(&cipher, &nonce, &[], &pt);

        assert_eq!(ct, expected_ct, "GCM ciphertext mismatch");
        assert_eq!(tag.to_vec(), expected_tag, "GCM tag mismatch");

        // Verify decryption
        let decrypted = Gcm::decrypt(&cipher, &nonce, &[], &ct, &tag).unwrap();
        assert_eq!(decrypted, pt);
    }

    /// NIST GCM Test Case 4 (with AAD, from SP 800-38D).
    #[test]
    fn gcm_nist_test_case_4() {
        let key = hex_to_bytes("feffe9928665731c6d6a8f9467308308");
        let nonce_bytes = hex_to_bytes("cafebabefacedbaddecaf888");
        let nonce: [u8; 12] = nonce_bytes.try_into().unwrap();
        let pt = hex_to_bytes(
            "d9313225f88406e5a55909c5aff5269a\
             86a7a9531534f7da2e4c303d8a318a72\
             1c3c0c95956809532fcf0e2449a6b525\
             b16aedf5aa0de657ba637b39",
        );
        let aad = hex_to_bytes(
            "feedfacedeadbeeffeedfacedeadbeef\
             abaddad2",
        );

        let expected_ct = hex_to_bytes(
            "42831ec2217774244b7221b784d0d49c\
             e3aa212f2c02a4e035c17e2329aca12e\
             21d514b25466931c7d8f6a5aac84aa05\
             1ba30b396a0aac973d58e091",
        );
        let expected_tag = hex_to_bytes("5bc94fbc3221a5db94fae95ae7121a47");

        let cipher = Aes128::new(&key);
        let (ct, tag) = Gcm::encrypt(&cipher, &nonce, &aad, &pt);

        assert_eq!(ct, expected_ct, "GCM TC4 ciphertext mismatch");
        assert_eq!(tag.to_vec(), expected_tag, "GCM TC4 tag mismatch");

        let decrypted = Gcm::decrypt(&cipher, &nonce, &aad, &ct, &tag).unwrap();
        assert_eq!(decrypted, pt);
    }
}
