use crate::{Config, Sign, SignError, Spawn};
use reqwest::{Body, Request, RequestBuilder};
use std::fmt::Display;

#[cfg(feature = "ring")]
pub mod ring;
#[cfg(feature = "sha-2")]
mod sha2;
#[cfg(feature = "sha-3")]
mod sha3;

/// A trait for creating digests of an array of bytes
pub trait DigestCreate {
    /// The name of the digest algorithm
    const NAME: &'static str;

    /// Compute the digest of the input bytes
    fn compute(&mut self, input: &[u8]) -> String;
}

/// Extend the Sign trait with support for adding Digest Headers to the request
///
/// It generates HTTP Signatures after the Digest header has been added, in order to have
/// verification that the body has not been tampered with, or that the request can't be replayed by
/// a malicious entity
#[async_trait::async_trait]
pub trait SignExt: Sign {
    async fn authorization_signature_with_digest<F, E, K, S, D, V>(
        self,
        config: Config<S>,
        key_id: K,
        digest: D,
        v: V,
        f: F,
    ) -> Result<Request, E>
    where
        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
        E: From<SignError> + From<reqwest::Error> + Send + 'static,
        K: Display + Send + 'static,
        D: DigestCreate + Send + 'static,
        V: AsRef<[u8]> + Into<Body> + Send + 'static,
        S: Spawn + Send + Sync,
        Self: Sized;

    async fn signature_with_digest<F, E, K, S, D, V>(
        self,
        config: Config<S>,
        key_id: K,
        digest: D,
        v: V,
        f: F,
    ) -> Result<Request, E>
    where
        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
        E: From<SignError> + From<reqwest::Error> + Send + 'static,
        K: Display + Send + 'static,
        D: DigestCreate + Send + 'static,
        V: AsRef<[u8]> + Into<Body> + Send + 'static,
        S: Spawn + Send + Sync,
        Self: Sized;
}

#[async_trait::async_trait]
impl SignExt for RequestBuilder {
    async fn authorization_signature_with_digest<F, E, K, S, D, V>(
        self,
        config: Config<S>,
        key_id: K,
        mut digest: D,
        v: V,
        f: F,
    ) -> Result<Request, E>
    where
        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
        E: From<SignError> + From<reqwest::Error> + Send + 'static,
        K: Display + Send + 'static,
        D: DigestCreate + Send + 'static,
        V: AsRef<[u8]> + Into<Body> + Send + 'static,
        S: Spawn + Send + Sync,
    {
        let (v, digest) = config
            .spawner
            .spawn_blocking(move || {
                let digest = digest.compute(v.as_ref());
                (v, digest)
            })
            .await
            .map_err(|_| SignError::Canceled)?;

        let mut req = self
            .header("Digest", format!("{}={}", D::NAME, digest))
            .authorization_signature(&config, key_id, f)
            .await?;

        *req.body_mut() = Some(Body::from(v.as_ref().to_vec()));

        Ok(req)
    }

    async fn signature_with_digest<F, E, K, S, D, V>(
        self,
        config: Config<S>,
        key_id: K,
        mut digest: D,
        v: V,
        f: F,
    ) -> Result<Request, E>
    where
        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
        E: From<SignError> + From<reqwest::Error> + Send + 'static,
        K: Display + Send + 'static,
        D: DigestCreate + Send + 'static,
        V: AsRef<[u8]> + Into<Body> + Send + 'static,
        S: Spawn + Send + Sync,
    {
        let (v, digest) = config
            .spawner
            .spawn_blocking(move || {
                let digest = digest.compute(v.as_ref());
                (v, digest)
            })
            .await
            .map_err(|_| SignError::Canceled)?;

        let mut req = self
            .header("Digest", format!("{}={}", D::NAME, digest))
            .signature(&config, key_id, f)
            .await?;

        *req.body_mut() = Some(Body::from(v.as_ref().to_vec()));

        Ok(req)
    }
}

#[cfg(feature = "middleware")]
mod middleware {
    use super::{Config, DigestCreate, Sign, SignError, SignExt, Spawn};
    use reqwest::{Body, Request};
    use reqwest_middleware::RequestBuilder;
    use std::fmt::Display;

    #[async_trait::async_trait]
    impl SignExt for RequestBuilder {
        async fn authorization_signature_with_digest<F, E, K, S, D, V>(
            self,
            config: Config<S>,
            key_id: K,
            mut digest: D,
            v: V,
            f: F,
        ) -> Result<Request, E>
        where
            F: FnOnce(&str) -> Result<String, E> + Send + 'static,
            E: From<SignError> + From<reqwest::Error> + Send + 'static,
            K: Display + Send + 'static,
            D: DigestCreate + Send + 'static,
            V: AsRef<[u8]> + Into<Body> + Send + 'static,
            S: Spawn + Send + Sync,
            Self: Sized,
        {
            let (v, digest) = config
                .spawner
                .spawn_blocking(move || {
                    let digest = digest.compute(v.as_ref());
                    (v, digest)
                })
                .await
                .map_err(|_| SignError::Canceled)?;

            let mut req = self
                .header("Digest", format!("{}={}", D::NAME, digest))
                .authorization_signature(&config, key_id, f)
                .await?;

            *req.body_mut() = Some(Body::from(v.as_ref().to_vec()));

            Ok(req)
        }

        async fn signature_with_digest<F, E, K, S, D, V>(
            self,
            config: Config<S>,
            key_id: K,
            mut digest: D,
            v: V,
            f: F,
        ) -> Result<Request, E>
        where
            F: FnOnce(&str) -> Result<String, E> + Send + 'static,
            E: From<SignError> + From<reqwest::Error> + Send + 'static,
            K: Display + Send + 'static,
            D: DigestCreate + Send + 'static,
            V: AsRef<[u8]> + Into<Body> + Send + 'static,
            S: Spawn + Send + Sync,
            Self: Sized,
        {
            let (v, digest) = config
                .spawner
                .spawn_blocking(move || {
                    let digest = digest.compute(v.as_ref());
                    (v, digest)
                })
                .await
                .map_err(|_| SignError::Canceled)?;

            let mut req = self
                .header("Digest", format!("{}={}", D::NAME, digest))
                .signature(&config, key_id, f)
                .await?;

            *req.body_mut() = Some(Body::from(v.as_ref().to_vec()));

            Ok(req)
        }
    }
}
