use crate::database::{settings::Setting, Storage};
use axum::{
    extract::{Form, FromRef, Json, Query, State},
    http::StatusCode,
    response::{Html, IntoResponse, Response},
};
#[cfg_attr(not(feature = "webauthn"), allow(unused_imports))]
use axum_extra::extract::{
    cookie::{Cookie, CookieJar},
    Host,
};
use axum_extra::{
    headers::{authorization::Bearer, Authorization, ContentType, HeaderMapExt},
    TypedHeader,
};
use futures::FutureExt;
use kittybox_indieauth::{
    AuthorizationRequest, AuthorizationResponse, ClientMetadata, Error, ErrorKind, GrantRequest,
    GrantResponse, GrantType, IntrospectionEndpointAuthMethod, Metadata, PKCEMethod, Profile,
    ProfileUrl, ResponseType, RevocationEndpointAuthMethod, Scope, Scopes, TokenData,
    TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest,
};
use microformats::types::Class;
use serde::Deserialize;
use std::marker::PhantomData;
use std::str::FromStr;
use tracing::error;

pub mod backend;
#[cfg(feature = "webauthn")]
mod webauthn;
use backend::AuthBackend;

const ACCESS_TOKEN_VALIDITY: u64 = 7 * 24 * 60 * 60; // 7 days
const REFRESH_TOKEN_VALIDITY: u64 = ACCESS_TOKEN_VALIDITY / 7 * 60; // 60 days
/// Internal scope for accessing the token introspection endpoint.
const KITTYBOX_TOKEN_STATUS: &str = "kittybox:token_status";

