use aes_gcm::{AeadCore, AeadInPlace, Aes128Gcm, Aes256Gcm, Key, KeyInit};
use rand::rngs::OsRng;
use zerocopy::{AsBytes, FromBytes};

use crate::tunnel::packet_def::{StandardAeadTail, ZCPacket};

use super::{Encryptor, Error};

#[derive(Clone)]
pub struct AesGcmCipher {
    pub(crate) cipher: AesGcmEnum,
}

#[derive(Clone)]
pub enum AesGcmEnum {
    AES128GCM(Box<Aes128Gcm>),
    AES256GCM(Box<Aes256Gcm>),
}

impl AesGcmCipher {
    pub fn new_128(key: [u8; 16]) -> Self {
        let key: &Key<Aes128Gcm> = &key.into();
        Self {
            cipher: AesGcmEnum::AES128GCM(Box::new(Aes128Gcm::new(key))),
        }
    }
    pub fn new_256(key: [u8; 32]) -> Self {
        let key: &Key<Aes256Gcm> = &key.into();
        Self {
            cipher: AesGcmEnum::AES256GCM(Box::new(Aes256Gcm::new(key))),
        }
    }
}

impl Encryptor for AesGcmCipher {
    fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> {
        let pm_header = zc_packet.peer_manager_header().unwrap();
        if !pm_header.is_encrypted() {
            return Ok(());
        }

        let payload_len = zc_packet.payload().len();
        if payload_len < StandardAeadTail::SIZE {
            return Err(Error::PacketTooShort(zc_packet.payload().len()));
        }

        let text_len = payload_len - StandardAeadTail::SIZE;

        let aes_tail = StandardAeadTail::ref_from_suffix(zc_packet.payload())
            .unwrap()
            .clone();

        let nonce = aes_tail.nonce.into();
        let tag = aes_tail.tag.into();

        let rs = match &self.cipher {
            AesGcmEnum::AES128GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached(
                &nonce,
                &[],
                &mut zc_packet.mut_payload()[..text_len],
                &tag,
            ),
            AesGcmEnum::AES256GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached(
                &nonce,
                &[],
                &mut zc_packet.mut_payload()[..text_len],
                &tag,
            ),
        };

        if let Err(e) = rs {
            println!("error: {:?}", e.to_string());
            return Err(Error::DecryptionFailed);
        }

        let pm_header = zc_packet.mut_peer_manager_header().unwrap();
        pm_header.set_encrypted(false);
        let old_len = zc_packet.buf_len();
        zc_packet
            .mut_inner()
            .truncate(old_len - StandardAeadTail::SIZE);
        Ok(())
    }

    fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> {
        self.encrypt_with_nonce(zc_packet, None)
    }

    fn encrypt_with_nonce(
        &self,
        zc_packet: &mut ZCPacket,
        nonce: Option<&[u8]>,
    ) -> Result<(), Error> {
        let pm_header = zc_packet.peer_manager_header().unwrap();
        if pm_header.is_encrypted() {
            tracing::warn!(?zc_packet, "packet is already encrypted");
            return Ok(());
        }

        let nonce = nonce
            .map(|n| {
                <[u8; StandardAeadTail::NONCE_SIZE]>::try_from(n)
                    .map(Into::into)
                    .map_err(|_| Error::EncryptionFailed)
            })
            .transpose()?;

        let (tag, nonce) = match &self.cipher {
            AesGcmEnum::AES128GCM(aes_gcm) => {
                let nonce = nonce.unwrap_or_else(|| Aes128Gcm::generate_nonce(&mut OsRng));
                (
                    aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()),
                    nonce,
                )
            }
            AesGcmEnum::AES256GCM(aes_gcm) => {
                let nonce = nonce.unwrap_or_else(|| Aes256Gcm::generate_nonce(&mut OsRng));
                (
                    aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()),
                    nonce,
                )
            }
        };

        let tail = StandardAeadTail {
            tag: tag.map_err(|_| Error::EncryptionFailed)?.into(),
            nonce: nonce.into(),
        };

        let pm_header = zc_packet.mut_peer_manager_header().unwrap();
        pm_header.set_encrypted(true);
        zc_packet.mut_inner().extend_from_slice(tail.as_bytes());
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        peers::encrypt::{Encryptor, aes_gcm::AesGcmCipher},
        tunnel::packet_def::{StandardAeadTail, ZCPacket},
    };
    use zerocopy::FromBytes;

    #[test]
    fn test_aes_gcm_cipher() {
        let key = [0u8; 16];
        let cipher = AesGcmCipher::new_128(key);
        let text = b"1234567";
        let mut packet = ZCPacket::new_with_payload(text);
        packet.fill_peer_manager_hdr(0, 0, 0);
        cipher.encrypt(&mut packet).unwrap();
        assert_eq!(packet.payload().len(), text.len() + StandardAeadTail::SIZE);
        assert!(packet.peer_manager_header().unwrap().is_encrypted());

        cipher.decrypt(&mut packet).unwrap();
        assert_eq!(packet.payload(), text);
        assert!(!packet.peer_manager_header().unwrap().is_encrypted());
    }

    #[test]
    fn test_aes_gcm_cipher_with_nonce() {
        let key = [7u8; 16];
        let cipher = AesGcmCipher::new_128(key);
        let text = b"Hello";
        let nonce = [3u8; 12];

        let mut packet1 = ZCPacket::new_with_payload(text);
        packet1.fill_peer_manager_hdr(0, 0, 0);
        cipher
            .encrypt_with_nonce(&mut packet1, Some(&nonce))
            .unwrap();

        let mut packet2 = ZCPacket::new_with_payload(text);
        packet2.fill_peer_manager_hdr(0, 0, 0);
        cipher
            .encrypt_with_nonce(&mut packet2, Some(&nonce))
            .unwrap();

        assert_eq!(packet1.payload(), packet2.payload());

        let tail = StandardAeadTail::ref_from_suffix(packet1.payload()).unwrap();
        assert_eq!(tail.nonce, nonce);

        cipher.decrypt(&mut packet1).unwrap();
        assert_eq!(packet1.payload(), text);
    }
}
