use std::fmt::{Debug, Formatter};
use argon2::{
    Argon2,
    Algorithm,
    Version,
    ParamsBuilder,
    password_hash::rand_core::{RngCore, OsRng},
};
use chacha20poly1305::{
    XChaCha20Poly1305,
    XNonce,
    aead::{
        Aead,
        AeadCore,
        KeyInit,
        generic_array::GenericArray,
    },
};

use crate::errors::*;


#[derive(Clone)]
pub struct Crypto {
    cipher: XChaCha20Poly1305,
}

impl Crypto {
    /// Argon2 params rationale:
    ///
    /// m_cost is measured in KiB, so 128 * 1024 gives us 128MiB.
    /// This should roughly double the memory usage of the application
    /// while deriving the key.
    ///
    /// p_cost is irrelevant since (at present) there isn't any parallelism
    /// implemented, so we leave it at 1.
    ///
    /// With the above m_cost, t_cost = 8 results in about 800ms to derive
    /// a key on my (somewhat older) CPU. This is probably overkill, but
    /// given that it should only have to happen ~once a day for most 
    /// usage, it should be acceptable.
    #[cfg(not(debug_assertions))]
    const MEM_COST: u32 = 128 * 1024;
    #[cfg(not(debug_assertions))]
    const TIME_COST: u32 = 8;

    /// But since this takes a million years without optimizations,
    /// we turn it way down in debug builds.
    #[cfg(debug_assertions)]
    const MEM_COST: u32 = 48 * 1024;
    #[cfg(debug_assertions)]
    const TIME_COST: u32 = 1;
    

    pub fn new(passphrase: &str, salt: &[u8]) -> argon2::Result<Crypto> {
        let params = ParamsBuilder::new()
            .m_cost(Self::MEM_COST)
            .p_cost(1)
            .t_cost(Self::TIME_COST)
            .build()
            .unwrap(); // only errors if the given params are invalid

        let hasher = Argon2::new(
            Algorithm::Argon2id,
            Version::V0x13,
            params,
        );

        let mut key = [0; 32];
        hasher.hash_password_into(passphrase.as_bytes(), &salt, &mut key)?;
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
        Ok(Crypto { cipher })
    }

    #[cfg(test)]
    pub fn random() -> Crypto {
        // salt and key are the same length, so we can just use this
        let key = Crypto::salt();
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
        Crypto { cipher }
    }

    #[cfg(test)]
    pub fn fixed() -> Crypto {
        let key = [
            1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
            17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
        ];

        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
        Crypto { cipher }
    }

    pub fn salt() -> [u8; 32] {
        let mut salt = [0; 32];
        OsRng.fill_bytes(&mut salt);
        salt
    }

    pub fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), CryptoError> {
        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
        let ciphertext = self.cipher.encrypt(&nonce, data)?;
        Ok((nonce, ciphertext))
    }

    pub fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
        let plaintext = self.cipher.decrypt(nonce, data)?;
        Ok(plaintext)
    }
}

impl Debug for Crypto {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "Crypto {{ [...] }}")
    }
}