pub(crate) struct User<A: AuthBackend>(pub(crate) TokenData, pub(crate) PhantomData<A>);
impl<A: AuthBackend> std::fmt::Debug for User<A> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("User").field(&self.0).finish()
    }
}
impl<A: AuthBackend> std::ops::Deref for User<A> {
    type Target = TokenData;

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

pub enum IndieAuthResourceError {
    InvalidRequest,
    Unauthorized,
    InvalidToken,
}
impl axum::response::IntoResponse for IndieAuthResourceError {
    fn into_response(self) -> axum::response::Response {
        use IndieAuthResourceError::*;

        match self {
            Unauthorized => {
                (StatusCode::UNAUTHORIZED, [("WWW-Authenticate", "Bearer")]).into_response()
            }
            InvalidRequest => (
                StatusCode::BAD_REQUEST,
                Json(&serde_json::json!({"error": "invalid_request"})),
            )
                .into_response(),
            InvalidToken => (
                StatusCode::UNAUTHORIZED,
                [("WWW-Authenticate", "Bearer, error=\"invalid_token\"")],
                Json(&serde_json::json!({"error": "not_authorized"})),
            )
                .into_response(),
        }
    }
}

impl<A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static>
    axum::extract::OptionalFromRequestParts<St> for User<A>
{
    type Rejection = <Self as axum::extract::FromRequestParts<St>>::Rejection;

    async fn from_request_parts(
        req: &mut axum::http::request::Parts,
        state: &St,
    ) -> Result<Option<Self>, Self::Rejection> {
        let res =
            <Self as axum::extract::FromRequestParts<St>>::from_request_parts(req, state).await;

        match res {
            Ok(user) => Ok(Some(user)),
            Err(IndieAuthResourceError::Unauthorized) => Ok(None),
            Err(err) => Err(err),
        }
    }
}

impl<A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static>
    axum::extract::FromRequestParts<St> for User<A>
{
    type Rejection = IndieAuthResourceError;

    async fn from_request_parts(
        req: &mut axum::http::request::Parts,
        state: &St,
    ) -> Result<Self, Self::Rejection> {
        let TypedHeader(Authorization(token)) =
            TypedHeader::<Authorization<Bearer>>::from_request_parts(req, state)
                .await
                .map_err(|_| IndieAuthResourceError::Unauthorized)?;

        let auth = A::from_ref(state);

        let Host(host) = Host::from_request_parts(req, state)
            .await
            .map_err(|_| IndieAuthResourceError::InvalidRequest)?;

        auth.get_token(&format!("https://{host}/").parse().unwrap(), token.token())
            .await
            .unwrap()
            .ok_or(IndieAuthResourceError::InvalidToken)
            .map(|t| User(t, PhantomData))
    }
}

pub async fn metadata(Host(host): Host) -> Metadata {
    let issuer: url::Url = format!("https://{}/", host).parse().unwrap();

    let indieauth: url::Url = issuer.join("/.kittybox/indieauth/").unwrap();
    Metadata {
        issuer,
        authorization_endpoint: indieauth.join("auth").unwrap(),
        token_endpoint: indieauth.join("token").unwrap(),
        introspection_endpoint: indieauth.join("token_status").unwrap(),
        introspection_endpoint_auth_methods_supported: Some(vec![
            IntrospectionEndpointAuthMethod::Bearer,
        ]),
        revocation_endpoint: Some(indieauth.join("revoke_token").unwrap()),
        revocation_endpoint_auth_methods_supported: Some(vec![RevocationEndpointAuthMethod::None]),
        scopes_supported: Some(vec![
            Scope::Create,
            Scope::Update,
            Scope::Delete,
            Scope::Media,
            Scope::Profile,
        ]),
        response_types_supported: Some(vec![ResponseType::Code]),
        grant_types_supported: Some(vec![GrantType::AuthorizationCode, GrantType::RefreshToken]),
        service_documentation: None,
        code_challenge_methods_supported: vec![PKCEMethod::S256],
        authorization_response_iss_parameter_supported: Some(true),
        userinfo_endpoint: Some(indieauth.join("userinfo").unwrap()),
        client_id_metadata_document_supported: true,
    }
}

async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    Query(mut request): Query<AuthorizationRequest>,
    State(db): State<D>,
    State(http): State<reqwest_middleware::ClientWithMiddleware>,
    State(auth): State<A>,
) -> Response {
    let me: url::Url = format!("https://{host}/").parse().unwrap();
    // XXX: attempt fetching OAuth application metadata
    let h_app: ClientMetadata = if request.client_id.domain().unwrap() == "localhost"
        && me.domain().unwrap() != "localhost"
    {
        // If client is localhost, but we aren't localhost, generate synthetic metadata.
        tracing::warn!("Client is localhost, not fetching metadata");
        let mut metadata =
            ClientMetadata::new(request.client_id.clone(), request.client_id.clone()).unwrap();

        metadata.client_name = Some("Your locally hosted app".to_string());

        metadata
    } else {
        tracing::debug!("Sending request to {} to fetch metadata", request.client_id);
        let metadata_request = http
            .get(request.client_id.clone())
            .header("Accept", "application/json, text/html");
        match metadata_request.send().await.and_then(|res| {
            res.error_for_status()
                .map_err(reqwest_middleware::Error::Reqwest)
        }) {
            Ok(response)
                if response
                    .headers()
                    .typed_get::<ContentType>()
                    .to_owned()
                    .map(mime::Mime::from)
                    .map(|m| m.type_() == "text" && m.subtype() == "html")
                    .unwrap_or(false) =>
            {
                let url = response.url().clone();
                let text = response.text().await.unwrap();
                tracing::debug!("Received {} bytes in response", text.len());
                match microformats::from_html(&text, url) {
                    Ok(mf2) => {
                        if let Some(relation) = mf2.rels.items.get(&request.redirect_uri) {
                            if !relation.rels.iter().any(|i| i == "redirect_uri") {
                                return (
                                    StatusCode::BAD_REQUEST,
                                    [("Content-Type", "text/plain")],
                                    "The redirect_uri provided was declared as \
                                         something other than redirect_uri.",
                                )
                                    .into_response();
                            }
                        } else if request.redirect_uri.origin() != request.client_id.origin() {
                            return (
                                StatusCode::BAD_REQUEST,
                                [("Content-Type", "text/plain")],
                                "The redirect_uri didn't match the origin \
                                     and wasn't explicitly allowed. You were being tricked.",
                            )
                                .into_response();
                        }
                        // Should we attempt to create synthetic metadata from an h-card?
                        //
                        // This would increase compatibility with personal websites.
                        if let Some(app) = mf2
                            .items
                            .iter()
                            .find(|&i| {
                                i.r#type.iter().any(|i| {
                                    *i == Class::from_str("h-app").unwrap()
                                        || *i == Class::from_str("h-x-app").unwrap()
                                })
                            })
                            .cloned()
                        {
                            // Create a synthetic metadata document. Be forgiving.
                            let mut metadata = ClientMetadata::new(
                                request.client_id.clone(),
                                app.properties
                                    .get("url")
                                    .and_then(|v| v.first())
                                    .and_then(|i| match i {
                                        microformats::types::PropertyValue::Url(url) => {
                                            Some(url.clone())
                                        }
                                        _ => None,
                                    })
                                    .unwrap_or_else(|| request.client_id.clone()),
                            )
                            .unwrap();

                            metadata.client_name = app
                                .properties
                                .get("name")
                                .and_then(|v| v.first())
                                .and_then(|i| match i {
                                    microformats::types::PropertyValue::Plain(name) => {
                                        Some(name.to_owned())
                                    }
                                    _ => None,
                                });

                            metadata.redirect_uris = mf2.rels.by_rels().remove("redirect_uri");

                            metadata
                        } else {
                            return (
                                StatusCode::BAD_REQUEST,
                                [("Content-Type", "text/plain")],
                                "No h-app or JSON application metadata found.",
                            )
                                .into_response();
                        }
                    }
                    Err(err) => {
                        tracing::error!("Error parsing application metadata: {}", err);
                        return (
                            StatusCode::BAD_REQUEST,
                            [("Content-Type", "text/plain")],
                            "Parsing h-app metadata failed.",
                        )
                            .into_response();
                    }
                }
            }
            Ok(response) => match response.json::<ClientMetadata>().await {
                Ok(client_metadata) => client_metadata,
                Err(err) => {
                    tracing::error!("Error parsing JSON application metadata: {}", err);
                    return (
                        StatusCode::BAD_REQUEST,
                        [("Content-Type", "text/plain")],
                        format!("Parsing OAuth2 JSON app metadata failed: {}", err),
                    )
                        .into_response();
                }
            },
            Err(err) => {
                tracing::error!("Error fetching application metadata: {}", err);
                return (
                    StatusCode::BAD_REQUEST,
                    [("Content-Type", "text/plain")],
                    format!("Fetching app metadata failed: {}", err),
                )
                    .into_response();
            }
        }
    };

    tracing::debug!("Application metadata: {:#?}", h_app);

    // Sanity check: some older applications don't ask for scopes when they're supposed to.
    //
    // Give them the profile scope at least?
    if request
        .scope
        .as_ref()
        .map(|scope: &Scopes| scope.is_empty())
        .unwrap_or(true)
    {
        request.scope.replace(Scopes::new(vec![Scope::Profile]));
    }
    let (blog_name, theme, channels) = tokio::join!(
        db.get_setting::<crate::database::settings::SiteName>(&me)
            .map(Result::unwrap_or_default),
        db.get_setting::<crate::database::settings::Theme>(&me)
            .map(Result::unwrap_or_default),
        db.get_channels(&me).map(|i| i.unwrap_or_default())
    );

    Html(
        kittybox_frontend_renderer::Template {
            title: "Confirm sign-in via IndieAuth",
            blog_name: &blog_name.as_ref(),
            feeds: channels,
            theme: theme.into_inner(),
            user: None,
            content: kittybox_frontend_renderer::AuthorizationRequestPage {
                request,
                credentials: auth.list_user_credential_types(&me).await.unwrap(),
                user: db.get_post(me.as_str()).await.unwrap().unwrap(),
                app: h_app,
            }
            .to_string(),
        }
        .to_string(),
    )
    .into_response()
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Credential {
    Password(String),
    #[cfg(feature = "webauthn")]
    WebAuthn(::webauthn::prelude::PublicKeyCredential),
}

