use anyhow::anyhow;
use anyhow::Result;
use http::header;
use log::debug;

use super::credential::CredentialLoader;
use super::credential::Token;
use super::credential::TokenLoad;
use super::v4;
use crate::request::SignableRequest;
use crate::time::Duration;

/// Builder for Signer.
#[derive(Default)]
pub struct Builder {
    scope: Option<String>,
    service_account: Option<String>,

    credential_path: Option<String>,
    credential_content: Option<String>,

    allow_anonymous: bool,
    disable_load_from_env: bool,
    disable_load_from_well_known_location: bool,
    disable_load_from_vm_metadata: bool,
    customed_token_loader: Option<Box<dyn TokenLoad>>,
}

impl Builder {
    /// Specify scope for Signer.
    ///
    /// For example, valid scopes for google cloud services should be
    ///
    /// - read-only: `https://www.googleapis.com/auth/devstorage.read_only`
    /// - read-write: `https://www.googleapis.com/auth/devstorage.read_write`
    /// - full-control: `https://www.googleapis.com/auth/devstorage.full_control`
    ///
    /// Reference: [Cloud Storage authentication](https://cloud.google.com/storage/docs/authentication)
    pub fn scope(&mut self, scope: &str) -> &mut Self {
        self.scope = Some(scope.to_string());
        self
    }

    /// Specify service account for Signer.
    ///
    /// If not set, use `default` instead.
    pub fn service_account(&mut self, service_account: &str) -> &mut Self {
        self.service_account = Some(service_account.to_string());
        self
    }

    /// Load credential from path.
    ///
    /// The credential should be generated by Google Cloud Platform.
    ///
    /// # Notes
    ///
    /// We will load from default credential by default, `credential_path`
    /// only used to for user customed credential path.
    ///
    /// Read more in [Authenticating as a service account](https://cloud.google.com/docs/authentication/production)
    pub fn credential_path(&mut self, path: &str) -> &mut Self {
        self.credential_path = Some(path.to_string());
        self
    }

    /// Load credential from base64 content.
    ///
    /// The credential should be generated by Google Cloud Platform.
    ///
    /// # Notes
    ///
    /// We will load from default credential by default, `credential_content`
    /// only used to for user customed credential content.
    ///
    /// Read more in [Authenticating as a service account](https://cloud.google.com/docs/authentication/production)
    pub fn credential_content(&mut self, credential: &str) -> &mut Self {
        self.credential_content = Some(credential.to_string());
        self
    }

    /// Set customed token loader for builder.
    ///
    /// We will load token from customed_token_loader first if set.
    pub fn customed_token_loader(&mut self, f: impl TokenLoad) -> &mut Self {
        self.customed_token_loader = Some(Box::new(f));
        self
    }

    /// Use exising information to build a new signer.
    ///
    ///
    /// The builder should not be used anymore.
    pub fn build(&mut self) -> Result<Signer> {
        let scope = match &self.scope {
            Some(v) => v.clone(),
            None => return Err(anyhow!("google signer requires scope, but not set")),
        };

        let mut cred_loader = if let Some(path) = &self.credential_path {
            CredentialLoader::from_path(path)?
        } else if let Some(content) = &self.credential_content {
            CredentialLoader::from_base64(content)?
        } else {
            CredentialLoader::default()
        };
        cred_loader = cred_loader.with_scope(&scope);

        if self.disable_load_from_env {
            cred_loader = cred_loader.with_disable_env();
        }
        if self.disable_load_from_well_known_location {
            cred_loader = cred_loader.with_disable_well_known_location();
        }
        if self.disable_load_from_vm_metadata {
            cred_loader = cred_loader.with_disable_vm_metadata();
        }
        if self.allow_anonymous {
            cred_loader = cred_loader.with_allow_anonymous();
        }
        if let Some(acc) = &self.service_account {
            cred_loader = cred_loader.with_service_account(acc);
        }
        if let Some(f) = self.customed_token_loader.take() {
            cred_loader = cred_loader.with_customed_token_loader(f);
        }

        Ok(Signer {
            credential_loader: cred_loader,
            allow_anonymous: self.allow_anonymous,
        })
    }
}

/// Singer that implement Google OAuth2 Authentication.
///
/// ## Reference
///
/// -  [Authenticating as a service account](https://cloud.google.com/docs/authentication/production)
pub struct Signer {
    credential_loader: CredentialLoader,

    /// Allow anonymous request if credential is not loaded.
    allow_anonymous: bool,
}

impl Signer {
    /// Create a builder of Signer.
    pub fn builder() -> Builder {
        Builder::default()
    }

    /// Load credential via credential load chain specified while building.
    ///
    /// # Note
    ///
    /// This function should never be exported to avoid credential leaking by
    /// mistake.
    fn token(&self) -> Option<Token> {
        self.credential_loader.load()
    }

