//! High-level crypto bindings.
use std::sync::Mutex;

use duplicate::duplicate_item;

use crate::{
    common::Nonce,
    crypto::{
        argon2, blake2b, chacha20,
        chunked::{self, InvalidTag},
        deprecated::scrypt,
        salsa20, sha2, x25519,
    },
    utils::sync::MutexIgnorePoison as _,
};

/// A general crypto-related error.
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum CryptoError {
    /// Invalid parameter provided by foreign code.
    #[error("Invalid parameter: {0}")]
    InvalidParameter(String),

    /// Non-contributory key.
    #[error("Key is non-contributory")]
    NonContributoryKey,

    /// Unable to encrypt/decrypt.
    #[error("Unable to encrypt/decrypt")]
    CipherFailed,
}

impl From<InvalidTag> for CryptoError {
    fn from(_: InvalidTag) -> Self {
        CryptoError::CipherFailed
    }
}

/// Compute the SHA-256 hash of the provided data.
#[must_use]
#[uniffi::export]
pub fn sha256(data: &[u8]) -> Vec<u8> {
    use crate::crypto::digest::{Digest as _, FixedOutput as _};

    sha2::Sha256::new().chain_update(data).finalize_fixed().to_vec()
}

/// Compute the HMAC-SHA256 from the provided key and data.
#[expect(clippy::missing_panics_doc, reason = "Panic will never happen")]
#[must_use]
#[uniffi::export]
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
    use crate::crypto::digest::{FixedOutput as _, Mac as _};

    sha2::HmacSha256::new_from_slice(key)
        .expect("HMAC can take key of any size")
        .chain_update(data)
        .finalize_fixed()
        .to_vec()
}

/// Derive a Blake2b MAC of length 256 bits (32 bytes) of the data from the provided key, personal and salt.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `key` is present and less than 1 or more than 128
/// bytes and when `personal` or `salt` is more than 32 bytes.
#[expect(clippy::ref_option, reason = "Bindings don't allow to use Option<&Vec<u8>>")]
#[uniffi::export]
pub fn blake2b_mac_256(
    key: &Option<Vec<u8>>,
    personal: &[u8],
    salt: &[u8],
    data: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    use crate::crypto::digest::{FixedOutput as _, Mac as _};

    Ok(
        blake2b::Blake2bMac256::new_with_salt_and_personal(key.as_deref(), salt, personal)
            .map_err(|_| {
                CryptoError::InvalidParameter(
                    "'key' if provided must be between 1 and 128 bytes, 'personal' and 'salt' must be up to \
                     32 bytes"
                        .to_owned(),
                )
            })?
            .chain_update(data)
            .finalize_fixed()
            .to_vec(),
    )
}

/// Derive a Blake2b MAC of length 512 bits (64 bytes) of the data from the provided key, personal and salt.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `key` is present and less than 1 or more than 128
/// bytes and when `personal` or `salt` is more than 32 bytes.
#[expect(clippy::ref_option, reason = "Bindings don't allow to use Option<&Vec<u8>>")]
#[uniffi::export]
pub fn blake2b_mac_512(
    key: &Option<Vec<u8>>,
    personal: &[u8],
    salt: &[u8],
    data: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    use crate::crypto::digest::{FixedOutput as _, Mac as _};

    Ok(
        blake2b::Blake2bMac512::new_with_salt_and_personal(key.as_deref(), salt, personal)
            .map_err(|_| {
                CryptoError::InvalidParameter(
                    "'key' if provided must be between 1 and 128 bytes, 'personal' and 'salt' must be up to \
                     32 bytes"
                        .to_owned(),
                )
            })?
            .chain_update(data)
            .finalize_fixed()
            .to_vec(),
    )
}

/// Parameters for [`argon2id`].
#[derive(Clone, Copy, uniffi::Record)]
pub struct Argon2idParameters {
    /// Memory size in 1 KiB blocks. Between 8*`parallelism` and (2^32)-1.
    pub memory_cost: u32,

