use std::{ops::Deref, str::FromStr, time::Duration};
use url::Url;

use oauth2::{
    basic::{
        BasicErrorResponse, BasicErrorResponseType, BasicRevocationErrorResponse,
        BasicTokenIntrospectionResponse, BasicTokenType,
    },
    revocation::StandardRevocableToken,
    CsrfToken, StandardErrorResponse, TokenResponse,
};

use crate::{algorithms::link_rel, http::Client as HttpClient, traits::as_string_or_list};
pub use oauth2::{
    AccessToken, AuthUrl, AuthorizationCode, ClientId, PkceCodeChallenge, PkceCodeVerifier,
    RedirectUrl, RefreshToken, ResponseType, Scope, TokenUrl,
};

/// Represents the OAuth 2 client that IndieAuth is based on.
pub type OAuth2Client = oauth2::Client<
    BasicErrorResponse,
    Token,
    BasicTokenType,
    BasicTokenIntrospectionResponse,
    StandardRevocableToken,
    BasicRevocationErrorResponse,
>;

/// Represents all of the known errors that can happen with IndieAuth.
#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Failed to verify an access token: {0}.")]
    AccessTokenFailure(oauth2::RequestTokenError<Box<crate::Error>, ErrorResponse>),

    #[error("Failed request: {0:?}.")]
    Request(StandardErrorResponse<BasicErrorResponseType>),

    #[error("No redirect URI provided for authorization request.")]
    NoRedirectUriProvidedForAuthorization,

    #[error(transparent)]
    Configuration(#[from] oauth2::ConfigurationError),

    #[error("No token endpoint URL was provided for this client.")]
    NoTokenEndpoint,

    #[error("Got an unexpected response while dispatching a request.")]
    InvalidResponse,

    #[error("No authorization endpoint was found.")]
    NoAuthorizationEndpoint,

    #[error("The metadata response was invalid: {0}.")]
    InvalidMetadataResponse(String),

    #[error("No metadata information returned from the endpoint.")]
    NoMetadataResponse,

    // https://www.rfc-editor.org/rfc/rfc8414#section-3.3
    #[error("The obtained issuer did not match the expected value.")]
    MismatchInIssuer,

    #[error("No IndieAuth metadata endpoint could be found.")]
    NoMetadataEndpointFound,

    #[error("The client ID provided did not match the initial one used in this request.")]
    MismatchedClientId,

    #[error("The redirect URL provided did not satisfy any requirements for redirect URLs by this client.")]
    InvalidRedirectUrl,

    #[error("The provided token is invalid.")]
    InvalidToken,

    #[error("{error}: {error_description}")]
    Conventional {
        error: String,
        error_description: String,
        fields: serde_json::Value,
    },
}

#[non_exhaustive]
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum ProfileResponse {
    Profile(Profile),
    Error(ErrorResponse),
}

/// Provides a structured interface to use for OAuth2.
pub struct OAuth2Interface(pub OAuth2Client);

impl Deref for OAuth2Interface {
    type Target = OAuth2Client;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Provides a structured response for OAuth2 failures.
pub type ErrorResponse = StandardErrorResponse<BasicErrorResponseType>;

impl OAuth2Interface {
    /// Trades in an authorization code for either a profile or token.
    ///
    /// This handles the work of using an [code][AuthorizationCode]
    /// and a [desired resource][DesiredResourceAuthorization][] with
    /// other fields (for PKCE, etc) and aims to return back either
    /// an [token][AccessToken] or a profile of one's own definition.
    pub async fn authorize(
        &self,
        client: &impl HttpClient,
        resource: DesiredResourceAuthorization,
        code: oauth2::AuthorizationCode,
        redirect_uri: Option<RedirectUrl>,
        code_verifier: PkceCodeVerifier,
    ) -> Result<Response, crate::Error> {
        match resource {
            DesiredResourceAuthorization::Token => {
                let mut request = self.exchange_code(code).set_pkce_verifier(code_verifier);
                if let Some(ru) = redirect_uri {
                    request = request.set_redirect_uri(std::borrow::Cow::Owned(ru));
                }

                request
                    .request_async(|r| async { send_using_client(r, client).await })
                    .await
                    .map_err(Error::AccessTokenFailure)
                    .map_err(crate::Error::IndieAuth)
                    .map(|t| Response::AccessToken(Box::new(t)))
            }
            DesiredResourceAuthorization::Profile => {
                let redirect_url = redirect_uri
                    .or_else(|| self.redirect_url().cloned())
                    .ok_or(crate::Error::IndieAuth(
                        Error::NoRedirectUriProvidedForAuthorization,
                    ))?;
                let body = crate::http::Body::Bytes(
                    serde_qs::to_string(&serde_json::json!({
                        "grant_type": "authorization_code",
                        "code": code,
                        "client_id": self.client_id(),
                        "code_verifier": code_verifier,
                        "redirect_uri": redirect_url,
                    }))
                    .map(|s| s.into_bytes())
                    .map_err(crate::Error::Qs)?,
                );
                let request = http::Request::builder()
                    .method("POST")
                    .uri(self.auth_url().as_str())
                    .header(
                        http::header::CONTENT_TYPE,
                        crate::http::CONTENT_TYPE_FORM_URLENCODED,
                    )
                    .body(body)
                    .map_err(crate::Error::Http)?;
                client.send_request(request).await.and_then(|response| {
                    let body = crate::http::from_json_value::<ProfileResponse>(response)?;

                    match body {
                        ProfileResponse::Profile(profile) => {
                            Ok(Response::Profile(Box::new(profile)))
                        }
                        ProfileResponse::Error(e) => {
                            Err(crate::Error::IndieAuth(Error::Request(e)))
                        }
                    }
                })
            }
        }
    }
}

/// Represents a collection of the most common values in IndieAuth contexts.
pub struct Parameters {
    /// The scopes used for this request.
    pub scopes: Scopes,

