use std::fs;
use std::path::Path;

use aes::Aes256;
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use aes_gcm_siv::aead::{Aead, KeyInit};
use aes_gcm_siv::{Aes256GcmSiv, Nonce};
use cbc::{Decryptor, Encryptor};
use hex_simd::AsciiCase;

use crate::Error;

pub(crate) fn encrypt_and_save_to_file<P, T, E>(
    plaintext: String,
    file_path: P,
    password: T,
    nonce: E,
) -> Result<(), Error>
where
    P: AsRef<Path>,
    T: AsRef<str>,
    E: AsRef<str>,
{
    let (cipher, nonce) = make_cipher_and_nonce(password, nonce)?;
    let ciphertext = cipher
        .encrypt(&nonce, plaintext.as_bytes())
        .map_err(|err| Error::AesGcmSiv(err.to_string()))?;

    fs::write(
        file_path,
        base64_simd::STANDARD.encode_to_string(&ciphertext),
    )?;

    Ok(())
}

pub(crate) fn decrypt_from_file<P, T, E>(
    file_path: P,
    password: T,
    nonce: E,
) -> Result<String, Error>
where
    P: AsRef<Path>,
    T: AsRef<str>,
    E: AsRef<str>,
{
    let mut ciphertext = fs::read(file_path)?;
    let ciphertext = base64_simd::STANDARD.decode_inplace(ciphertext.as_mut_slice())?;

    let (cipher, nonce) = make_cipher_and_nonce(password, nonce)?;
    let plaintext = cipher
        .decrypt(&nonce, &*ciphertext)
        .map_err(|err| Error::AesGcmSiv(err.to_string()))?;

    Ok(unsafe { String::from_utf8_unchecked(plaintext) })
}

fn make_cipher_and_nonce<T, E>(password: T, nonce: E) -> Result<(Aes256GcmSiv, Nonce), Error>
where
    T: AsRef<str>,
    E: AsRef<str>,
{
    let key = crate::sha256(password.as_ref().as_bytes());
    let cipher = Aes256GcmSiv::new_from_slice(key.as_ref())?;
    let nonce = crate::md5_hex(nonce.as_ref(), AsciiCase::Lower);
    let nonce = Nonce::from_slice(&nonce.as_bytes()[0..12]).to_owned();

    Ok((cipher, nonce))
}

#[must_use]
pub(crate) fn aes_256_cbc_no_iv_base64_encrypt<T, E>(key: T, data: E) -> String
where
    T: AsRef<[u8]>,
    E: AsRef<[u8]>,
{
    type Aes256CbcEnc = Encryptor<Aes256>;

    let ciphertext = Aes256CbcEnc::new(key.as_ref().into(), &[0; 16].into())
        .encrypt_padded_vec_mut::<Pkcs7>(data.as_ref());

    base64_simd::STANDARD.encode_to_string(&ciphertext)
}

pub(crate) fn aes_256_cbc_no_iv_base64_decrypt<T, E>(key: T, data: E) -> Result<Vec<u8>, Error>
where
    T: AsRef<[u8]>,
    E: AsRef<[u8]>,
{
    type Aes256CbcDec = Decryptor<Aes256>;

    let ciphertext = base64_simd::STANDARD.decode_to_vec(data.as_ref())?;
    let result = Aes256CbcDec::new(key.as_ref().into(), &[0; 16].into())
        .decrypt_padded_vec_mut::<Pkcs7>(&ciphertext)?;

    Ok(result)
}

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

    #[test]
    fn aes_256_gcm_base64() -> Result<(), Error> {
        let dir = tempfile::tempdir()?;
        let file = dir.path().join("aes.txt");

        let data = String::from("Hello World");
        encrypt_and_save_to_file(data, &file, "password", "nonce")?;

        assert!(file.is_file());

        let decrypted = decrypt_from_file(file, "password", "nonce")?;

        assert_eq!(decrypted, "Hello World");

        Ok(())
    }
}