    /// Signing request.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use anyhow::Result;
    /// use reqsign::GoogleSigner;
    /// use reqwest::Client;
    /// use reqwest::Request;
    /// use reqwest::Url;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<()> {
    ///     // Signer will load region and credentials from environment by default.
    ///     let signer = GoogleSigner::builder()
    ///         .scope("https://www.googleapis.com/auth/devstorage.read_only")
    ///         .build()?;
    ///
    ///     // Construct request
    ///     let url = Url::parse("https://storage.googleapis.com/storage/v1/b/test")?;
    ///     let mut req = reqwest::Request::new(http::Method::GET, url);
    ///
    ///     // Signing request with Signer
    ///     signer.sign(&mut req)?;
    ///
    ///     // Sending already signed request.
    ///     let resp = Client::new().execute(req).await?;
    ///     println!("resp got status: {}", resp.status());
    ///     Ok(())
    /// }
    /// ```
    ///
    /// # TODO
    ///
    /// we can also send API via signed JWT: [Addendum: Service account authorization without OAuth](https://developers.google.com/identity/protocols/oauth2/service-account#jwt-auth)
    pub fn sign(&self, req: &mut impl SignableRequest) -> Result<()> {
        if let Some(token) = self.token() {
            req.insert_header(header::AUTHORIZATION, {
                let mut value: http::HeaderValue =
                    format!("Bearer {}", token.access_token()).parse()?;
                value.set_sensitive(true);

                value
            })?;

            return Ok(());
        }

        if self.allow_anonymous {
            debug!("token not found and anonymous is allowed, skipping signing.");
            return Ok(());
        }

        Err(anyhow!("token not found"))
    }

    /// Sign the query with a duration.
    ///
    /// # Example
    /// ```no_run
    /// use time::Duration;
    /// use anyhow::Result;
    /// use reqsign::GoogleSigner;
    /// use reqwest::{Client, Url};
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<()> {
    ///     // Signer will load region and credentials from environment by default.
    ///     let signer = GoogleSigner::builder()
    ///         .credential_path("/Users/wolfv/Downloads/noted-throne-361708-bf95cdbf3fea.json")
    ///         .scope("storage")
    ///         .build()?;
    ///
    ///     // Construct request
    ///     let url = Url::parse("https://storage.googleapis.com/testbucket-reqsign/CONTRIBUTING.md")?;
    ///     let mut req = reqwest::Request::new(http::Method::GET, url);
    ///
    ///     // Signing request with Signer
    ///     signer.sign_query(&mut req, Duration::hours(1))?;
    ///
    ///     println!("signed request: {:?}", req);
    ///     // Sending already signed request.
    ///     let resp = Client::new().execute(req).await?;
    ///     println!("resp got status: {}", resp.status());
    ///     println!("resp got body: {}", resp.text().await?);
    ///     Ok(())
    /// }
    /// ```
    pub fn sign_query(&self, query: &mut impl SignableRequest, duration: Duration) -> Result<()> {
        let credentials = self.credential_loader.load_credential().unwrap();
        let v4_signer = v4::Signer::builder()
            // TODO set this from outside?
            .service("storage")
            .region("auto")
            .credential(credentials)
            .build()?;
        v4_signer.sign_query(query, duration)
    }
}

#[cfg(test)]
mod tests {
    use reqwest::blocking::Client;

    use super::*;

    #[derive(Debug)]
    struct TestLoader {
        client: Client,
    }

    impl TokenLoad for TestLoader {
        fn load_token(&self) -> Result<Option<Token>> {
            self.client.get("https://xuanwo.io").send()?;
            Ok(None)
        }
    }

    #[test]
    fn test_with_customed_token_loader() -> Result<()> {
        let client = Client::default();

        let _ = Builder::default()
            .scope("test")
            .customed_token_loader(TestLoader { client })
            .build()?;

        Ok(())
    }

    #[test]
    fn test_sign_query() -> Result<()> {
        let credential_path = format!(
            "{}/testdata/services/google/testbucket_credential.json",
            std::env::current_dir()
                .expect("current_dir must exist")
                .to_string_lossy()
        );

        let signer = Signer::builder()
            .credential_path(&credential_path)
            .scope("storage")
            .build()?;

        let mut req = http::Request::new("");
        *req.method_mut() = http::Method::GET;
        *req.uri_mut() = "https://storage.googleapis.com/testbucket-reqsign/CONTRIBUTING.md"
            .parse()
            .expect("url must be valid");

        signer.sign_query(&mut req, time::Duration::hours(1))?;

        let query = req.query().unwrap();
        assert!(query.contains("X-Goog-Algorithm=GOOG4-RSA-SHA256"));
        assert!(query.contains("X-Goog-Credential"));

        Ok(())
    }
}