    /// See [ClientId][ClientId] for more information.
    pub client_id: ClientId,

    /// See [AuthUrl][AuthUrl] for more information.
    pub authorization_url: AuthUrl,

    /// See [TokenUrl][TokenUrl] for more information.
    ///
    /// This is only required when wanting to work with tokens.
    pub token_endpoint_url: Option<TokenUrl>,
}

/// Different kinds of requests made with IndieAuth.
///
/// This is meant to capture individual steps around common IndieAuth
/// operations This is most useful when using [`dispatch`][Client::dispatch].
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum Request {
    /// Provides the information for an authorization URL.
    ///
    /// When successful, [`dispatch`][Client::dispatch] will return
    /// [`Response::AuthenticationUrl`][Response::AuthenticationUrl] for this
    /// request.
    BuildAuthorizationUrl {
        /// Optional scopes to use in replace of the ones predefined by a client.
        ///
        /// More information at [the spec
        /// page](https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.4).
        scope: Option<Scopes>,

        /// Optional redirect URL to use for this request.
        ///
        /// More information at [the spec
        /// page](https://indieauth.spec.indieweb.org/20201126/#redirect-url)
        redirect_uri: Option<RedirectUrl>,

        /// The URL of the identity you'd want to identify as.
        ///
        /// More information at [the spec page](https://indieauth.spec.indieweb.org/20201126/#authorization-request-p-7).
        me: Option<Url>,
    },

    /// Attempts to exchange the provided code for profile information.
    ///
    /// When successful, [`dispatch`][Client::dispatch] will return
    /// [`Response::Profile`][Response::Profile] for this request.
    CompleteAuthorization {
        /// The kind of content wanted from this authorization attempt.
        resource: DesiredResourceAuthorization,

        /// The authorization code to exchange for a [profile][Profile].
        code: AuthorizationCode,

        /// The string used to verify the PKCE transaction of this request.
        code_verifier: String,

        /// The location that this request should be redirected to after completion.
        redirect_uri: Option<RedirectUrl>,
    },

    /// Attempts to verify that a token is still valid.
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#access-token-verification-request> for
    /// more information.
    VerifyAccessToken(AccessToken),

    /// Attempts to revoke a token.
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#x7-1-token-revocation-request> for
    /// more information.
    RevokeAccessToken(AccessToken),
}

/// Different kinds of responses made with IndieAuth.
///
/// This is meant to capture the results of common IndieAuth
/// operations This is most useful when matching against [`dispatch`][Client::dispatch].
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum Response {
    /// This represents a signed URL to visit to begin the authorization attempt.
    ///
    /// This is the successful return value of calling
    /// [Request::BuildAuthorizationUrl][Request::BuildAuthorizationUrl]. See
    /// <https://indieauth.spec.indieweb.org/20201126/#authorization-request> for more information.
    AuthenticationUrl {
        /// The PKCE verification string to check against in a later request (usually when seeking
        /// [Response::AccessToken][Response::AccessToken] when calling
        /// [Request::CompleteAuthorization][Request::CompleteAuthorization]).
        ///
        /// See <https://indieauth.spec.indieweb.org/20201126/#authorization-request-p-4> for more
        /// information.
        verifier: String,

        /// The PKCE challenge string that represents the unsigned value to be checked against in a
        /// later request (usually when seeking [Response::AccessToken][Response::AccessToken] when calling
        /// [Request::CompleteAuthorization][Request::CompleteAuthorization]).
        ///
        /// See <https://indieauth.spec.indieweb.org/20201126/#authorization-request-li-5> for more
        /// information.
        challenge: String,

        /// The signed URL to be used to authenticate this request.
        /// See <https://indieauth.spec.indieweb.org/20201126/#example-5> for more information.
        url: Url,

        /// A state value used to prevent cross site forgery.
        ///
        /// See <https://indieauth.spec.indieweb.org/20201126/#authorization-request-li-4> for more
        /// information.
        csrf_token: String,

        /// The identifier representing the issuer of this request.
        ///
        /// See <https://indieauth.spec.indieweb.org/#authorization-response-li-3> for more
        /// information.
        issuer: ClientId,
    },

    /// Represents a successful attempt at generating an [access token][Token].
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#authorization-request-li-4=> for more
    /// information.
    AccessToken(Box<Token>),

    /// Represents a successful attempting at collecting [profile information][Profile].
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#profile-url-response> for more
    /// information.
    Profile(Box<Profile>),

    /// Represents a successful attempt at grabbing [endpoint information][EndpointDiscovery].
    ///
    /// See <https://www.rfc-editor.org/rfc/rfc8414#section-3.1> for more information.
    Discovery(Option<Box<EndpointDiscovery>>),

    /// Represents a successful attempt at revoking a token.
    ///
    /// See <https://indieauth.spec.indieweb.org/#token-revocation> for more information.
    TokenRevoked,

    /// Represents the body of a verified token.
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#access-token-verification-response> for
    /// more information.
    VerifiedToken(Box<VerifiedToken>),
}

fn from_oauth2_request(
    hrequest: oauth2::HttpRequest,
) -> Result<http::Request<crate::http::Body>, crate::Error> {
    let mut request = http::Request::builder()
        .method(http::method::Method::from_str(hrequest.method.as_ref()).unwrap())
        .uri(hrequest.url.to_string());

    for (name_opt, value) in hrequest.headers {
        if let Some(name) = name_opt {
            request = request.header(name.as_str(), value.as_bytes());
        }
    }

    let body = crate::http::Body::Bytes(hrequest.body);

    request.body(body).map_err(crate::Error::Http)
}

fn into_oauth2_response(response: http::Response<crate::http::Body>) -> oauth2::HttpResponse {
    let mut headers = oauth2::http::HeaderMap::default();

    for (name_opt, value) in response.headers().clone() {
        if let Some(name) = name_opt {
            headers.insert(
                oauth2::http::HeaderName::from_str(name.as_ref()).unwrap(),
                oauth2::http::HeaderValue::from_bytes(value.as_bytes()).unwrap(),
            );
        }
    }
    oauth2::HttpResponse {
        status_code: oauth2::http::StatusCode::from_u16(response.status().as_u16())
            .expect("invalid status code"),
        headers,
        body: response.body().clone().as_bytes().to_vec(),
    }
}

#[tracing::instrument]
fn capture_errors(e: crate::Error) -> Result<oauth2::HttpResponse, Box<crate::Error>> {
    tracing::warn!(e = format!("{:?}", e), "Failed to complete request");
    Err(Box::new(e))
}

#[tracing::instrument(skip(client))]
async fn send_using_client(
    hrequest: oauth2::HttpRequest,
    client: &impl HttpClient,
) -> Result<oauth2::HttpResponse, Box<crate::Error>> {
    client
        .send_request(from_oauth2_request(hrequest)?)
        .await
        .map(into_oauth2_response)
        .or_else(capture_errors)
}

/// Represents a client for IndieAuth.
///
/// This trait captures a lot of the common parameters needed for IndieAuth
/// (represented in [Parameters][Parameters]) and the main method for handling
/// IndieAuth logic itself via [dispatch][Client::dispatch] by way of [Request][Request]
/// and [Response][Response].
impl Client {
    /// Crafts a specialized [OAuth2 client][oauth2::Client] for working with IndieAuth systems.
    fn oauth2_client(&self) -> OAuth2Interface {
        OAuth2Interface(OAuth2Client::new(
            self.client_id.clone(),
            None,
            AuthUrl::from_url(self.metadata.authorization_endpoint()),
            Some(TokenUrl::from_url(self.metadata.token_endpoint())),
        ))
    }

