use http_signature_normalization::create::Signed;
use httpdate::HttpDate;
use reqwest::{
    header::{InvalidHeaderValue, ToStrError},
    Request, RequestBuilder,
};
use std::{
    convert::TryInto,
    fmt::Display,
    time::{Duration, SystemTime},
};

pub use http_signature_normalization::RequiredError;

#[cfg(feature = "digest")]
pub mod digest;

pub mod prelude {
    pub use crate::{Config, Sign, SignError};

    #[cfg(feature = "default-spawner")]
    pub use crate::default_spawner::DefaultSpawner;

    #[cfg(feature = "digest")]
    pub use crate::digest::{DigestCreate, SignExt};
}

#[cfg(feature = "default-spawner")]
pub use default_spawner::DefaultSpawner;

#[cfg(feature = "default-spawner")]
#[derive(Clone, Debug, Default)]
/// Configuration for signing and verifying signatures
///
/// By default, the config is set up to create and verify signatures that expire after 10 seconds,
/// and use the `(created)` and `(expires)` fields that were introduced in draft 11
pub struct Config<Spawner = DefaultSpawner> {
    /// The inner config type
    config: http_signature_normalization::Config,

    /// Whether to set the Host header
    set_host: bool,

    /// Whether to set the Date header
    set_date: bool,

    /// How to spawn blocking tasks
    spawner: Spawner,
}

#[cfg(not(feature = "default-spawner"))]
#[derive(Clone, Debug, Default)]
/// Configuration for signing and verifying signatures
///
/// By default, the config is set up to create and verify signatures that expire after 10 seconds,
/// and use the `(created)` and `(expires)` fields that were introduced in draft 11
pub struct Config<Spawner> {
    /// The inner config type
    config: http_signature_normalization::Config,

    /// Whether to set the Host header
    set_host: bool,

    /// Whether to set the Date header
    set_date: bool,

    /// How to spawn blocking tasks
    spawner: Spawner,
}

#[cfg(feature = "default-spawner")]
mod default_spawner {
    use super::{Canceled, Config, Spawn};

    impl Config<DefaultSpawner> {
        /// Create a new config with the default spawner
        pub fn new() -> Self {
            Default::default()
        }
    }

    /// A default implementation of Spawner for spawning blocking operations
    #[derive(Clone, Copy, Debug, Default)]
    pub struct DefaultSpawner;

    /// The future returned by DefaultSpawner when spawning blocking operations on the tokio
    /// blocking threadpool
    pub struct DefaultSpawnerFuture<Out> {
        inner: tokio::task::JoinHandle<Out>,
    }

    impl Spawn for DefaultSpawner {
        type Future<T> = DefaultSpawnerFuture<T> where T: Send;

        fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
        where
            Func: FnOnce() -> Out + Send + 'static,
            Out: Send + 'static,
        {
            DefaultSpawnerFuture {
                inner: tokio::task::spawn_blocking(func),
            }
        }
    }

    impl<Out> std::future::Future for DefaultSpawnerFuture<Out> {
        type Output = Result<Out, Canceled>;

        fn poll(
            mut self: std::pin::Pin<&mut Self>,
            cx: &mut std::task::Context<'_>,
        ) -> std::task::Poll<Self::Output> {
            let res = std::task::ready!(std::pin::Pin::new(&mut self.inner).poll(cx));

            std::task::Poll::Ready(res.map_err(|_| Canceled))
        }
    }
}

/// An error that indicates a blocking operation panicked and cannot return a response
#[derive(Debug)]
pub struct Canceled;

impl std::fmt::Display for Canceled {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Operation was canceled")
    }
}

impl std::error::Error for Canceled {}

/// A trait dictating how to spawn a future onto a blocking threadpool. By default,
/// http-signature-normalization-actix will use tokio's built-in blocking threadpool, but this
/// can be customized
pub trait Spawn {
    /// The future type returned by spawn_blocking
    type Future<T>: std::future::Future<Output = Result<T, Canceled>> + Send
    where
        T: Send;

