use chacha20poly1305::{
    XChaCha20Poly1305, XNonce,
    aead::{Aead, AeadCore, KeyInit, OsRng},
};

use uuid::Uuid;

use std::error::Error;

pub enum KeyAlg {
    Ed25519Pair,
    Blake2SMac256,
}

impl KeyAlg {
    #[must_use]
    pub fn as_byte(&self) -> u8 {
        match self {
            Self::Ed25519Pair => 0,
            Self::Blake2SMac256 => 1,
        }
    }
}

#[allow(clippy::type_complexity)]
pub fn generate_ed25519_pair(
    cipher: &XChaCha20Poly1305,
    rng: &mut OsRng,
) -> Result<(Vec<u8>, [u8; 32], [u8; 32], Uuid), Box<dyn Error>> {
    let signing_key = ed25519_dalek::SigningKey::generate(rng);

    let verifying_key = signing_key.verifying_key().to_bytes();
    let signing_key = signing_key.to_bytes();

    let kid = Uuid::now_v7();
    let mut kid_key = kid.as_bytes().to_vec();

    let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
    let encrypted_signing_key = match cipher.encrypt(&nonce, &signing_key[..]) {
        Ok(v) => v,
        Err(err) => return Err(err.to_string().into()),
    };

    kid_key.extend_from_slice(&verifying_key[..]);
    kid_key.extend_from_slice(&nonce[..]);
    kid_key.extend_from_slice(&encrypted_signing_key[..]);

    Ok((kid_key, signing_key, verifying_key, kid))
}

#[allow(clippy::type_complexity)]
pub fn decrypt_ed25519_pair(
    cipher: &XChaCha20Poly1305,
    payload: &[u8],
) -> Result<(Uuid, [u8; 32], [u8; 32]), Box<dyn Error>> {
    let kid: [u8; 16] = payload[0..16].try_into()?;
    let verifying_key: [u8; 32] = payload[16..48].try_into()?;

    let nonce = XNonce::from_slice(&payload[48..72]);

    let signing_key = match cipher.decrypt(nonce, &payload[72..]) {
        Ok(v) => v,
        Err(err) => return Err(err.to_string().into()),
    };

    let signing_key_fixed: [u8; 32] = signing_key[0..32].try_into()?;

    let kid = Uuid::from_bytes(kid);

    Ok((kid, signing_key_fixed, verifying_key))
}

#[allow(clippy::type_complexity)]
pub fn generate_256_bit_key(
    cipher: &XChaCha20Poly1305,
    rng: &mut OsRng,
) -> Result<(Vec<u8>, [u8; 32], Uuid), Box<dyn Error>> {
    let hmac_key: [u8; 32] = XChaCha20Poly1305::generate_key(rng).into();

    let kid = Uuid::now_v7();
    let mut kid_key = kid.as_bytes().to_vec();

    let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
    let encrypted_hmac_key = match cipher.encrypt(&nonce, &hmac_key[..]) {
        Ok(v) => v,
        Err(err) => return Err(err.to_string().into()),
    };

    kid_key.extend_from_slice(&nonce[..]);
    kid_key.extend_from_slice(&encrypted_hmac_key[..]);

    Ok((kid_key, hmac_key, kid))
}

pub fn decrypt_256_bit_key(
    cipher: &XChaCha20Poly1305,
    payload: &[u8],
) -> Result<(Uuid, [u8; 32]), Box<dyn Error>> {
    let kid: [u8; 16] = payload[0..16].try_into()?;

    let nonce = XNonce::from_slice(&payload[16..40]);

    let key = match cipher.decrypt(nonce, &payload[40..]) {
        Ok(v) => v,
        Err(err) => return Err(err.to_string().into()),
    };

    let key_fixed: [u8; 32] = key[0..32].try_into()?;

    let kid = Uuid::from_bytes(kid);

    Ok((kid, key_fixed))
}

#[cfg(test)]
mod test {
    use super::*;

    const ENCRYPTION_KEY: [u8; 32] = [0u8; 32];

    #[test]
    fn ed25519() -> Result<(), Box<dyn Error>> {
        let mut rng = OsRng;
        let cipher = XChaCha20Poly1305::new(&ENCRYPTION_KEY.into());

        let (encrypted, signing_key, verifying_key, generated_kid) =
            generate_ed25519_pair(&cipher, &mut rng)?;

        let (kid, decrypted_signing, verifying) = decrypt_ed25519_pair(&cipher, &encrypted)?;

        assert_eq!(kid, generated_kid);
        assert_eq!(signing_key, decrypted_signing);
        assert_eq!(verifying_key, verifying);

        Ok(())
    }

    #[test]
    fn a_256_bit() -> Result<(), Box<dyn Error>> {
        let mut rng = OsRng;
        let cipher = XChaCha20Poly1305::new(&ENCRYPTION_KEY.into());

        let (encrypted, key, generated_kid) = generate_256_bit_key(&cipher, &mut rng)?;

        let (kid, decrypted) = decrypt_256_bit_key(&cipher, &encrypted)?;

        assert_eq!(kid, generated_kid);
        assert_eq!(key, decrypted);

        Ok(())
    }
}