    /// Handles the act of invoking different IndieAuth requests and normalizes them.
    ///
    /// With your built client, you can use this method with a [request][Request] to
    /// normalize your IndieAuth iteractions. Each request's documented with their
    /// corresponding potential [response][Response].
    #[tracing::instrument(skip(client))]
    pub async fn dispatch(
        &self,
        client: &impl HttpClient,
        request: Request,
    ) -> Result<Response, crate::Error> {
        match request {
            Request::BuildAuthorizationUrl {
                scope,
                redirect_uri,
                me,
            } => {
                tracing::trace!("Attempting to craft an authorization URL..");
                let (pkce_code_challenge, pkce_code_verifier) =
                    PkceCodeChallenge::new_random_sha256();
                let oauth_client = self.oauth2_client();
                let mut builder = oauth_client
                    .authorize_url(CsrfToken::new_random)
                    .add_scopes(scope.unwrap_or_default().as_vec().clone())
                    .add_extra_param("issuer", self.client_id.as_str())
                    .set_pkce_challenge(pkce_code_challenge.clone());

                if let Some(m) = me {
                    builder = builder.add_extra_param("me", m.to_string());
                }

                if let Some(r) = &redirect_uri {
                    builder = builder.set_redirect_uri(std::borrow::Cow::Borrowed(r));
                }

                let (auth_url, csrf_token) = builder.url();

                tracing::trace!(
                    signed_authorization_url = auth_url.to_string(),
                    "Formed authorization_url"
                );

                Ok(Response::AuthenticationUrl {
                    verifier: pkce_code_verifier.secret().clone(),
                    challenge: pkce_code_challenge.as_str().to_string(),
                    url: auth_url,
                    csrf_token: csrf_token.secret().clone(),
                    issuer: self.client_id.clone(),
                })
            }
            Request::CompleteAuthorization {
                resource,
                code,
                code_verifier,
                redirect_uri,
            } => {
                let oauth_client = self.oauth2_client();
                tracing::trace!(
                    redirect_uri = redirect_uri.as_ref().map(|s| s.to_string()),
                    "Attempting to complete the authorization flow",
                );

                oauth_client
                    .authorize(
                        client,
                        resource,
                        code,
                        redirect_uri,
                        PkceCodeVerifier::new(code_verifier),
                    )
                    .await
            }
            Request::VerifyAccessToken(token_secret) => {
                let request = http::Request::builder()
                    .method("GET")
                    .uri(self.metadata.token_introspection_endpoint().as_str())
                    .header(
                        http::header::AUTHORIZATION,
                        &format!("Bearer {}", token_secret.secret()),
                    )
                    .header(http::header::ACCEPT, crate::http::CONTENT_TYPE_JSON)
                    .body(crate::http::Body::None)
                    .map_err(crate::Error::Http)?;
                tracing::trace!(
                    "Formed request to {:?} to verify the token {:?}",
                    self.metadata.token_endpoint().to_string(),
                    token_secret
                );
                let resp = client
                    .send_request(request)
                    .await
                    .and_then(crate::http::from_json_value)
                    .and_then(|token_resp| {
                        if let AccessTokenResponse::Verified(verified_token) = token_resp {
                            Ok(Response::VerifiedToken(verified_token))
                        } else {
                            Err(crate::Error::IndieAuth(Error::InvalidToken))
                        }
                    });

                tracing::trace!("Got the response {:#?} after verifying the token.", resp);
                resp
            }
            Request::RevokeAccessToken(token_secret) => {
                let authorization_header_value = format!("Bearer {}", token_secret.secret());
                let request = http::Request::builder()
                    .method("POST")
                    .uri(self.metadata.token_endpoint().as_str())
                    .header(http::header::AUTHORIZATION, &authorization_header_value)
                    .header(http::header::ACCEPT, crate::http::CONTENT_TYPE_JSON)
                    .header(
                        http::header::CONTENT_TYPE,
                        crate::http::CONTENT_TYPE_FORM_URLENCODED,
                    )
                    .body(crate::http::Body::Bytes(
                        serde_qs::to_string(&serde_json::json!({
                            "action": "revoke",
                            "token": token_secret.secret()
                        }))
                        .map(String::into_bytes)
                        .map_err(crate::Error::Qs)?,
                    ))
                    .map_err(crate::Error::Http)?;
                client.send_request(request).await.and_then(|resp| {
                    if resp.status() == 200 {
                        tracing::trace!(
                            "The provided token was ideally revoked by the remote token endpoint."
                        );
                        Ok(Response::TokenRevoked)
                    } else {
                        tracing::warn!(
                            "The remote token endpoint rejected the provided token for revocation."
                        );
                        Err(crate::Error::IndieAuth(Error::InvalidToken))
                    }
                })
            }
        }
    }
}

#[derive(Clone, Debug)]
enum AccessTokenResponse {
    Token(Box<Token>),
    Verified(Box<VerifiedToken>),
    Invalid,
}

impl<'de> serde::Deserialize<'de> for AccessTokenResponse {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let json_obj = serde_json::Value::deserialize(deserializer)?;

