use std::ffi::CStr;
use std::hash::Hasher;
use std::ptr;

use libsodium_sys::*;
use serde_big_array::BigArray;
use siphasher::sip::SipHasher13;

use crate::errors::*;

#[derive(Educe)]
#[educe(Default)]
pub struct Signature(
    #[educe(Default = [0u8; crypto_sign_BYTES as usize])] [u8; crypto_sign_BYTES as usize],
);

impl Signature {
    pub fn as_bytes(&self) -> &[u8; crypto_sign_BYTES as usize] {
        &self.0
    }

    pub fn from_bytes(bytes: [u8; crypto_sign_BYTES as usize]) -> Self {
        Signature(bytes)
    }
}

#[derive(Serialize, Deserialize, Educe, Clone)]
#[educe(Default)]
pub struct SignSK(
    #[serde(with = "BigArray")]
    #[educe(Default = [0u8; crypto_sign_SECRETKEYBYTES as usize])]
    [u8; crypto_sign_SECRETKEYBYTES as usize],
);

impl SignSK {
    pub fn as_bytes(&self) -> &[u8; crypto_sign_SECRETKEYBYTES as usize] {
        &self.0
    }

    pub fn from_bytes(bytes: [u8; crypto_sign_SECRETKEYBYTES as usize]) -> Self {
        SignSK(bytes)
    }

    pub fn sign(&self, bytes: &[u8]) -> Signature {
        let mut signature = Signature::default();
        let ret = unsafe {
            crypto_sign_detached(
                signature.0.as_mut_ptr(),
                ptr::null_mut(),
                bytes.as_ptr(),
                bytes.len() as _,
                self.as_bytes().as_ptr(),
            )
        };
        assert_eq!(ret, 0);
        signature
    }
}

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct SignPK([u8; crypto_sign_PUBLICKEYBYTES as usize]);

impl SignPK {
    pub fn as_bytes(&self) -> &[u8; crypto_sign_PUBLICKEYBYTES as usize] {
        &self.0
    }

    pub fn from_bytes(bytes: [u8; crypto_sign_PUBLICKEYBYTES as usize]) -> Self {
        SignPK(bytes)
    }

    pub fn as_string(&self) -> String {
        bin2hex(self.as_bytes())
    }
}

#[derive(Educe, Serialize, Deserialize, Clone)]
#[educe(Debug, Default)]
pub struct SignKeyPair {
    #[educe(Debug(ignore))]
    pub sk: SignSK,
    pub pk: SignPK,
}

