use crate::crypt::symmetric::data::{
    InboundEncryptedMessage, InboundPlainMessage, Iv, OutboundEncryptedMessage,
    OutboundPlainMessage,
};

pub mod aes_gcm;
pub mod chacha20poly1305;
pub mod data;

pub const V1_CIPHER_AES_128_GCM: u8 = 0x10;
pub const V1_CIPHER_AES_256_GCM: u8 = 0x11;
pub const V1_CIPHER_CHACHA20POLY1305: u8 = 0x12;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Invalid symmetric ciphersuite: {0:#02x}")]
    InvalidCiphersuite(u8),
    #[error("Invalid key size")]
    InvalidKeySize,
    #[error("Decryption error")]
    DecryptError,
    #[error("Encryption error")]
    EncryptError,
}

pub struct SymmetricCipher {
    pub encrypter: Box<dyn Encrypter>,
    pub decrypter: Box<dyn Decrypter>,
}

pub trait CiphersuiteAlgorithm {
    fn encrypter(&self, key: &[u8], iv: &Iv) -> Result<Box<dyn Encrypter>, Error>;
    fn decrypter(&self, key: &[u8], iv: &Iv) -> Result<Box<dyn Decrypter>, Error>;
    fn key_size(&self) -> usize;
}

pub trait Encrypter {
    fn encrypt(
        &mut self,
        seq: u64,
        msg: OutboundPlainMessage<'_>,
    ) -> Result<OutboundEncryptedMessage, Error>;
}

pub trait Decrypter {
    fn decrypt<'a>(
        &mut self,
        seq: u64,
        msg: InboundEncryptedMessage<'a>,
    ) -> Result<InboundPlainMessage<'a>, Error>;
}

pub fn make_aad(total_len: usize, seq: u64) -> [u8; 4] {
    [
        total_len as u8,
        (total_len >> 8) as u8,
        seq as u8,
        (seq >> 8) as u8,
    ]
}

pub fn ciphersuite(id: u8) -> Option<(&'static dyn CiphersuiteAlgorithm, &'static str)> {
    match id {
        V1_CIPHER_AES_128_GCM => Some((&aes_gcm::AES128GCM, "aes-128-gcm")),
        V1_CIPHER_AES_256_GCM => Some((&aes_gcm::AES256GCM, "aes-256-gcm")),
        V1_CIPHER_CHACHA20POLY1305 => {
            Some((&chacha20poly1305::CHACHA20POLY1305, "chacha20poly1305"))
        }
        _ => None,
    }
}

impl SymmetricCipher {
    pub fn from_ciphersuite(
        ciphersuite: &dyn CiphersuiteAlgorithm,
        key: &[u8],
        iv: Iv,
    ) -> Result<Self, Error> {
        let encrypter = ciphersuite.encrypter(key, &iv)?;
        let decrypter = ciphersuite.decrypter(key, &iv)?;

        Ok(Self {
            encrypter,
            decrypter,
        })
    }

    pub fn from_ciphersuite_id(id: u8, key: &[u8], iv: Iv) -> Result<Self, Error> {
        let ciphersuite = ciphersuite(id).ok_or(Error::InvalidCiphersuite(id))?.0;
        Self::from_ciphersuite(ciphersuite, key, iv)
    }
}