        tracing::trace!("Obtained {:#?} as the token response", json_obj);

        if json_obj
            .as_object()
            .and_then(|v| v.get("active"))
            .filter(|v| *v == &serde_json::Value::Bool(false))
            .is_some()
        {
            tracing::trace!("Token was deemed to be invalid.");
            Ok(Self::Invalid)
        } else if json_obj
            .as_object()
            .and_then(|v| v.get("access_token"))
            .is_none()
        {
            tracing::trace!("Token is deemed to be a verification response representing a token.");
            serde_json::from_value(json_obj)
                .map(|t| Self::Verified(Box::new(t)))
                .map_err(|e| serde::de::Error::custom(e.to_string()))
        } else {
            tracing::trace!("Token is freshly issued.");
            serde_json::from_value(json_obj)
                .map(|t| Self::Token(Box::new(t)))
                .map_err(|e| serde::de::Error::custom(e.to_string()))
        }
    }
}

/// Describes the different kinds of authorization attempts supported by this client.
///
/// This is usually accompained with an [authorization code][oauth2::AuthorizationCode] to
/// determine what path to take when redeeming it. See
/// <https://indieauth.spec.indieweb.org/20201126/#redeeming-the-authorization-code> for more
/// information.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DesiredResourceAuthorization {
    /// Signifes redemption in exchange for a [profile][Profile].
    /// See <https://indieauth.spec.indieweb.org/20201126/#request-p-2> for more information.
    Profile,

    /// Signifies redemption in exchange for an [access token][Token].
    /// See <https://indieauth.spec.indieweb.org/20201126/#request-p-1> for more information.
    Token,
}