    /// Number of iterations. Between 1 and (2^32)-1.
    pub time_cost: u32,

    /// Degree of parallelism. Between 1 and (2^24)-1.
    pub parallelism: u32,

    /// Size of the output in bytes. Between 1 and 64.
    pub output_length: u8,
}
impl TryFrom<Argon2idParameters> for argon2::Params {
    type Error = CryptoError;

    fn try_from(parameters: Argon2idParameters) -> Result<Self, Self::Error> {
        if !(1..=64).contains(&parameters.output_length) {
            return Err(CryptoError::InvalidParameter(
                "output length must be between 1 and 64 bytes".to_owned(),
            ));
        }

        argon2::Params::new(
            parameters.memory_cost,
            parameters.time_cost,
            parameters.parallelism,
            Some(parameters.output_length.into()),
        )
        .map_err(|error| CryptoError::InvalidParameter(error.to_string()))
    }
}

/// Derive a key from the provided password and salt using Argon2id.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if the passed parameters are invalid (see
/// [`Argon2idParameters`] for the requirements).
#[uniffi::export]
pub fn argon2id(
    password: &[u8],
    salt: &[u8],
    parameters: Argon2idParameters,
) -> Result<Vec<u8>, CryptoError> {
    let context = argon2::Argon2::new(argon2::Argon2id, argon2::Version::V0x13, parameters.try_into()?);
    let mut output = vec![0; parameters.output_length as usize];
    context
        .hash_password_into(password, salt, &mut output)
        .map_err(|error| CryptoError::InvalidParameter(error.to_string()))?;

    Ok(output)
}

/// Parameters for [`scrypt()`].
#[derive(uniffi::Record)]
pub struct ScryptParameters {
    /// The logarithm of the CPU/memory cost parameter (aka `N`). Less than 64. The resulting
    /// memory cost will be `2 ^ log_memory_cost` in KiB. For example, `log_memory_cost = 17` would
    /// be 128 MiB.
    pub log_memory_cost: u8,

    /// Block size as multiplicator of 128 bytes (aka `r`). Between 1 and (2^32)-1.
    pub block_size: u32,

    /// Degree of parallelism / amount of threads (aka `p`). Between 1 and (2^32)-1.
    pub parallelism: u32,

    /// Size of the output in bytes. Between 10 and 64.
    pub output_length: u8,
}
impl TryFrom<ScryptParameters> for scrypt::Params {
    type Error = CryptoError;

    fn try_from(parameters: ScryptParameters) -> Result<Self, Self::Error> {
        scrypt::Params::new(
            parameters.log_memory_cost,
            parameters.block_size,
            parameters.parallelism,
            parameters.output_length.into(),
        )
        .map_err(|error| CryptoError::InvalidParameter(error.to_string()))
    }
}

/// Derive a key from the provided password and salt using Scrypt.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if the passed parameters are invalid (see
/// [`ScryptParameters`] for the requirements).
#[uniffi::export]
pub fn scrypt(password: &[u8], salt: &[u8], parameters: ScryptParameters) -> Result<Vec<u8>, CryptoError> {
    let mut output = vec![0; parameters.output_length as usize];
    scrypt::scrypt(password, salt, &parameters.try_into()?, &mut output)
        .map_err(|error| CryptoError::InvalidParameter(error.to_string()))?;

    Ok(output)
}

/// Derive the X25519 public key associated to the provided X25519 secret key.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `secret_key` is not exactly 32 bytes.
#[uniffi::export]
pub fn x25519_derive_public_key(secret_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
    let secret_key = x25519::StaticSecret::from(
        <[u8; 32]>::try_from(secret_key)
            .map_err(|_| CryptoError::InvalidParameter("'secret_key' must be 32 bytes".to_owned()))?,
    );
    let public_key = x25519::PublicKey::from(&secret_key);
    Ok(public_key.as_bytes().to_vec())
}

