use crate::zerokms::IndexKey;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use zeroize::{Zeroize, ZeroizeOnDrop};

use super::{path_values::PathSegment, ste_vec::TokenizedSelector};

type HmacSha256 = Hmac<Sha256>;

pub(crate) trait PrefixMac {
    fn finalize_reset<const N: usize>(&mut self) -> [u8; N];
}

pub(crate) trait UpdatePrefixMac<T> {
    fn update(&mut self, value: T);
}

pub(crate) trait NewPrefixMac: Sized {
    fn new_prefix_mac(key: &IndexKey, prefix: Vec<u8>) -> Self;
}

///
/// Note: this appears to be unused
///
impl<'p, M> UpdatePrefixMac<PathSegment<'p>> for M
where
    for<'s> M: PrefixMac + UpdatePrefixMac<&'s str>,
{
    fn update(&mut self, value: PathSegment<'p>) {
        match value {
            PathSegment::Root => self.update("$"),
            PathSegment::ArrayWildcardItem => self.update("[*]"),
            PathSegment::ArrayItem => self.update("[@]"),
            PathSegment::ArrayPositionItem(i) => self.update(&format!("{i}")),
            PathSegment::ObjectItem(key) => self.update(&format!(".{key}")),
        }
    }
}

impl<const N: usize, M> UpdatePrefixMac<TokenizedSelector<N>> for M
where
    M: PrefixMac + UpdatePrefixMac<[u8; N]>,
{
    fn update(&mut self, value: TokenizedSelector<N>) {
        self.update(value.as_bytes());
    }
}

#[derive(Zeroize, ZeroizeOnDrop)]
pub(super) struct Blake3PrefixMac {
    prefix: Vec<u8>,
    hasher: blake3::Hasher,
}

impl Blake3PrefixMac {
    pub fn new(key: &IndexKey, prefix: Vec<u8>) -> Self {
        let mut hasher = blake3::Hasher::new_keyed(key.key());
        hasher.update(&prefix);

        Self { prefix, hasher }
    }

    fn reset(&mut self) {
        self.hasher.reset();
        self.hasher.update(&self.prefix);
    }
}

impl PrefixMac for Blake3PrefixMac {
    fn finalize_reset<const N: usize>(&mut self) -> [u8; N] {
        let mut output = [0; N];
        self.hasher.finalize_xof().fill(&mut output);
        self.reset();
        output
    }
}

impl NewPrefixMac for Blake3PrefixMac {
    fn new_prefix_mac(key: &IndexKey, prefix: Vec<u8>) -> Self {
        Self::new(key, prefix)
    }
}

impl UpdatePrefixMac<String> for Blake3PrefixMac {
    fn update(&mut self, mut value: String) {
        self.hasher.update(value.as_bytes());
        value.zeroize();
    }
}

impl UpdatePrefixMac<&str> for Blake3PrefixMac {
    fn update(&mut self, value: &str) {
        self.hasher.update(value.as_bytes());
    }
}

impl UpdatePrefixMac<[u8; 16]> for Blake3PrefixMac {
    fn update(&mut self, mut value: [u8; 16]) {
        self.hasher.update(&value);
        value.zeroize();
    }
}

#[derive(Zeroize, ZeroizeOnDrop)]
pub(super) struct HmacSha256PrefixMac {
    prefix: Vec<u8>,
    #[zeroize(skip)]
    mac: HmacSha256,
}

impl HmacSha256PrefixMac {
    pub fn new(key: &IndexKey, prefix: Vec<u8>) -> Self {
        let mut mac = HmacSha256::new_from_slice(key.key()).expect("HMAC accepts any key length");
        mac.update(&prefix);
        Self { prefix, mac }
    }

    fn reset(&mut self) {
        self.mac.reset();
        self.mac.update(&self.prefix);
    }
}

impl PrefixMac for HmacSha256PrefixMac {
    fn finalize_reset<const N: usize>(&mut self) -> [u8; N] {
        const {
            assert!(N <= 32, "HmacSha256PrefixMac supports N <= 32 only");
        }
        let tag = self.mac.finalize_reset().into_bytes();
        let mut out = [0u8; N];
        out.copy_from_slice(&tag[..N]);
        self.reset();
        out
    }
}