/// A helper object for representing an interaction with a fixed client for fetching tokens.
///
/// You can use this client if you know you'll be working with a token.
#[derive(Debug)]
pub struct Client {
    pub client_id: ClientId,
    pub metadata: EndpointDiscovery,
}

impl From<(ClientId, EndpointDiscovery)> for Client {
    fn from((client_id, metadata): (ClientId, EndpointDiscovery)) -> Self {
        Self {
            client_id,
            metadata,
        }
    }
}

/// A representation of a list of [scopes][Scope].
///
/// This structure allows for the normalization of common behaviors when working
/// with scopes.
///
/// # Examples
/// ```
/// # use indieweb::standards::indieauth::Scopes;
/// # use std::str::FromStr;
/// #
/// let scopes = Scopes::from_str("read create:read").unwrap();
///
/// assert!(scopes.has("read"),
///         "can report if it has a scope");
///
/// assert_eq!(scopes.assert("aircraft"),
///            Err(indieweb::Error::MissingScope(Scopes::from_str("aircraft").unwrap())),
///            "can report if it does not have a scope");
///
/// assert_eq!(scopes.to_string(),
///            "read create:read".to_string(),
///            "easy to pass as a string");
/// ```
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Scopes(Vec<Scope>);

impl std::fmt::Display for Scopes {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(
            &self
                .0
                .iter()
                .map(|s| s.to_string())
                .collect::<Vec<_>>()
                .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 {
    /// Reports if this has no scopes defined.
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// assert_eq!(
    ///     Ok(false),
    ///     Scopes::from_str("read")
    ///         .map(|scopes| scopes.is_empty()),
    ///     "there's at least one scope in here"
    ///     )
    /// ```
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Confirms if there's a scope matching or beginning to match with the provided value.
    ///
    /// This works with matching a direct scope (like 'create') or a scope with prefixes in
    /// its authored order (like 'update:note'). There's no real "format" to what a scope
    /// can look like, that's completely up to your implementation. For example, capitalist
    /// companies like [Slack](https://en.wikipedia.org/wiki/Slack_(software)) use a combination
    /// of colons and dots for scopes (like "files.metadata:read") and monopolies like
    /// [Google](https://en.wikipedia.org/wiki/Google) use URLs as scopes.
    ///
    /// # Examples
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// #
    /// let scopes = Scopes::from_str("read update:note").unwrap();
    /// assert!(scopes.has("read"));
    /// assert!(scopes.has("update:note"));
    /// ```
    pub fn has(&self, scope: &str) -> bool {
        self.0.iter().any(|s| s.to_string().starts_with(scope))
    }

    /// Returns the scopes as a list.
    pub fn as_vec(&self) -> &Vec<Scope> {
        &self.0
    }

    /// Confirm the presence of a provided scope.
    ///
    /// # Examples
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// #
    /// let scopes = "read create:note".parse::<Scopes>();
    ///
    /// assert_eq!(
    ///     Ok(()),
    ///     scopes.as_ref().unwrap().assert("read"),
    ///     "confirms explicit scope match without prefix");
    /// assert_eq!(
    ///     Ok(()),
    ///     scopes.as_ref().unwrap().assert("create:note"),
    ///     "confirms explicit scope match with prefix");
    /// assert_eq!(
    ///     Err(indieweb::Error::MissingScope(Scopes::from_str("airplane").unwrap())),
    ///     scopes.as_ref().unwrap().assert("create:note airplane"),
    ///     "denies provided scopes if one is not valid");

    /// ```
    pub fn assert(&self, needed_scopes: &str) -> Result<(), crate::Error> {
        tracing::debug!("Asserting that {:?} exists in {:?}", needed_scopes, self);
        let scope_list: Scopes = needed_scopes.parse().unwrap_or_else(|_| Scopes::default());

        let missing_scopes: Scopes = scope_list
            .as_vec()
            .iter()
            .filter(|expected_scope| !self.has(expected_scope))
            .cloned()
            .collect::<Vec<_>>()
            .into();

        if missing_scopes.is_empty() {
            Ok(())
        } else {
            Err(crate::Error::MissingScope(missing_scopes))
        }
    }

    /// Returns a list of scopes that are matching the list of scopes; a union of the two lists.
    ///
    /// # Examples
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    ///
    /// let wanted_scopes = Scopes::from_str("read update:note").unwrap();
    /// let scopes = Scopes::from_str("read create update:note").unwrap();
    /// assert_eq!(scopes.matching("read update:note"), wanted_scopes);
    /// ```
    pub fn matching(&self, wanted_scopes: &str) -> Self {
        wanted_scopes
            .parse::<Self>()
            .unwrap_or_default()
            .as_vec()
            .iter()
            .filter(|expected_scope| self.has(expected_scope))
            .cloned()
            .collect::<Scopes>()
    }
}

impl From<Vec<String>> for Scopes {
    fn from(scopes: Vec<String>) -> Self {
        scopes.into_iter().map(Scope::new).collect()
    }
}

impl FromIterator<Scope> for Scopes {
    fn from_iter<T: IntoIterator<Item = Scope>>(iter: T) -> Self {
        Self(iter.into_iter().collect::<Vec<_>>())
    }
}

impl From<Vec<Scope>> for Scopes {
    fn from(scopes: Vec<Scope>) -> Self {
        Self(scopes)
    }
}

impl From<Scopes> for Vec<Scope> {
    fn from(val: Scopes) -> Self {
        val.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())
    }
}

impl<'de> serde::de::Deserialize<'de> for Scopes {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        as_string_or_list::deserialize(deserializer)?
            .into_iter()
            .filter(|s: &String| !s.is_empty())
            .collect::<Vec<_>>()
            .join(" ")
            .parse()
            .map_err(serde::de::Error::custom)
    }
}