impl SignKeyPair {
    pub fn new() -> Self {
        let mut kp = SignKeyPair::default();
        unsafe { crypto_sign_keypair(kp.pk.0.as_mut_ptr(), kp.sk.0.as_mut_ptr()) };
        kp
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CryptSK([u8; crypto_box_curve25519xchacha20poly1305_SECRETKEYBYTES as usize]);

impl CryptSK {
    pub fn as_bytes(
        &self,
    ) -> &[u8; crypto_box_curve25519xchacha20poly1305_SECRETKEYBYTES as usize] {
        &self.0
    }

    pub fn from_bytes(
        bytes: [u8; crypto_box_curve25519xchacha20poly1305_SECRETKEYBYTES as usize],
    ) -> Self {
        CryptSK(bytes)
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CryptPK([u8; crypto_box_curve25519xchacha20poly1305_PUBLICKEYBYTES as usize]);

impl CryptPK {
    pub fn as_bytes(
        &self,
    ) -> &[u8; crypto_box_curve25519xchacha20poly1305_PUBLICKEYBYTES as usize] {
        &self.0
    }

    pub fn from_bytes(
        bytes: [u8; crypto_box_curve25519xchacha20poly1305_PUBLICKEYBYTES as usize],
    ) -> Self {
        CryptPK(bytes)
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CryptKeyPair {
    pub sk: CryptSK,
    pub pk: CryptPK,
}

impl CryptKeyPair {
    pub fn from_seed(
        seed: [u8; crypto_box_curve25519xchacha20poly1305_SEEDBYTES as usize],
    ) -> Self {
        let mut kp = CryptKeyPair::default();
        unsafe {
            crypto_box_curve25519xchacha20poly1305_seed_keypair(
                kp.pk.0.as_mut_ptr(),
                kp.sk.0.as_mut_ptr(),
                seed.as_ptr(),
            )
        };
        kp
    }

    pub fn compute_shared_key(&self, pk: &[u8]) -> Result<SharedKey, Error> {
        let mut shared_key = SharedKey::default();
        let res = unsafe {
            crypto_box_curve25519xchacha20poly1305_beforenm(
                shared_key.0.as_mut_ptr(),
                pk.as_ptr(),
                self.sk.0.as_ptr(),
            )
        };
        ensure!(res == 0, "Weak public key");
        Ok(shared_key)
    }
}

#[derive(Debug, Clone, Default)]
pub struct SharedKey([u8; crypto_box_curve25519xchacha20poly1305_BEFORENMBYTES as usize]);

impl SharedKey {
    pub fn from_bytes(
        bytes: [u8; crypto_box_curve25519xchacha20poly1305_BEFORENMBYTES as usize],
    ) -> Self {
        SharedKey(bytes)
    }

    pub fn as_raw_bytes(
        &self,
    ) -> &[u8; crypto_box_curve25519xchacha20poly1305_BEFORENMBYTES as usize] {
        &self.0
    }

    pub fn seal_raw(&self, nonce: &[u8], plaintext: &[u8]) -> Vec<u8> {
        let mut encrypted =
            vec![0u8; plaintext.len() + crypto_box_curve25519xchacha20poly1305_MACBYTES as usize];
        let res = unsafe {
            crypto_box_curve25519xchacha20poly1305_easy_afternm(
                encrypted.as_mut_ptr(),
                plaintext.as_ptr(),
                plaintext.len() as _,
                nonce.as_ptr(),
                self.0.as_ptr(),
            )
        };
        assert_eq!(res, 0);
        encrypted
    }

    pub fn open_raw(&self, nonce: &[u8], encrypted: &[u8]) -> Result<Vec<u8>, Error> {
        let encrypted_len = encrypted.len();
        ensure!(
            encrypted_len >= crypto_box_curve25519xchacha20poly1305_MACBYTES as usize,
            "Unable to decrypt"
        );
        let mut decrypted =
            vec![0u8; encrypted_len - crypto_box_curve25519xchacha20poly1305_MACBYTES as usize];
        let res = unsafe {
            crypto_box_curve25519xchacha20poly1305_open_easy_afternm(
                decrypted.as_mut_ptr(),
                encrypted.as_ptr(),
                encrypted_len as _,
                nonce.as_ptr(),
                self.0.as_ptr(),
            )
        };
        ensure!(res == 0, "Unable to decrypt");
        Ok(decrypted)
    }

    pub fn decrypt(&self, nonce: &[u8], encrypted: &[u8]) -> Result<Vec<u8>, Error> {
        let encrypted_len = encrypted.len();
        ensure!(
            encrypted_len >= crypto_box_curve25519xchacha20poly1305_MACBYTES as usize,
            "Unable to decrypt"
        );
        let mut decrypted =
            vec![0u8; encrypted_len - crypto_box_curve25519xchacha20poly1305_MACBYTES as usize];
        let res = unsafe {
            libsodium_sys::crypto_box_curve25519xchacha20poly1305_open_easy_afternm(
                decrypted.as_mut_ptr(),
                encrypted.as_ptr(),
                encrypted_len as _,
                nonce.as_ptr(),
                self.0.as_ptr(),
            )
        };
        ensure!(res == 0, "Unable to decrypt");
        let idx = decrypted
            .iter()
            .rposition(|x| *x != 0x00)
            .ok_or_else(|| anyhow!("Padding error"))?;
        ensure!(decrypted[idx] == 0x80, "Padding error");
        decrypted.truncate(idx);
        Ok(decrypted)
    }

    pub fn encrypt_into(
        &self,
        target: &mut Vec<u8>,
        nonce: &[u8],
        client_nonce: &[u8],
        plaintext: Vec<u8>,
        max_target_size: usize,
    ) -> Result<(), Error> {
        ensure!(
            max_target_size >= crypto_box_curve25519xchacha20poly1305_MACBYTES as usize,
            "Max target size too small"
        );
        let plaintext_len = plaintext.len();
        let max_padded_plaintext_len =
            max_target_size - crypto_box_curve25519xchacha20poly1305_MACBYTES as usize;
        let mut hasher = SipHasher13::new();
        hasher.write(&self.0);
        hasher.write(client_nonce);
        let pad_size: usize = 1 + (hasher.finish() as usize & 0xff);
        let mut padded_plaintext_len = (plaintext_len + pad_size) & !63;
        if padded_plaintext_len < plaintext_len {
            padded_plaintext_len += 256;
        }
        if padded_plaintext_len > max_padded_plaintext_len {
            padded_plaintext_len = max_padded_plaintext_len;
        }
        ensure!(padded_plaintext_len > plaintext_len, "No room for padding");
        let mut padded_plaintext = plaintext;
        padded_plaintext.push(0x80);
        while padded_plaintext.len() != padded_plaintext_len {
            padded_plaintext.push(0x00);
        }
        let padded_plaintext_len = padded_plaintext.len();
        let target_header_len = target.len();
        target.resize(
            target_header_len
                + padded_plaintext_len
                + crypto_box_curve25519xchacha20poly1305_MACBYTES as usize,
            0,
        );
        let encrypted = &mut target[target_header_len..];
        let res = unsafe {
            libsodium_sys::crypto_box_curve25519xchacha20poly1305_easy_afternm(
                encrypted.as_mut_ptr(),
                padded_plaintext.as_ptr(),
                padded_plaintext_len as _,
                nonce.as_ptr(),
                self.0.as_ptr(),
            )
        };
        ensure!(res == 0, "Unable to encrypt");
        Ok(())
    }
}

pub fn bin2hex(bin: &[u8]) -> String {
    let bin_len = bin.len();
    let hex_len = bin_len * 2 + 1;
    let mut hex = vec![0u8; hex_len];
    unsafe {
        sodium_bin2hex(hex.as_mut_ptr() as *mut _, hex_len, bin.as_ptr(), bin_len);
    }
    CStr::from_bytes_with_nul(&hex)
        .unwrap()
        .to_str()
        .unwrap()
        .to_string()
}

pub fn init() -> Result<(), Error> {
    let res = unsafe { sodium_init() };
    ensure!(res >= 0, "Unable to initialize libsodium");
    Ok(())
}