impl NewPrefixMac for HmacSha256PrefixMac {
    fn new_prefix_mac(key: &IndexKey, prefix: Vec<u8>) -> Self {
        Self::new(key, prefix)
    }
}

impl UpdatePrefixMac<String> for HmacSha256PrefixMac {
    fn update(&mut self, mut value: String) {
        self.mac.update(value.as_bytes());
        value.zeroize();
    }
}

impl UpdatePrefixMac<&str> for HmacSha256PrefixMac {
    fn update(&mut self, value: &str) {
        self.mac.update(value.as_bytes());
    }
}

impl UpdatePrefixMac<[u8; 16]> for HmacSha256PrefixMac {
    fn update(&mut self, mut value: [u8; 16]) {
        self.mac.update(&value);
        value.zeroize();
    }
}

#[cfg(test)]
mod test {
    // TODO: test that info gets used properly after reset
    // TODO: Check that 2 different indexers with different prefixes produce different results with the same key
    // TODO: Check that 2 different indexers with the same prefix produce different results with different keys
}

#[cfg(test)]
mod new_prefix_mac_tests {
    use super::*;
    use crate::zerokms::IndexKey;

    fn make_with<M: NewPrefixMac>(key: &IndexKey, prefix: Vec<u8>) -> M {
        M::new_prefix_mac(key, prefix)
    }

    #[test]
    fn test_blake3_constructs_via_trait() {
        let key = IndexKey::from([0u8; 32]);
        let mut m: Blake3PrefixMac = make_with(&key, b"p".to_vec());
        m.update("x");
        let _: [u8; 16] = m.finalize_reset();
    }

    #[test]
    fn test_hmac_constructs_via_trait() {
        let key = IndexKey::from([0u8; 32]);
        let mut m: HmacSha256PrefixMac = make_with(&key, b"p".to_vec());
        m.update("x");
        let _: [u8; 16] = m.finalize_reset();
    }
}

#[cfg(test)]
mod hmac_tests {
    use super::*;
    use crate::zerokms::IndexKey;

    #[test]
    fn test_hmac_finalize_reset_16_truncates_first_16_bytes() {
        let key = IndexKey::from([1u8; 32]);
        let mut macca = HmacSha256PrefixMac::new(&key, b"prefix".to_vec());
        macca.update("hello");
        let out_16: [u8; 16] = macca.finalize_reset();

        // Recompute and compare: the truncated output must equal the first
        // 16 bytes of the full 32-byte HMAC-SHA256 tag.
        let mut macca2 = HmacSha256PrefixMac::new(&key, b"prefix".to_vec());
        macca2.update("hello");
        let out_32: [u8; 32] = macca2.finalize_reset();

        assert_eq!(&out_32[..16], &out_16[..]);
    }

    #[test]
    fn test_hmac_different_keys_produce_different_output() {
        let mut a = HmacSha256PrefixMac::new(&IndexKey::from([1u8; 32]), b"p".to_vec());
        a.update("x");
        let mut b = HmacSha256PrefixMac::new(&IndexKey::from([2u8; 32]), b"p".to_vec());
        b.update("x");
        assert_ne!(a.finalize_reset::<32>(), b.finalize_reset::<32>());
    }

    #[test]
    fn test_hmac_different_prefixes_produce_different_output() {
        let key = IndexKey::from([1u8; 32]);
        let mut a = HmacSha256PrefixMac::new(&key, b"p1".to_vec());
        a.update("x");
        let mut b = HmacSha256PrefixMac::new(&key, b"p2".to_vec());
        b.update("x");
        assert_ne!(a.finalize_reset::<32>(), b.finalize_reset::<32>());
    }

    #[test]
    fn test_hmac_reset_after_finalize() {
        let key = IndexKey::from([1u8; 32]);
        let mut macca = HmacSha256PrefixMac::new(&key, b"p".to_vec());
        macca.update("first");
        let _first: [u8; 32] = macca.finalize_reset();
        macca.update("first");
        let second: [u8; 32] = macca.finalize_reset();

        let mut fresh = HmacSha256PrefixMac::new(&key, b"p".to_vec());
        fresh.update("first");
        let fresh_out: [u8; 32] = fresh.finalize_reset();

        // After reset, hashing the same input must produce the same output as
        // a fresh instance with the same input.
        assert_eq!(second, fresh_out);
    }
}