/// Represents the canonically relevant fields of a profile response.
///
/// See <https://indieauth.spec.indieweb.org/20201126/#profile-information> for more information.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Profile {
    /// Returns the display name the user selected.
    ///
    /// NOTE: In order for this field to be populated, you should request the `profile` scope.
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#profile-information-li-1>
    pub name: Option<String>,

    /// Returns the URL the user selected.
    ///
    /// NOTE: In order for this field to be populated, you should request the `profile` scope.
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#profile-information-li-1>
    pub url: Option<url::Url>,

    /// Returns the display name the user selected.
    ///
    /// NOTE: In order for this field to be populated, you should request the `profile` scope.
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#profile-information-li-1>
    pub photo: Option<url::Url>,

    /// Returns the display name the user selected.
    ///
    /// NOTE: In order for this field to be populated, you should request the `email` scope.
    ///
    /// See <https://indieauth.spec.indieweb.org/20201126/#profile-information-li-2>
    pub email: Option<String>,
}

/// Represents the different kinds of discovery results one can find when looking for IndieAuth.
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum EndpointDiscovery {
    Classic {
        #[serde(rename = "authorization_endpoint")]
        authorization: url::Url,

        #[serde(rename = "token_endpoint")]
        token: url::Url,

        #[serde(rename = "ticket_endpoint", skip_serializing_if = "Option::is_none")]
        ticket: Option<url::Url>,
    },
    Metadata(Box<EndpointMetadata>),
}

impl EndpointDiscovery {
    pub fn authorization_endpoint(&self) -> Url {
        match self {
            EndpointDiscovery::Classic { authorization, .. } => authorization.clone(),
            EndpointDiscovery::Metadata(metadata) => {
                metadata.metadata.authorization_endpoint.clone()
            }
        }
    }

    pub fn token_endpoint(&self) -> Url {
        match self {
            EndpointDiscovery::Classic { token, .. } => token.clone(),
            EndpointDiscovery::Metadata(metadata) => metadata.metadata.token_endpoint.clone(),
        }
    }

    pub fn ticket_endpoint(&self) -> Option<Url> {
        match self {
            EndpointDiscovery::Classic { ticket, .. } => ticket.clone(),
            EndpointDiscovery::Metadata(metadata) => metadata.metadata.ticket_endpoint.clone(),
        }
    }

    pub fn token_introspection_endpoint(&self) -> Url {
        match self {
            Self::Classic { token, .. } => token.clone(),
            Self::Metadata(metadata) => metadata
                .metadata
                .introspection_endpoint
                .clone()
                .unwrap_or_else(|| metadata.metadata.token_endpoint.clone()),
        }
    }
}

impl EndpointMetadata {
    pub fn valid(&self, expected_issuer: &Url) -> bool {
        &self.metadata.issuer == expected_issuer
    }
}

/// Represents a successful attempt at collecting [metadata info][Metadata].
///
/// See <https://www.rfc-editor.org/rfc/rfc8414#section-3.1> for more information.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EndpointMetadata {
    pub endpoint: url::Url,
    #[serde(flatten)]
    pub metadata: Metadata,
}