// The IndieAuth standard doesn't prescribe a format for confirming
// authorizations, since that's supposed to be internal to the
// server. We are merely passing through the authorization request,
// so the endpoint is stateless, plus a credential.
//
// CSRF protection is supposed to be taken care of by the IndieAuth
// data we are passing through.
#[derive(Deserialize, Debug)]
struct AuthorizationConfirmation {
    authorization_method: Credential,
    request: AuthorizationRequest,
}

#[tracing::instrument(skip(auth, credential))]
async fn verify_credential<A: AuthBackend>(
    auth: &A,
    website: &url::Url,
    credential: Credential,
    #[cfg_attr(not(feature = "webauthn"), allow(unused_variables))] challenge_id: Option<&str>,
) -> std::io::Result<bool> {
    match credential {
        Credential::Password(password) => auth.verify_password(website, password).await,
        #[cfg(feature = "webauthn")]
        Credential::WebAuthn(credential) => {
            webauthn::verify(auth, website, credential, challenge_id.unwrap()).await
        }
    }
}

#[tracing::instrument(skip(backend, confirmation))]
async fn authorization_endpoint_confirm<A: AuthBackend>(
    Host(host): Host,
    State(backend): State<A>,
    cookies: CookieJar,
    Json(confirmation): Json<AuthorizationConfirmation>,
) -> Response {
    tracing::debug!("Received authorization confirmation from user");
    #[cfg(feature = "webauthn")]
    let challenge_id = cookies
        .get(webauthn::CHALLENGE_ID_COOKIE)
        .map(|cookie| cookie.value());
    #[cfg(not(feature = "webauthn"))]
    let challenge_id = None;

    let website = format!("https://{}/", host).parse().unwrap();
    let AuthorizationConfirmation {
        authorization_method: credential,
        request: mut auth,
    } = confirmation;

    match verify_credential(&backend, &website, credential, challenge_id).await {
        Ok(verified) => {
            if !verified {
                error!("User failed verification, bailing out.");
                return StatusCode::UNAUTHORIZED.into_response();
            }
        }
        Err(err) => {
            error!("Error while verifying credential: {}", err);
            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
        }
    }
    // Insert the correct `me` value into the request
    //
    // From this point, the `me` value that hits the backend is
    // guaranteed to be authoritative and correct, and can be safely
    // unwrapped.
    auth.me = Some(website.clone());
    // Cloning these two values, because we can't destructure
    // the AuthorizationRequest - we need it for the code
    let state = auth.state.clone();
    let redirect_uri = auth.redirect_uri.clone();

    let code = match backend.create_code(auth).await {
        Ok(code) => code,
        Err(err) => {
            error!("Error creating authorization code: {}", err);
            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
        }
    };

    let location = {
        let mut uri = redirect_uri;
        uri.set_query(Some(
            &serde_urlencoded::to_string(AuthorizationResponse {
                code,
                state,
                iss: website,
            })
            .unwrap(),
        ));

        uri
    };

    // DO NOT SET `StatusCode::FOUND` here! `fetch()` cannot read from
    // redirects, it can only follow them or choose to receive an
    // opaque response instead that is completely useless
    (
        StatusCode::NO_CONTENT,
        [("Location", location.as_str())],
        #[cfg(feature = "webauthn")]
        cookies.remove(Cookie::from(webauthn::CHALLENGE_ID_COOKIE)),
    )
        .into_response()
}