/// Derive the X25519 shared secret from the provided X25519 public and secret key.
///
/// Note: The resulting shared secret will be hashed with HSalsa20 to ensure uniform distribution.
/// This is compatible with classic NaCl implementations.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `public_key` or `secret_key` is not exactly 32 bytes.
/// Returns [`CryptoError::NonContributoryKey`] if `public_key` is non-contributory.
#[uniffi::export]
pub fn x25519_hsalsa20_derive_shared_secret(
    public_key: &[u8],
    secret_key: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    let public_key = x25519::PublicKey::from(
        <[u8; 32]>::try_from(public_key)
            .map_err(|_| CryptoError::InvalidParameter("'public_key' must be 32 bytes".to_owned()))?,
    );
    let secret_key = x25519::StaticSecret::from(
        <[u8; 32]>::try_from(secret_key)
            .map_err(|_| CryptoError::InvalidParameter("'secret_key' must be 32 bytes".to_owned()))?,
    );
    let shared_secret = x25519::SharedSecretHSalsa20::from(
        secret_key
            .diffie_hellman(&public_key)
            .ok_or(CryptoError::NonContributoryKey)?,
    );
    Ok(shared_secret.as_bytes().to_vec())
}

/// Encrypt the provided data using XChaCha20 and append a Poly1305 MAC.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `key` is not exactly 32 bytes or `nonce` is not
/// exactly 24 bytes.
///
/// Returns [`CryptoError::CipherFailed`] if encryption failed.
#[uniffi::export]
pub fn xchacha20_poly1305_encrypt(
    key: &[u8],
    nonce: &[u8],
    mut data: Vec<u8>,
    associated_data: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    use crate::crypto::{aead::AeadInPlace as _, cipher::KeyInit as _};

    let cipher = chacha20::XChaCha20Poly1305::new_from_slice(key)
        .map_err(|_| CryptoError::InvalidParameter("'key' must be 32 bytes".to_owned()))?;
    let nonce: Nonce = nonce
        .try_into()
        .map_err(|_| CryptoError::InvalidParameter("'nonce' must be 24 bytes".to_owned()))?;
    cipher
        .encrypt_in_place((&nonce).into(), associated_data, &mut data)
        .map_err(|_| CryptoError::CipherFailed)?;
    Ok(data)
}

/// Decrypt the provided data using XChaCha20.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `key` is not exactly 32 bytes or `nonce` is not
/// exactly 24 bytes.
///
/// Returns [`CryptoError::CipherFailed`] if decryption failed.
#[uniffi::export]
pub fn xchacha20_poly1305_decrypt(
    key: &[u8],
    nonce: &[u8],
    mut data: Vec<u8>,
    associated_data: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    use crate::crypto::{aead::AeadInPlace as _, cipher::KeyInit as _};

    let cipher = chacha20::XChaCha20Poly1305::new_from_slice(key)
        .map_err(|_| CryptoError::InvalidParameter("'key' must be 32 bytes".to_owned()))?;
    let nonce: Nonce = nonce
        .try_into()
        .map_err(|_| CryptoError::InvalidParameter("'nonce' must be 24 bytes".to_owned()))?;
    cipher
        .decrypt_in_place((&nonce).into(), associated_data, &mut data)
        .map_err(|_| CryptoError::CipherFailed)?;
    Ok(data)
}

/// Encrypt the provided data using XSalsa20 and append a Poly1305 MAC.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `key` is not exactly 32 bytes or `nonce` is not
/// exactly 24 bytes.
///
/// Returns [`CryptoError::CipherFailed`] if encryption failed.
#[uniffi::export]
pub fn xsalsa20_poly1305_encrypt(
    key: &[u8],
    nonce: &[u8],
    mut data: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
    use crate::crypto::{aead::AeadInPlace as _, cipher::KeyInit as _};

    let cipher = salsa20::XSalsa20Poly1305::new_from_slice(key)
        .map_err(|_| CryptoError::InvalidParameter("'key' must be 32 bytes".to_owned()))?;
    let nonce: Nonce = nonce
        .try_into()
        .map_err(|_| CryptoError::InvalidParameter("'nonce' must be 24 bytes".to_owned()))?;
    cipher
        .encrypt_in_place((&nonce).into(), &[], &mut data)
        .map_err(|_| CryptoError::CipherFailed)?;
    Ok(data)
}