    /// Spawn the blocking function onto the threadpool
    fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
    where
        Func: FnOnce() -> Out + Send + 'static,
        Out: Send + 'static;
}

/// A trait implemented by the reqwest RequestBuilder type to add an HTTP Signature to the request
#[async_trait::async_trait]
pub trait Sign {
    /// Add an Authorization Signature to the request
    async fn authorization_signature<F, E, K, S>(
        self,
        config: &Config<S>,
        key_id: K,
        f: F,
    ) -> Result<Request, E>
    where
        Self: Sized,
        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
        E: From<SignError> + From<reqwest::Error> + Send + 'static,
        K: Display + Send,
        S: Spawn + Send + Sync;

    /// Add a Signature to the request
    async fn signature<F, E, K, S>(self, config: &Config<S>, key_id: K, f: F) -> Result<Request, E>
    where
        Self: Sized,
        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
        E: From<SignError> + From<reqwest::Error> + Send + 'static,
        K: Display + Send,
        S: Spawn + Send + Sync;
}

#[derive(Debug, thiserror::Error)]
pub enum SignError {
    #[error("Failed to read header, {0}")]
    /// An error occurred when reading the request's headers
    Header(#[from] ToStrError),

    #[error("Failed to write header, {0}")]
    /// An error occured when adding a new header
    NewHeader(#[from] InvalidHeaderValue),

    #[error("{0}")]
    /// Some headers were marked as required, but are missing
    RequiredError(#[from] RequiredError),

    #[error("No host provided for URL, {0}")]
    /// Missing host
    Host(String),

    #[error("Cannot sign request with body already present")]
    BodyPresent,

    #[error("Panic in spawn blocking")]
    Canceled,
}

impl<Spawner> Config<Spawner> {
    /// Create a new config with the provided spawner
    pub fn new_with_spawner(spawner: Spawner) -> Self {
        Config {
            config: Default::default(),
            set_host: Default::default(),
            set_date: Default::default(),
            spawner,
        }
    }

    /// This method can be used to include the Host header in the HTTP Signature without
    /// interfering with Reqwest's built-in Host mechanisms
    pub fn set_host_header(self) -> Self {
        Config {
            config: self.config,
            set_host: true,
            set_date: self.set_date,
            spawner: self.spawner,
        }
    }

    /// Enable mastodon compatibility
    ///
    /// This is the same as disabling the use of `(created)` and `(expires)` signature fields,
    /// requiring the Date header, and requiring the Host header
    pub fn mastodon_compat(self) -> Self {
        Config {
            config: self.config.mastodon_compat(),
            set_host: true,
            set_date: true,
            spawner: self.spawner,
        }
    }

    /// Require the Digest header be set
    ///
    /// This is useful for POST, PUT, and PATCH requests, but doesn't make sense for GET or DELETE.
    pub fn require_digest(self) -> Self {
        Config {
            config: self.config.require_digest(),
            set_host: self.set_host,
            set_date: self.set_date,
            spawner: self.spawner,
        }
    }

    /// Opt out of using the (created) and (expires) fields introduced in draft 11
    ///
    /// Note that by enabling this, the Date header becomes required on requests. This is to
    /// prevent replay attacks
    pub fn dont_use_created_field(self) -> Self {
        Config {
            config: self.config.dont_use_created_field(),
            set_host: self.set_host,
            set_date: self.set_date,
            spawner: self.spawner,
        }
    }

    /// Set the expiration to a custom duration
    pub fn set_expiration(self, expiries_after: Duration) -> Self {
        Config {
            config: self.config.set_expiration(expiries_after),
            set_host: self.set_host,
            set_date: self.set_date,
            spawner: self.spawner,
        }
    }

    /// Require a header on signed requests
    pub fn require_header(self, header: &str) -> Self {
        Config {
            config: self.config.require_header(header),
            set_host: self.set_host,
            set_date: self.set_date,
            spawner: self.spawner,
        }
    }

    pub fn set_spawner<NewSpawner: Spawn>(self, spawner: NewSpawner) -> Config<NewSpawner> {
        Config {
            config: self.config,
            set_host: self.set_host,
            set_date: self.set_date,
            spawner,
        }
    }
}

#[async_trait::async_trait]
impl Sign for RequestBuilder {
    async fn authorization_signature<F, E, K, S>(
        self,
        config: &Config<S>,
        key_id: K,
        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,
        S: Spawn + Send + Sync,
    {
        let mut request = self.build()?;
        let signed = prepare(&mut request, config, key_id, f).await?;

        let auth_header = signed.authorization_header();
        request.headers_mut().insert(
            "Authorization",
            auth_header.parse().map_err(SignError::NewHeader)?,
        );

        Ok(request)
    }

    async fn signature<F, E, K, S>(self, config: &Config<S>, key_id: K, 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,
        S: Spawn + Send + Sync,
    {
        let mut request = self.build()?;
        let signed = prepare(&mut request, config, key_id, f).await?;

        let sig_header = signed.signature_header();

        request.headers_mut().insert(
            "Signature",
            sig_header.parse().map_err(SignError::NewHeader)?,
        );

        Ok(request)
    }
}

async fn prepare<F, E, K, S>(
    req: &mut Request,
    config: &Config<S>,
    key_id: K,
    f: F,
) -> Result<Signed, E>
where
    F: FnOnce(&str) -> Result<String, E> + Send + 'static,
    E: From<SignError> + Send + 'static,
    K: Display + Send,
    S: Spawn,
{
    if config.set_date && !req.headers().contains_key("date") {
        req.headers_mut().insert(
            "date",
            HttpDate::from(SystemTime::now())
                .to_string()
                .try_into()
                .map_err(SignError::from)?,
        );
    }
    let mut bt = std::collections::BTreeMap::new();
    for (k, v) in req.headers().iter() {
        bt.insert(
            k.as_str().to_owned(),
            v.to_str().map_err(SignError::from)?.to_owned(),
        );
    }
    if config.set_host {
        let header_string = req
            .url()
            .host()
            .ok_or_else(|| SignError::Host(req.url().to_string()))?
            .to_string();

        let header_string = match req.url().port() {
            None | Some(443) | Some(80) => header_string,
            Some(port) => format!("{}:{}", header_string, port),
        };

        bt.insert("Host".to_string(), header_string);
    }
    let path_and_query = if let Some(query) = req.url().query() {
        format!("{}?{}", req.url().path(), query)
    } else {
        req.url().path().to_string()
    };
    let unsigned = config
        .config
        .begin_sign(req.method().as_str(), &path_and_query, bt)
        .map_err(SignError::from)?;

    let key_string = key_id.to_string();
    let signed = config
        .spawner
        .spawn_blocking(move || unsigned.sign(key_string, f))
        .await
        .map_err(|_| SignError::Canceled)??;
    Ok(signed)
}

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

    #[async_trait::async_trait]
    impl Sign for RequestBuilder {
        async fn authorization_signature<F, E, K, S>(
            self,
            config: &Config<S>,
            key_id: K,
            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,
            S: Spawn + Send + Sync,
        {
            let mut request = self.build()?;
            let signed = prepare(&mut request, config, key_id, f).await?;

            let auth_header = signed.authorization_header();
            request.headers_mut().insert(
                "Authorization",
                auth_header.parse().map_err(SignError::NewHeader)?,
            );

            Ok(request)
        }

        async fn signature<F, E, K, S>(
            self,
            config: &Config<S>,
            key_id: K,
            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,
            S: Spawn + Send + Sync,
        {
            let mut request = self.build()?;
            let signed = prepare(&mut request, config, key_id, f).await?;

            let sig_header = signed.signature_header();

            request.headers_mut().insert(
                "Signature",
                sig_header.parse().map_err(SignError::NewHeader)?,
            );

            Ok(request)
        }
    }
}
