use crate::IConfiguration;
use serde_json::json;
use std::fmt::{self, Formatter};
use std::str::FromStr;
use std::time::Duration;

use anyhow::anyhow;
use oauth2::basic::BasicTokenIntrospectionResponse;
use oauth2::revocation::StandardRevocableToken;
use oauth2::{basic::BasicRevocationErrorResponse, RedirectUrl};
use oauth2::{
    basic::{BasicErrorResponse, BasicTokenType},
    Scope,
};
use oauth2::{reqwest::http_client, TokenResponse};
pub use oauth2::{AccessToken, RefreshToken};
use oauth2::{
    AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, TokenUrl,
};

type Client = oauth2::Client<
    BasicErrorResponse,
    Token,
    BasicTokenType,
    BasicTokenIntrospectionResponse,
    StandardRevocableToken,
    BasicRevocationErrorResponse,
>;

// FIXME: Implement errors around the IndieAuth flow.
#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("An IndieAuth operation failed to completeE: {0}")]
    Other(anyhow::Error),
}

// This module holds abstractions for interacting with IndieAuth servers,
// effectively making this a 'client' module.

#[derive(Clone, Debug, Default, PartialEq)]
pub struct Scopes(Vec<Scope>);

impl ToString for Scopes {
    fn to_string(&self) -> String {
        self.0
            .iter()
            .map(|s| s.to_string())
            .collect::<Vec<String>>()
            .join(" ")
    }
}

impl FromStr for Scopes {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self(
            s.split(" ")
                .map(|st| Scope::new(st.to_string()))
                .collect::<Vec<Scope>>(),
        ))
    }
}

impl Scopes {
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn has(&self, scope: &str) -> bool {
        self.0.iter().any(|s| s.to_string().starts_with(scope))
    }

    pub fn as_vec(&self) -> &Vec<Scope> {
        &self.0
    }
}

impl serde::Serialize for Scopes {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

struct ScopesVisitor;

impl<'de> serde::de::Visitor<'de> for ScopesVisitor {
    type Value = Scopes;

    fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
        formatter.write_str("a list of strings or a string of space-separated values")
    }

    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(Scopes(
            s.split(" ")
                .map(|scope_str| Scope::new(scope_str.to_string()))
                .collect::<Vec<_>>(),
        ))
    }
}