#[tracing::instrument(skip(backend, db))]
async fn authorization_endpoint_post<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    State(backend): State<A>,
    State(db): State<D>,
    Form(grant): Form<GrantRequest>,
) -> Response {
    tracing::debug!("Processing grant...");
    match grant {
        GrantRequest::AuthorizationCode {
            code,
            client_id,
            redirect_uri,
            code_verifier,
        } => {
            let request: AuthorizationRequest = match backend.get_code(&code).await {
                Ok(Some(request)) => request,
                Ok(None) => {
                    return Error {
                        kind: ErrorKind::InvalidGrant,
                        msg: Some("The provided authorization code is invalid.".to_string()),
                        error_uri: None,
                    }
                    .into_response()
                }
                Err(err) => {
                    tracing::error!("Error retrieving auth request: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };
            if client_id != request.client_id {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization code isn't yours.".to_string()),
                    error_uri: None,
                }
                .into_response();
            }
            if redirect_uri != request.redirect_uri {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some(
                        "This redirect_uri doesn't match the one the code has been sent to."
                            .to_string(),
                    ),
                    error_uri: None,
                }
                .into_response();
            }
            if !request.code_challenge.verify(code_verifier) {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("The PKCE challenge failed.".to_string()),
                    // are RFCs considered human-readable? 😝
                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6"
                        .parse()
                        .ok(),
                }
                .into_response();
            }
            let me: url::Url = format!("https://{}/", host).parse().unwrap();
            if request.me.unwrap() != me {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization endpoint does not serve this user.".to_string()),
                    error_uri: None,
                }
                .into_response();
            }
            let profile = if request
                .scope
                .as_ref()
                .map(|s| s.has(&Scope::Profile))
                .unwrap_or_default()
            {
                match get_profile(
                    db,
                    me.as_str(),
                    request
                        .scope
                        .as_ref()
                        .map(|s| s.has(&Scope::Email))
                        .unwrap_or_default(),
                )
                .await
                {
                    Ok(profile) => {
                        tracing::debug!("Retrieved profile: {:?}", profile);
                        profile
                    }
                    Err(err) => {
                        tracing::error!("Error retrieving profile from database: {}", err);

                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                    }
                }
            } else {
                None
            };

            GrantResponse::ProfileUrl(ProfileUrl { me, profile }).into_response()
        }
        _ => Error {
            kind: ErrorKind::InvalidGrant,
            msg: Some("The provided grant_type is unusable on this endpoint.".to_string()),
            error_uri: "https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code"
                .parse()
                .ok(),
        }
        .into_response(),
    }
}