/// Decrypt the provided data using XSalsa20.
///
/// # Errors
///
/// Returns [`CryptoError::InvalidParameter`] if `key` is not exactly 32 bytes or `nonce` is not
/// exactly 24 bytes.
///
/// Returns [`CryptoError::CipherFailed`] if decryption failed.
#[uniffi::export]
pub fn xsalsa20_poly1305_decrypt(
    key: &[u8],
    nonce: &[u8],
    mut data: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
    use crate::crypto::{aead::AeadInPlace as _, cipher::KeyInit as _};

    let cipher = salsa20::XSalsa20Poly1305::new_from_slice(key)
        .map_err(|_| CryptoError::InvalidParameter("'key' must be 32 bytes".to_owned()))?;
    let nonce: Nonce = nonce
        .try_into()
        .map_err(|_| CryptoError::InvalidParameter("'nonce' must be 24 bytes".to_owned()))?;
    cipher
        .decrypt_in_place((&nonce).into(), &[], &mut data)
        .map_err(|_| CryptoError::CipherFailed)?;
    Ok(data)
}

fn ensure_xdance_key_and_nonce<'data>(
    key: &'data [u8],
    nonce: &'data [u8],
) -> Result<(&'data [u8; 32], &'data [u8; Nonce::LENGTH]), CryptoError> {
    let key: &[u8; 32] = key
        .try_into()
        .map_err(|_| CryptoError::InvalidParameter("'key' must be 32 bytes".to_owned()))?;
    let nonce: &[u8; Nonce::LENGTH] = nonce
        .try_into()
        .map_err(|_| CryptoError::InvalidParameter("'nonce' must be 24 bytes".to_owned()))?;
    Ok((key, nonce))
}

/// Binding version of [`chunked::ChunkedXChaCha20Poly1305Encryptor`].
#[derive(uniffi::Object)]
pub struct ChunkedXChaCha20Poly1305Encryptor(Mutex<Option<chunked::ChunkedXChaCha20Poly1305Encryptor>>);

/// Binding version of [`chunked::ChunkedXChaCha20Poly1305Decryptor`].
#[derive(uniffi::Object)]
pub struct ChunkedXChaCha20Poly1305Decryptor(Mutex<Option<chunked::ChunkedXChaCha20Poly1305Decryptor>>);

#[duplicate_item(
    cipher_name;
    [ ChunkedXChaCha20Poly1305Encryptor ];
    [ ChunkedXChaCha20Poly1305Decryptor ];
)]
#[uniffi::export]
impl cipher_name {
    /// Binding version of [`chunked::ChunkedXChaCha20Poly1305Encryptor::new`] /
    /// [`chunked::ChunkedXChaCha20Poly1305Decryptor::new`].
    ///
    /// # Errors
    ///
    /// Returns an error if `key` is not exactly 32 bytes or `nonce` is not exactly 24 bytes.
    #[uniffi::constructor]
    pub fn new(key: &[u8], nonce: &[u8], associated_data: &[u8]) -> Result<Self, CryptoError> {
        let (key, nonce) = ensure_xdance_key_and_nonce(key, nonce)?;
        Ok(Self(Mutex::new(Some(chunked::cipher_name::new(
            key,
            nonce,
            associated_data,
        )))))
    }
}

/// Binding version of [`chunked::ChunkedXSalsa20Poly1305Encryptor`].
#[derive(uniffi::Object)]
pub struct ChunkedXSalsa20Poly1305Encryptor(Mutex<Option<chunked::ChunkedXSalsa20Poly1305Encryptor>>);

/// Binding version of [`chunked::ChunkedXSalsa20Poly1305Decryptor`].
#[derive(uniffi::Object)]
pub struct ChunkedXSalsa20Poly1305Decryptor(Mutex<Option<chunked::ChunkedXSalsa20Poly1305Decryptor>>);

