use core::ops::Deref;

use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, KeyInit};

use crate::bytearray::SensitiveByteArray;
use crate::error::{CipherError, CipherResult};
use crate::traits::{Cipher, CryptoComponent};

/// ChaCha20-Poly1305 cipher implementation
#[derive(Clone)]
pub struct ChaChaPoly;

impl CryptoComponent for ChaChaPoly {
    fn name() -> &'static str {
        "ChaChaPoly"
    }
}

impl Cipher for ChaChaPoly {
    type Key = SensitiveByteArray<[u8; 32]>;

    fn encrypt(
        k: &Self::Key,
        nonce: u64,
        ad: &[u8],
        plaintext: &[u8],
        out: &mut [u8],
    ) -> CipherResult<()> {
        assert!(plaintext.len().checked_add(Self::tag_len()) == Some(out.len()));
        out[..plaintext.len()].copy_from_slice(plaintext);
        Self::encrypt_in_place(k, nonce, ad, out, plaintext.len())?;
        Ok(())
    }

    fn encrypt_in_place(
        k: &Self::Key,
        nonce: u64,
        ad: &[u8],
        in_out: &mut [u8],
        plaintext_len: usize,
    ) -> CipherResult<usize> {
        assert!(plaintext_len
            .checked_add(Self::tag_len())
            .is_some_and(|len| len <= in_out.len()));

        let mut full_nonce = [0u8; 12];
        full_nonce[4..].copy_from_slice(&nonce.to_le_bytes());

        let out_len = plaintext_len + Self::tag_len();
        let (buffer, tag_out) = in_out[..out_len].split_at_mut(plaintext_len);

        let tag = ChaCha20Poly1305::new(k.deref().into())
            .encrypt_in_place_detached(&full_nonce.into(), ad, buffer)
            .map_err(|_| CipherError::Encrypt)?;

        tag_out.copy_from_slice(&tag);
        Ok(out_len)
    }

    fn decrypt(
        k: &Self::Key,
        nonce: u64,
        ad: &[u8],
        ciphertext: &[u8],
        out: &mut [u8],
    ) -> CipherResult<()> {
        assert!(ciphertext.len().checked_sub(Self::tag_len()) == Some(out.len()));

        let mut full_nonce = [0u8; 12];
        full_nonce[4..].copy_from_slice(&nonce.to_le_bytes());

        out.copy_from_slice(&ciphertext[..out.len()]);
        let tag = &ciphertext[out.len()..];

        ChaCha20Poly1305::new(k.deref().into())
            .decrypt_in_place_detached(&full_nonce.into(), ad, out, tag.into())
            .map_err(|_| CipherError::Decrypt)?;

        Ok(())
    }

    fn decrypt_in_place(
        k: &Self::Key,
        nonce: u64,
        ad: &[u8],
        in_out: &mut [u8],
        ciphertext_len: usize,
    ) -> CipherResult<usize> {
        assert!(ciphertext_len <= in_out.len());
        assert!(ciphertext_len >= Self::tag_len());

        let mut full_nonce = [0u8; 12];
        full_nonce[4..].copy_from_slice(&nonce.to_le_bytes());

        let (buffer, tag) = in_out[..ciphertext_len].split_at_mut(ciphertext_len - Self::tag_len());

        ChaCha20Poly1305::new(k.deref().into())
            .decrypt_in_place_detached(&full_nonce.into(), ad, buffer, tag.as_ref().into())
            .map_err(|_| CipherError::Decrypt)?;

        Ok(buffer.len())
    }

    fn tag_len() -> usize {
        16
    }
}