#[tracing::instrument(skip(backend, db))]
async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    State(backend): State<A>,
    State(db): State<D>,
    Form(grant): Form<GrantRequest>,
) -> Response {
    #[inline]
    fn prepare_access_token(me: url::Url, client_id: url::Url, scope: Scopes) -> TokenData {
        TokenData {
            me,
            client_id,
            scope,
            exp: (std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                + std::time::Duration::from_secs(ACCESS_TOKEN_VALIDITY))
            .as_secs()
            .into(),
            iat: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs()
                .into(),
        }
    }

    #[inline]
    fn prepare_refresh_token(me: url::Url, client_id: url::Url, scope: Scopes) -> TokenData {
        TokenData {
            me,
            client_id,
            scope,
            exp: (std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                + std::time::Duration::from_secs(REFRESH_TOKEN_VALIDITY))
            .as_secs()
            .into(),
            iat: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs()
                .into(),
        }
    }

    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    match grant {
        GrantRequest::AuthorizationCode {
            code,
            client_id,
            redirect_uri,
            code_verifier,
        } => {
            let request: AuthorizationRequest = match backend.get_code(&code).await {
                Ok(Some(request)) => request,
                Ok(None) => {
                    return Error {
                        kind: ErrorKind::InvalidGrant,
                        msg: Some("The provided authorization code is invalid.".to_string()),
                        error_uri: None,
                    }
                    .into_response()
                }
                Err(err) => {
                    tracing::error!("Error retrieving auth request: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };

            tracing::debug!("Retrieved authorization request: {:?}", request);

            let scope = if let Some(scope) = request.scope {
                scope
            } else {
                return Error {
                    kind: ErrorKind::InvalidScope,
                    msg: Some("Tokens cannot be issued if no scopes are requested.".to_string()),
                    error_uri: "https://indieauth.spec.indieweb.org/#access-token-response"
                        .parse()
                        .ok(),
                }
                .into_response();
            };
            if client_id != request.client_id {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization code isn't yours.".to_string()),
                    error_uri: None,
                }
                .into_response();
            }
            if redirect_uri != request.redirect_uri {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some(
                        "This redirect_uri doesn't match the one the code has been sent to."
                            .to_string(),
                    ),
                    error_uri: None,
                }
                .into_response();
            }
            if !request.code_challenge.verify(code_verifier) {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("The PKCE challenge failed.".to_string()),
                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6"
                        .parse()
                        .ok(),
                }
                .into_response();
            }

            // Note: we can trust the `request.me` value, since we set
            // it earlier before generating the authorization code
            if request.me.unwrap() != me {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization endpoint does not serve this user.".to_string()),
                    error_uri: None,
                }
                .into_response();
            }

            let profile = if dbg!(scope.has(&Scope::Profile)) {
                match get_profile(db, me.as_str(), scope.has(&Scope::Email)).await {
                    Ok(profile) => dbg!(profile),
                    Err(err) => {
                        tracing::error!("Error retrieving profile from database: {}", err);

                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                    }
                }
            } else {
                None
            };

            let access_token = match backend
                .create_token(prepare_access_token(
                    me.clone(),
                    client_id.clone(),
                    scope.clone(),
                ))
                .await
            {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating access token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };
            // TODO: only create refresh token if user allows it
            let refresh_token = match backend
                .create_refresh_token(prepare_refresh_token(me.clone(), client_id, scope.clone()))
                .await
            {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating refresh token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };

            GrantResponse::AccessToken {
                me,
                profile,
                access_token,
                token_type: kittybox_indieauth::TokenType::Bearer,
                scope: Some(scope),
                expires_in: Some(ACCESS_TOKEN_VALIDITY),
                refresh_token: Some(refresh_token),
                state: None,
            }
            .into_response()
        }
        GrantRequest::RefreshToken {
            refresh_token,
            client_id,
            scope,
        } => {
            let data = match backend.get_refresh_token(&me, &refresh_token).await {
                Ok(Some(token)) => token,
                Ok(None) => {
                    return Error {
                        kind: ErrorKind::InvalidGrant,
                        msg: Some("This refresh token is not valid.".to_string()),
                        error_uri: None,
                    }
                    .into_response()
                }
                Err(err) => {
                    tracing::error!("Error retrieving refresh token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };

            if data.client_id != client_id {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This refresh token is not yours.".to_string()),
                    error_uri: None,
                }
                .into_response();
            }

            let scope = if let Some(scope) = scope {
                if !data.scope.has_all(scope.as_ref()) {
                    return Error {
                        kind: ErrorKind::InvalidScope,
                        msg: Some(
                            "You can't request additional scopes through the refresh token grant."
                                .to_string(),
                        ),
                        error_uri: None,
                    }
                    .into_response();
                }

                scope
            } else {
                // Note: check skipped because of redundancy (comparing a scope list with itself)
                data.scope
            };

            let profile = if scope.has(&Scope::Profile) {
                match get_profile(db, data.me.as_str(), scope.has(&Scope::Email)).await {
                    Ok(profile) => profile,
                    Err(err) => {
                        tracing::error!("Error retrieving profile from database: {}", err);

                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                    }
                }
            } else {
                None
            };

            let access_token = match backend
                .create_token(prepare_access_token(
                    data.me.clone(),
                    client_id.clone(),
                    scope.clone(),
                ))
                .await
            {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating access token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };

            let old_refresh_token = refresh_token;
            let refresh_token = match backend
                .create_refresh_token(prepare_refresh_token(
                    data.me.clone(),
                    client_id,
                    scope.clone(),
                ))
                .await
            {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating refresh token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };
            if let Err(err) = backend.revoke_refresh_token(&me, &old_refresh_token).await {
                tracing::error!("Error revoking refresh token: {}", err);
                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
            }

            GrantResponse::AccessToken {
                me: data.me,
                profile,
                access_token,
                token_type: kittybox_indieauth::TokenType::Bearer,
                scope: Some(scope),
                expires_in: Some(ACCESS_TOKEN_VALIDITY),
                refresh_token: Some(refresh_token),
                state: None,
            }
            .into_response()
        }
    }
}

#[tracing::instrument(skip(backend, token_request))]
async fn introspection_endpoint_post<A: AuthBackend>(
    Host(host): Host,
    TypedHeader(Authorization(auth_token)): TypedHeader<Authorization<Bearer>>,
    State(backend): State<A>,
    Form(token_request): Form<TokenIntrospectionRequest>,
) -> Response {
    use serde_json::json;

    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    // Check authentication first
    match backend.get_token(&me, auth_token.token()).await {
        Ok(Some(token)) => {
            if !token.scope.has(&Scope::custom(KITTYBOX_TOKEN_STATUS)) {
                return (
                    StatusCode::UNAUTHORIZED,
                    Json(json!({
                        "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
                    })),
                )
                    .into_response();
            }
        }
        Ok(None) => {
            return (
                StatusCode::UNAUTHORIZED,
                Json(json!({
                    "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
                })),
            )
                .into_response()
        }
        Err(err) => {
            tracing::error!("Error retrieving token data for introspection: {}", err);
            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
        }
    }
    let response: TokenIntrospectionResponse =
        match backend.get_token(&me, &token_request.token).await {
            Ok(maybe_data) => maybe_data.into(),
            Err(err) => {
                tracing::error!("Error retrieving token data: {}", err);
                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
            }
        };

    response.into_response()
}

async fn revocation_endpoint_post<A: AuthBackend>(
    Host(host): Host,
    State(backend): State<A>,
    Form(revocation): Form<TokenRevocationRequest>,
) -> impl IntoResponse {
    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    if let Err(err) = tokio::try_join!(
        backend.revoke_token(&me, &revocation.token),
        backend.revoke_refresh_token(&me, &revocation.token)
    ) {
        tracing::error!("Error revoking token: {}", err);

        StatusCode::INTERNAL_SERVER_ERROR
    } else {
        StatusCode::OK
    }
}

#[tracing::instrument(skip(db))]
async fn get_profile<D: Storage + 'static>(
    db: D,
    url: &str,
    email: bool,
) -> crate::database::Result<Option<Profile>> {
    fn get_first(v: serde_json::Value) -> Option<String> {
        match v {
            serde_json::Value::Array(mut a) => {
                a.truncate(1);
                match a.pop() {
                    Some(serde_json::Value::String(s)) => Some(s),
                    Some(serde_json::Value::Object(mut o)) => o.remove("value").and_then(get_first),
                    _ => None,
                }
            }
            _ => None,
        }
    }

    Ok(db.get_post(url).await?.map(|mut mf2| {
        // Ruthlessly manually destructure the MF2 document to save memory
        let mut properties = match mf2.as_object_mut().unwrap().remove("properties") {
            Some(serde_json::Value::Object(props)) => props,
            _ => unreachable!(),
        };
        drop(mf2);
        let name = properties.remove("name").and_then(get_first);
        let url = properties
            .remove("uid")
            .and_then(get_first)
            .and_then(|u| u.parse().ok());
        let photo = properties
            .remove("photo")
            .and_then(get_first)
            .and_then(|u| u.parse().ok());
        let email = properties.remove("name").and_then(get_first);

        Profile {
            name,
            url,
            photo,
            email,
        }
    }))
}

