use std::error::Error;
use std::time::{Duration, SystemTime};

use blake2::digest::{
    FixedOutput, Mac,
    generic_array::{GenericArray, typenum},
};
use bytes::{BufMut, Bytes, BytesMut};

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};

/// returns Ok(false) if expired
pub fn check_exp(token: &[u8]) -> Result<bool, Box<dyn Error>> {
    let exp = if token.len() > 4 {
        let exp_as_bytes: [u8; 4] = token[0..4].try_into()?;
        u32::from_be_bytes(exp_as_bytes)
    } else {
        return Err("no exp on token".into());
    };

    let now = u32::try_from(
        SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)?
            .as_secs(),
    )?;

    Ok(exp >= now)
}

pub fn generate_signed(
    claims: &[u8],
    from_now_secs: u32,
    secret_key: [u8; 32],
) -> Result<Bytes, Box<dyn Error>> {
    match SystemTime::now().checked_add(Duration::from_secs(u64::from(from_now_secs))) {
        Some(future_time) => {
            let exp_as_bytes = u32::try_from(
                future_time
                    .duration_since(SystemTime::UNIX_EPOCH)?
                    .as_secs(),
            )?
            .to_be_bytes();

            let singing_key: SigningKey = SigningKey::from_bytes(&secret_key);

            let mut buf = BytesMut::new();

            buf.put(&exp_as_bytes[..]);
            buf.put(claims);

            let signature = singing_key.sign(&buf[..]);

            let mut out = BytesMut::new();

            out.put(&exp_as_bytes[..]);
            out.put(&signature.to_bytes()[..]);
            out.put(claims);

            Ok(out.into())
        }
        None => Err("date is out of range".into()),
    }
}

pub fn extract_signed_no_check<'a>(token: &'a [u8]) -> Result<&'a [u8], Box<dyn Error>> {
    if token.len() >= 4 + 64 {
        let claims: &'a [u8] = &token[4 + 64..];

        Ok(claims)
    } else {
        Err("no signature for token".into())
    }
}

pub fn verify_signed<'a>(
    token: &'a [u8],
    public_key: [u8; 32],
) -> Result<&'a [u8], Box<dyn Error>> {
    let (exp, exp_as_bytes) = if token.len() > 4 {
        let exp_as_bytes: [u8; 4] = token[0..4].try_into()?;
        (u32::from_be_bytes(exp_as_bytes), exp_as_bytes)
    } else {
        return Err("no exp on token".into());
    };

    let now = u32::try_from(
        SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)?
            .as_secs(),
    )?;

    if exp < now {
        return Err("token is expired".into());
    }

    if token.len() >= 4 + 64 {
        let claims: &'a [u8] = &token[4 + 64..];

        let mut buf = BytesMut::new();

        buf.put(&exp_as_bytes[..]);
        buf.put(claims);

        let signature_bytes: [u8; 64] = token[4..4 + 64].try_into()?;
        let signature = Signature::from_bytes(&signature_bytes);
        let verifying_key = VerifyingKey::from_bytes(&public_key)?;

        verifying_key.verify(&buf, &signature)?;

        Ok(claims)
    } else {
        Err("no signature for token".into())
    }
}

#[inline]
pub fn generate_hmac(
    claims: &[u8],
    from_now_secs: u32,
    key: [u8; 32],
) -> Result<Bytes, Box<dyn Error>> {
    match SystemTime::now().checked_add(Duration::from_secs(u64::from(from_now_secs))) {
        Some(future_time) => {
            let exp_as_bytes = u32::try_from(
                future_time
                    .duration_since(SystemTime::UNIX_EPOCH)?
                    .as_secs(),
            )?
            .to_be_bytes();

            let hmac = blake2::Blake2sMac256::new_from_slice(key.as_ref())?
                .chain_update(exp_as_bytes)
                .chain_update(claims)
                .finalize_fixed();

            let mut buf = BytesMut::new();

            buf.put(&exp_as_bytes[..]);
            buf.put(&hmac[..]);
            buf.put(claims);

            Ok(buf.into())
        }
        None => Err("date is out of range".into()),
    }
}

pub fn extract_hmac_no_check<'a>(token: &'a [u8]) -> Result<&'a [u8], Box<dyn Error>> {
    if token.len() >= 4 + 32 {
        let claims: &'a [u8] = &token[4 + 32..];

        Ok(claims)
    } else {
        Err("no signature for token".into())
    }
}

#[inline]
pub fn verify_hmac<'a>(token: &'a [u8], key: [u8; 32]) -> Result<&'a [u8], Box<dyn Error>> {
    let (exp, exp_as_bytes) = if token.len() > 4 {
        let exp_as_bytes: [u8; 4] = token[0..4].try_into()?;
        (u32::from_be_bytes(exp_as_bytes), exp_as_bytes)
    } else {
        return Err("no exp on token".into());
    };

    let now = u32::try_from(
        SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)?
            .as_secs(),
    )?;

    if exp < now {
        return Err("token is expired".into());
    }

    if token.len() >= 4 + 32 {
        let claims: &'a [u8] = &token[4 + 32..];

        let comp = blake2::Blake2sMac256::new_from_slice(&key)?
            .chain_update(exp_as_bytes)
            .chain_update(claims)
            .finalize_fixed();

        let hmac = GenericArray::<u8, typenum::U32>::from_slice(&token[4..4 + 32]);

        if &comp == hmac {
            Ok(claims)
        } else {
            Err("invalid token".into())
        }
    } else {
        Err("no token hmac".into())
    }
}

pub fn verify_client_signature(
    token: &[u8],
    verifying_key_bytes: [u8; 32],
) -> Result<(), Box<dyn Error>> {
    let client_exp_as_bytes = token[token.len() - (4 + 64)..token.len() - 64].try_into()?;
    let client_exp = u32::from_be_bytes(client_exp_as_bytes);

    let now = u32::try_from(
        SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)?
            .as_secs(),
    )?;

    if client_exp < now {
        return Err("client exp exceeded".into());
    }

    let signature_bytes: [u8; 64] = token[token.len() - 64..].try_into()?;

    let signature = Signature::from_bytes(&signature_bytes);
    let verifying_key = VerifyingKey::from_bytes(&verifying_key_bytes)?;

    verifying_key.verify(&token[..token.len() - 64], &signature)?;

    Ok(())
}

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

    use chacha20poly1305::aead::OsRng;

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

    #[test]
    fn hmac() -> Result<(), Box<dyn Error>> {
        let token = generate_hmac(b"test", 60 * 60 * 24, HMAC_KEY)?;
        let payload = verify_hmac(&token[..], HMAC_KEY)?;

        assert_eq!(payload, b"test");

        Ok(())
    }

    #[test]
    fn signed() -> Result<(), Box<dyn Error>> {
        let mut rng = OsRng;

        let signing_key = SigningKey::generate(&mut rng);
        let verifying_key = signing_key.verifying_key();

        let token = generate_signed(b"test", 60 * 60 * 24, signing_key.to_bytes())?;
        let payload = verify_signed(&token[..], verifying_key.to_bytes())?;

        assert_eq!(payload, b"test");

        Ok(())
    }
}