impl<'de> serde::Deserialize<'de> for Scopes {
    fn deserialize<D>(d: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        d.deserialize_str(ScopesVisitor)
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct RedeemPayload {
    pub code: String,
    pub client_id: url::Url,
    pub redirect_uri: url::Url,
    pub code_verifier: Option<String>,
}

#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
#[serde(tag = "grant_type", rename_all = "snake_case")]
pub enum GrantPayload {
    AuthorizationCode(RedeemPayload),
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct Profile {
    pub name: Option<String>,
    pub url: Option<url::Url>,
    pub photo: Option<url::Url>,
    pub email: Option<String>,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct CodeResponse {
    me: url::Url,
    profile: Option<Profile>,
}

#[derive(Default, Debug)]
pub struct DiscoveryResult {
    authorization_endpoints: Vec<String>,
    token_endpoints: Vec<String>,
}

pub struct AuthenticationRequest {
    client: Box<Client>,
    me: String,
    scopes: Vec<String>,
    verifier: PkceCodeVerifier,
    challenge: PkceCodeChallenge,
    redirect_uri: Option<String>,
    client_id: String,
}

impl AuthenticationRequest {
    pub fn add_scope(&mut self, scope: &str) {
        self.scopes.push(scope.to_owned());
    }

    pub fn set_querying_identity(&mut self, me: &str) {
        self.me = me.to_string();
    }

    pub fn set_pkce_challenge(&mut self, verifier: &str) {
        self.verifier = PkceCodeVerifier::new(verifier.to_string());
    }

    pub fn get_pkce_verifier(&self) -> String {
        self.verifier.secret().clone()
    }

    /// Creates a new request to obtain authentication information from a IndieAuth server.
    pub fn new(
        me: &str,
        client_id: &str,
        authorization_endpoint: &str,
        redirect_uri: Option<String>,
        scopes: Option<Vec<String>>,
    ) -> anyhow::Result<Self> {
        let client = Box::new(Client::new(
            ClientId::new(client_id.to_string()),
            None,
            AuthUrl::new(authorization_endpoint.to_string())
                .map_err(|e| anyhow!("Failed to parse the authorization endpoint URL: {:#?}", e))?,
            None,
        ));
        let (challenge, verifier) = PkceCodeChallenge::new_random_sha256_len(48);

        Ok(Self {
            client,
            client_id: client_id.to_string(),
            me: me.to_string(),
            scopes: scopes.unwrap_or_default(),
            verifier,
            challenge,
            redirect_uri,
        })
    }

    pub fn construct_url(&self) -> anyhow::Result<(String, String)> {
        let redirect_uri = self
            .redirect_uri
            .clone()
            .or(Some(self.client_id.clone()))
            .ok_or(anyhow::format_err!(
                "No redirect URI or client ID was provied."
            ))?;
        let redirect_url = RedirectUrl::from_url(url::Url::from_str(&redirect_uri)?);
        let request = self
            .client
            .authorize_url(|| CsrfToken::new_random_len(48))
            .set_pkce_challenge(self.challenge.clone())
            .add_extra_param("me", self.me.clone())
            .add_scope(Scope::new(self.scopes.join(" ")))
            .set_redirect_uri(std::borrow::Cow::Borrowed(&redirect_url));
        let (url, state) = dbg!(request.url());

        Ok((url.to_string(), state.secret().clone()))
    }
}

pub struct CodeVerificationRequest {
    pub code: String,
    pub pkce: Option<PkceCodeVerifier>,
    pub client_id: String,
    pub redirect_uri: String,
    pub authorization_endpoint: String,
    pub token_endpoint: String,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Token {
    pub me: url::Url,
    pub access_token: AccessToken,

    #[serde(rename = "scope")]
    pub scopes: Scopes,

    pub expires_in: Option<u64>,
    pub refresh_token: Option<RefreshToken>,
    pub profile: Option<Profile>,
}

impl TokenResponse<BasicTokenType> for Token {
    fn scopes(&self) -> Option<&Vec<Scope>> {
        Some(self.scopes.as_vec())
    }

    fn access_token(&self) -> &AccessToken {
        &self.access_token
    }

    fn refresh_token(&self) -> Option<&RefreshToken> {
        self.refresh_token.as_ref()
    }

    fn expires_in(&self) -> Option<Duration> {
        self.expires_in.map(|s| Duration::from_secs(s))
    }

    fn token_type(&self) -> &BasicTokenType {
        &BasicTokenType::Bearer
    }
}

impl CodeVerificationRequest {
    pub fn redeem(&self) -> anyhow::Result<Token> {
        let client = Client::new(
            ClientId::new(self.client_id.clone()),
            None,
            AuthUrl::new(self.authorization_endpoint.clone())?,
            Some(TokenUrl::new(self.token_endpoint.clone())?),
        );

        let mut request = client.exchange_code(AuthorizationCode::new(self.code.clone()));

        request = match self.pkce.as_ref() {
            Some(verifier) => {
                request.set_pkce_verifier(PkceCodeVerifier::new(verifier.secret().to_string()))
            }
            None => request,
        };

        request
            .request(http_client)
            .map_err(|e| anyhow!("{:#?}", e))
    }
}

/// Confirms that the provided token is valid with the provided endpoint.
pub async fn verify<C>(cfg: &C, endpoint: &str, access_token: &str) -> Option<Token>
where
    C: IConfiguration + Send + Sync,
{
    let resp = cfg
        .request(Some("Requesting information about a token".to_owned()))
        .request(reqwest::Method::GET, endpoint.to_owned())
        .header("accept", "application/json")
        .bearer_auth(access_token)
        .send()
        .await
        .ok();

    match resp
        .expect("The endpoint didn't provide a valid response.")
        .error_for_status()
    {
        Ok(resp) => resp
            .text()
            .await
            .ok()
            .map(|json_text| dbg!(serde_json::from_str(&json_text)).ok())
            .flatten(),
        Err(_err) => None,
    }
}

pub async fn redeem<'a, C, R>(
    cfg: &C,
    endpoint: &str,
    redeem_payload: RedeemPayload,
) -> Result<R, crate::Error>
where
    C: IConfiguration + Send + Sync,
    R: for<'de> serde::Deserialize<'de>,
{
    let endpoint_resp = cfg
        .request(Some(
            "Requesting information about an authorization request".to_owned(),
        ))
        .request(reqwest::Method::POST, endpoint.to_owned())
        .header("accept", "application/json")
        .header("content-type", "application/json")
        .body(
            json!({
                "grant_type": "authorization_code",
                "code": redeem_payload.code,
                "client_id": redeem_payload.client_id,
                "redirect_uri": redeem_payload.redirect_uri,
                "code_verifier": redeem_payload.code_verifier
            })
            .to_string(),
        )
        .send()
        .await
        .map_err(|e| crate::Error::Other(anyhow::anyhow!(e)))?;

    let resp = endpoint_resp
        .error_for_status()
        .map_err(|e| anyhow::anyhow!(e))?;
    log::info!("Handling response from endpoint.");
    let text = resp
        .text()
        .await
        .map_err(|e| crate::Error::Other(anyhow::anyhow!(e)))?;

    serde_json::from_str(text.as_str()).map_err(|e| crate::Error::Other(anyhow::anyhow!(e)))
}

#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
    use super::*;
    use crate::test;
    use oauth2::{PkceCodeChallenge, PkceCodeVerifier};

    #[test]
    fn AuthenticationRequest_construct_url_has_expected_params() {
        let me = "https://jacky.wtf".to_string();
        let client_id = "http://sele.black.af".to_string();
        let endpoint = "https://sele.black.af/endpoints/wow".to_string();
        let redirect_uri =
            "https://special-domain-name.endpoints.sele.black.af/testing_page".to_string();
        let result = AuthenticationRequest::new(
            &me,
            &client_id,
            &endpoint,
            Some(redirect_uri.clone()),
            None,
        );

        assert!(matches!(result, Ok(_)));
        let mut req = result.unwrap();

        req.add_scope("read");
        req.add_scope("create");

        let url_and_state = req.construct_url();

        assert!(url_and_state.is_ok());
        let (url, state) = url_and_state.unwrap();
        let challenge = PkceCodeChallenge::from_code_verifier_sha256(&PkceCodeVerifier::new(
            req.get_pkce_verifier(),
        ));
        let challenge_str = challenge.as_str();

        use std::collections::HashMap;

        let params: HashMap<String, String> =
            dbg!(serde_qs::from_str(url.parse::<url::Url>().unwrap().query().unwrap()).unwrap());
        assert!(params.contains_key(&"redirect_uri".to_string()));
        assert_eq!(
            params.get("scope").cloned(),
            Some("read create".to_string())
        );
        assert_eq!(
            params.get("code_challenge").cloned(),
            Some(challenge_str.to_string())
        );
        assert_eq!(
            params.get("response_type").cloned(),
            Some("code".to_string())
        );
        assert_eq!(
            params.get("code_challenge_method").cloned(),
            Some("S256".to_string())
        );
        assert_eq!(params.get("state").cloned(), Some(state));
        assert_eq!(params.get("me").cloned(), Some(me));
        assert_eq!(params.get("client_id").cloned(), Some(client_id));
        assert_eq!(params.get("redirect_uri").cloned(), Some(redirect_uri));
    }

    #[test]
    fn CodeVerificationRequest_redeem_success() {
        let (_challenge, verifier) = PkceCodeChallenge::new_random_sha256();
        let code = "a-valid-code".to_owned();
        let authorization_endpoint = format!("{}/auth-endpoint", mockito::server_url());
        let token_endpoint = format!("{}/token-endpoint", mockito::server_url());

        let req = super::CodeVerificationRequest {
            code,
            pkce: Some(verifier),
            client_id: "https://sele.black.af".to_owned(),
            redirect_uri: "https://sele.black.af/bounce-back".to_owned(),
            authorization_endpoint,
            token_endpoint,
        };
        let body = serde_json::json!({
            "me": "https://jacky.wtf/",
            "token_type": "bearer",
            "scope": "read profile email create",
            "access_token": "the-access-token",
            "profile": {
                "name": "Jacky Alciné",
                "url": "https://jacky.wtf/",
                "photo": "https://jacky.wtf/photo.jpeg",
                "email": "yo+codespam@jacky.wtf"
            }
        })
        .to_string();

        let endpoint_mock = mockito::mock("POST", "/token-endpoint")
            .with_status(200)
            .with_body(body)
            .expect_at_least(1)
            .create();

        let result = dbg!(req.redeem());
        endpoint_mock.assert();
        assert!(matches!(result, Ok(_)));

        let token = result.unwrap();
        assert!(token.profile.is_some());
        let profile = token.profile.unwrap();
        assert_eq!(profile.url, "https://jacky.wtf/".parse().ok());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn verify_confirms_valid_token() {
        let cfg = test::init();
        let endpoint = format!("{}/endpoint", mockito::server_url());
        let access_token = "access-token";
        let body = serde_json::json!({
            "access_token": "a-token",
            "me": "https://jacky.wtf/",
            "scope": "read profile email"
        })
        .to_string();
        let endpoint_mock = mockito::mock("GET", "/endpoint")
            .with_body(&body)
            .expect_at_most(1)
            .create();

        endpoint_mock.assert();
        let result = super::verify(&cfg, &endpoint, access_token).await;
        assert!(result.is_some());

        let token = result.unwrap();
        assert_eq!(token.me, "https://jacky.wtf/".parse().unwrap());
        assert_eq!(token.access_token.secret().clone(), "a-token".to_owned());
        assert_eq!(token.scopes.to_string(), "read profile email".to_owned());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn redeem_resolves_responses() {
        let cfg = test::init();
        let endpoint = format!("{}/endpoint", mockito::server_url());
        let body = serde_json::json!({
            "me": "https://jacky.wtf/",
            "token_type": "bearer",
            "scope": "read profile email create",
            "access_token": "the-access-token",
            "profile": {
                "name": "Jacky Alciné",
                "url": "https://jacky.wtf/",
                "photo": "https://jacky.wtf/photo.jpeg",
                "email": "yo+codespam@jacky.wtf"
            }

        })
        .to_string();

        let endpoint_mock = mockito::mock("POST", "/endpoint")
            .with_body(&body)
            .expect_at_most(2)
            .create();
        let payload = RedeemPayload {
            code: "a-code".to_owned(),
            code_verifier: Some("a-code-verifier".to_owned()),
            client_id: "https://indieweb.org".parse().unwrap(),
            redirect_uri: "https://indieweb.org/back".parse().unwrap(),
        };

        let optional_token: Result<super::Token, crate::Error> =
            super::redeem(&cfg, &endpoint, payload.clone()).await;
        let optional_code: Result<super::CodeResponse, crate::Error> =
            super::redeem(&cfg, &endpoint, payload.clone()).await;
        endpoint_mock.assert();

        assert!(optional_code.is_ok());
        assert!(optional_token.is_ok());
    }

    #[test]
    fn Scopes_assert_presence() -> Result<(), anyhow::Error> {
        assert!(!Scopes::from_str("read profile")?.has("update"));
        assert!(Scopes::from_str("read profile")?.has("read"));
        Ok(())
    }
}