async fn userinfo_endpoint_get<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    TypedHeader(Authorization(auth_token)): TypedHeader<Authorization<Bearer>>,
    State(backend): State<A>,
    State(db): State<D>,
) -> Response {
    use serde_json::json;

    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    match backend.get_token(&me, auth_token.token()).await {
        Ok(Some(token)) => {
            if token.expired() {
                return (
                    StatusCode::UNAUTHORIZED,
                    Json(json!({
                        "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
                    })),
                )
                    .into_response();
            }
            if !token.scope.has(&Scope::Profile) {
                return (
                    StatusCode::UNAUTHORIZED,
                    Json(json!({
                        "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
                    })),
                )
                    .into_response();
            }

            match get_profile(db, me.as_str(), token.scope.has(&Scope::Email)).await {
                Ok(Some(profile)) => profile.into_response(),
                Ok(None) => Json(json!({
                    // We do this because ResourceErrorKind is IndieAuth errors only
                    "error": "invalid_request"
                }))
                .into_response(),
                Err(err) => {
                    tracing::error!("Error retrieving profile from database: {}", err);

                    StatusCode::INTERNAL_SERVER_ERROR.into_response()
                }
            }
        }
        Ok(None) => Json(json!({
            "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
        }))
        .into_response(),
        Err(err) => {
            tracing::error!("Error reading token: {}", err);

            StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
}

pub fn router<St, A, S>() -> axum::Router<St>
where
    S: Storage + FromRef<St> + 'static,
    A: AuthBackend + FromRef<St>,
    reqwest_middleware::ClientWithMiddleware: FromRef<St>,
    St: Clone + Send + Sync + 'static,
{
    use axum::routing::{get, post, Router};

    Router::new()
        .nest(
            "/.kittybox/indieauth",
            Router::new()
                .route("/metadata", get(metadata))
                .route(
                    "/auth",
                    get(authorization_endpoint_get::<A, S>)
                        .post(authorization_endpoint_post::<A, S>),
                )
                .route("/auth/confirm", post(authorization_endpoint_confirm::<A>))
                .route("/token", post(token_endpoint_post::<A, S>))
                .route("/token_status", post(introspection_endpoint_post::<A>))
                .route("/revoke_token", post(revocation_endpoint_post::<A>))
                .route("/userinfo", get(userinfo_endpoint_get::<A, S>))
                .route(
                    "/webauthn/pre_register",
                    get(
                        #[cfg(feature = "webauthn")]
                        webauthn::webauthn_pre_register::<A, S>,
                        #[cfg(not(feature = "webauthn"))]
                        || std::future::ready(axum::http::StatusCode::NOT_FOUND),
                    ),
                )
                .layer(
                    tower_http::cors::CorsLayer::new()
                        .allow_methods([axum::http::Method::GET, axum::http::Method::POST])
                        .allow_origin(tower_http::cors::Any),
                ),
        )
        .route(
            "/.well-known/oauth-authorization-server",
            get(|| {
                std::future::ready(
                    (
                        StatusCode::FOUND,
                        [("Location", "/.kittybox/indieauth/metadata")],
                    )
                        .into_response(),
                )
            }),
        )
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_deserialize_authorization_confirmation() {
        use super::{AuthorizationConfirmation, Credential};

        let confirmation = serde_json::from_str::<AuthorizationConfirmation>(
            r#"{
            "request":{
                "response_type": "code",
                "client_id": "https://quill.p3k.io/",
                "redirect_uri": "https://quill.p3k.io/",
                "state": "10101010",
                "code_challenge": "awooooooooooo",
                "code_challenge_method": "S256",
                "scope": "create+media"
            },
            "authorization_method": "swordfish"
        }"#,
        )
        .unwrap();

        match confirmation.authorization_method {
            Credential::Password(password) => assert_eq!(password.as_str(), "swordfish"),
            #[allow(unreachable_patterns)]
            other => panic!("Incorrect credential: {:?}", other),
        }
        assert_eq!(confirmation.request.state.as_ref(), "10101010");
    }
}