/// Represents the canonically relevant fields of an access token.
///
/// See <https://indieauth.spec.indieweb.org/20201126/#access-token-response> for more information.
#[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 client_id: Option<url::Url>,
    pub expires_in: Option<u64>,
    pub refresh_token: Option<RefreshToken>,
    pub profile: Option<Profile>,
}

impl PartialEq for Token {
    fn eq(&self, other: &Self) -> bool {
        self.me == other.me
            && self.client_id == other.client_id
            && self.access_token.secret() == other.access_token.secret()
            && self.scopes == other.scopes
            && self.expires_in == other.expires_in
            && self.refresh_token.as_ref().map(|t| t.secret())
                == other.refresh_token.as_ref().map(|t| t.secret())
            && self.profile.as_ref().map(|p| format!("{:?}", p))
                == other.profile.as_ref().map(|p| format!("{:?}", p))
    }
}

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(Duration::from_secs)
    }

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

/// Represents the canonically relevant fields of an verified token.
///
/// See <https://indieauth.spec.indieweb.org/#access-token-verification-request> for more information.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct VerifiedToken {
    pub active: bool,
    pub me: url::Url,
    pub client_id: url::Url,

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

    #[serde(rename = "exp", serialize_with = "timestamp")]
    pub expires_at: time::OffsetDateTime,

    #[serde(rename = "iat", serialize_with = "timestamp")]
    pub issued_at: time::OffsetDateTime,
}

fn timestamp<S: serde::Serializer>(
    dt: &time::OffsetDateTime,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    serializer.serialize_i64(dt.unix_timestamp())
}

fn first_url(rels: &std::collections::HashMap<String, Vec<Url>>, arg: &str) -> Option<Url> {
    rels.get(arg).and_then(|v| v.first().cloned())
}

/// Represents all of the server metadata known.
///
/// Provides a concrete form of <https://indieauth.spec.indieweb.org/#indieauth-server-metadata>
/// and <https://www.rfc-editor.org/rfc/rfc8414#section-2>
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq)]
pub struct Metadata {
    pub issuer: Url,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jwks_uri: Option<url::Url>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub code_challenge_methods_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scopes_supported: Option<Vec<Scope>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_types_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_modes_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub grant_types_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ui_locales_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revocation_endpoint_auth_methods_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revocation_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub introspection_endpoint_auth_methods_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,

    pub authorization_endpoint: Url,
    pub token_endpoint: Url,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub registration_endpoint: Option<url::Url>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ticket_endpoint: Option<Url>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revocation_endpoint: Option<url::Url>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub introspection_endpoint: Option<url::Url>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub userinfo_endpoint: Option<url::Url>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub service_documentation: Option<url::Url>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub op_policy_uri: Option<url::Url>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub op_tos_uri: Option<url::Url>,
}

impl TryFrom<(ClientId, AuthUrl, TokenUrl)> for Metadata {
    type Error = crate::Error;

    fn try_from(
        (client_id, authorization_endpoint, token_endpoint): (ClientId, AuthUrl, TokenUrl),
    ) -> Result<Self, crate::Error> {
        Ok(Self {
            issuer: client_id.parse().map_err(crate::Error::Url)?,
            authorization_endpoint: authorization_endpoint.url().clone(),
            token_endpoint: token_endpoint.url().clone(),
            ticket_endpoint: None,
            jwks_uri: None,
            registration_endpoint: None,
            scopes_supported: None,
            response_types_supported: None,
            response_modes_supported: None,
            revocation_endpoint: None,
            introspection_endpoint: None,
            grant_types_supported: None,
            userinfo_endpoint: None,
            service_documentation: None,
            op_policy_uri: None,
            op_tos_uri: None,
            ui_locales_supported: None,
            token_endpoint_auth_methods_supported: None,
            token_endpoint_auth_signing_alg_values_supported: None,
            code_challenge_methods_supported: None,
            revocation_endpoint_auth_methods_supported: None,
            revocation_endpoint_auth_signing_alg_values_supported: None,
            introspection_endpoint_auth_methods_supported: None,
            introspection_endpoint_auth_signing_alg_values_supported: None,
            userinfo_signing_alg_values_supported: None,
            userinfo_encryption_alg_values_supported: None,
            userinfo_encryption_enc_values_supported: None,
        })
    }
}

async fn find_metadata(
    client: &impl HttpClient,
    metadata_endpoint_url: &Url,
) -> Result<Option<Metadata>, crate::Error> {
    let request = http::Request::builder()
        .method("GET")
        .uri(metadata_endpoint_url.as_str())
        .header(http::header::ACCEPT, crate::http::CONTENT_TYPE_JSON)
        .body(crate::http::Body::None)
        .map_err(crate::Error::Http)?;

    client
        .send_request(request)
        .await
        .and_then(crate::http::from_json_value)
}