#[duplicate_item(
    cipher_name;
    [ ChunkedXSalsa20Poly1305Encryptor ];
    [ ChunkedXSalsa20Poly1305Decryptor ];
)]
#[uniffi::export]
impl cipher_name {
    /// Binding version of [`chunked::ChunkedXSalsa20Poly1305Encryptor::new`] /
    /// [`chunked::ChunkedXSalsa20Poly1305Decryptor::new`].
    ///
    /// # Errors
    ///
    /// Returns an error if `key` is not exactly 32 bytes or `nonce` is not exactly 24 bytes.
    #[uniffi::constructor]
    pub fn new(key: &[u8], nonce: &[u8]) -> Result<Self, CryptoError> {
        let (key, nonce) = ensure_xdance_key_and_nonce(key, nonce)?;
        Ok(Self(Mutex::new(Some(chunked::cipher_name::new(key, nonce)))))
    }
}

#[duplicate_item(
    cipher_name;
    [ ChunkedXChaCha20Poly1305Encryptor ];
    [ ChunkedXSalsa20Poly1305Encryptor ];
)]
#[uniffi::export]
impl cipher_name {
    /// Binding version of [`chunked::ChunkedXChaCha20Poly1305Encryptor::encrypt`] /
    /// [`chunked::ChunkedXSalsa20Poly1305Encryptor::encrypt`].
    ///
    /// # Errors
    ///
    /// Returns [`CryptoError::CipherFailed`] in case of an internal error.
    pub fn encrypt(&self, mut chunk: Vec<u8>) -> Result<Vec<u8>, CryptoError> {
        self.0
            .lock_ignore_poison()
            .as_mut()
            .ok_or(CryptoError::CipherFailed)?
            .encrypt(&mut chunk);
        Ok(chunk)
    }

    /// Binding version of [`chunked::ChunkedXChaCha20Poly1305Encryptor::finalize`] /
    /// [`chunked::ChunkedXSalsa20Poly1305Encryptor::finalize`].
    ///
    /// # Errors
    ///
    /// Returns [`CryptoError::CipherFailed`] in case of an internal error.
    pub fn finalize(&self) -> Result<Vec<u8>, CryptoError> {
        Ok(self
            .0
            .lock_ignore_poison()
            .take()
            .ok_or(CryptoError::CipherFailed)?
            .finalize()
            .into())
    }
}

#[duplicate_item(
    cipher_name;
    [ ChunkedXChaCha20Poly1305Decryptor ];
    [ ChunkedXSalsa20Poly1305Decryptor ];
)]
#[uniffi::export]
impl cipher_name {
    /// Binding version of [`chunked::ChunkedXChaCha20Poly1305Decryptor::decrypt`] /
    /// [`chunked::ChunkedXSalsa20Poly1305Decryptor::decrypt`].
    ///
    /// # Errors
    ///
    /// Returns [`CryptoError::CipherFailed`] in case of an internal error.
    pub fn decrypt(&self, mut chunk: Vec<u8>) -> Result<Vec<u8>, CryptoError> {
        self.0
            .lock_ignore_poison()
            .as_mut()
            .ok_or(CryptoError::CipherFailed)?
            .decrypt(&mut chunk);
        Ok(chunk)
    }

    /// Binding version of [`chunked::ChunkedXChaCha20Poly1305Decryptor::finalize_verify`] /
    /// [`chunked::ChunkedXSalsa20Poly1305Decryptor::finalize_verify`].
    ///
    /// # Errors
    ///
    /// Returns [`CryptoError::CipherFailed`] in case of an internal error or in case the tag does not match.
    pub fn finalize_verify(&self, expected_tag: &[u8]) -> Result<(), CryptoError> {
        let expected_tag = expected_tag
            .try_into()
            .map_err(|_| CryptoError::InvalidParameter("'tag' must be 16 bytes".to_owned()))?;
        Ok(self
            .0
            .lock_ignore_poison()
            .take()
            .ok_or(CryptoError::CipherFailed)?
            .finalize_verify(expected_tag)?)
    }
}