fn ensure_url(
    rels: &std::collections::HashMap<String, Vec<Url>>,
    arg: &str,
    not_found_err: crate::Error,
) -> Result<Url, crate::Error> {
    first_url(rels, arg).ok_or(not_found_err)
}

pub async fn ensure_discovery(
    client: &impl HttpClient,
    remote_profile_url: &Url,
    client_id: &Url,
) -> Result<EndpointMetadata, crate::Error> {
    let rels = link_rel::for_url(client, remote_profile_url, &["indieauth-metadata"]).await?;
    tracing::trace!(
        rels = format!("{:?}", rels),
        remote_profile_url = format!("{}", remote_profile_url),
        "Found the IndieAuth metadata values",
    );

    let metadata_endpoint_opt = first_url(&rels, "indieauth-metadata");

    if let Some(metadata_endpoint) = metadata_endpoint_opt {
        tracing::debug!(
            remote_profile_url = format!("{}", remote_profile_url),
            "Resolving endpoint metadata info",
        );
        find_metadata(client, &metadata_endpoint)
            .await
            .and_then(|metadata_opt| {
                metadata_opt
                    .map(|metadata| EndpointMetadata {
                        endpoint: metadata_endpoint,
                        metadata,
                    })
                    .ok_or(crate::Error::IndieAuth(Error::NoMetadataEndpointFound))
                    .and_then(|metadata| {
                        if metadata.valid(client_id) {
                            Ok(metadata)
                        } else {
                            Err(crate::Error::IndieAuth(Error::MismatchInIssuer))
                        }
                    })
            })
    } else {
        Err(crate::Error::IndieAuth(Error::NoMetadataEndpointFound))
    }
}

/// Finds discovery information about a profile URL for IndieAuth.
#[tracing::instrument(skip(client))]
pub async fn discover(
    client: &impl HttpClient,
    remote_profile_url: &Url,
) -> Result<Option<EndpointDiscovery>, crate::Error> {
    let rels = link_rel::for_url(
        client,
        remote_profile_url,
        &[
            "authorization_endpoint",
            "token_endpoint",
            "ticket_endpoint",
            "indieauth-metadata",
        ],
    )
    .await?;
    tracing::trace!(
        rels = format!("{:?}", rels),
        "Found the IndieAuth rel values",
    );

    let metadata_endpoint_opt = first_url(&rels, "indieauth-metadata");

    if let Some(metadata_endpoint) = metadata_endpoint_opt {
        tracing::debug!(
            url = remote_profile_url.as_str(),
            "Resolving endpoint metadata info",
        );
        find_metadata(client, &metadata_endpoint)
            .await
            .map(|metadata_opt| {
                metadata_opt.map(|metadata| {
                    EndpointDiscovery::Metadata(Box::new(EndpointMetadata {
                        endpoint: metadata_endpoint,
                        metadata,
                    }))
                })
            })
    } else {
        tracing::debug!(
            "Manually crafted endpoint metadata info for {:#?}",
            remote_profile_url
        );
        Ok(Some(EndpointDiscovery::Classic {
            authorization: ensure_url(
                &rels,
                "authorization_endpoint",
                crate::Error::IndieAuth(Error::NoAuthorizationEndpoint),
            )?,
            token: ensure_url(
                &rels,
                "token_endpoint",
                crate::Error::IndieAuth(Error::NoTokenEndpoint),
            )?,
            ticket: first_url(&rels, "ticket_endpoint"),
        }))
    }
}

/// Checks if the provided URL isn't a loopback address.
///
/// This is a requirement by the [IndieAuth standard][1] for profile and client URLs.
///
/// # Errors
///
/// This function will return an error if the provided client is not a valid URL.
///
/// [1]: https://indieauth.spec.indieweb.org/#identifiers
///
/// ```rust
/// use indieweb::standards::indieauth::is_loopback;
///
/// assert_eq!(is_loopback("http://example.com"),
///            Ok(false),
///            "checking remote URLs");
///
/// assert_eq!(is_loopback("http://127.0.0.1:1212"),
///            Ok(true),
///            "checking local IPv4 URLs");
///
/// assert_eq!(is_loopback("http://[::1]:1212"),
///            Ok(true),
///            "checking local IPv6 URLs");
///
/// assert_eq!(is_loopback("http://224.254.0.0:1212"),
///            Ok(true),
///            "checking multicast URLs - extra functionality");

/// ```
pub fn is_loopback(client_id: &str) -> Result<bool, crate::Error> {
    let client_id_url: Url = client_id.parse()?;

    if client_id_url.domain().is_none() {
        let addrs = client_id_url.socket_addrs(|| None)?;
        Ok(addrs
            .iter()
            .any(|ip| ip.ip().is_loopback() || ip.ip().is_multicast()))
    } else {
        Ok(false)
    }
}

mod test;
