use std::marker::PhantomData;

use tracing::error;
use serde::Deserialize;
use axum::{
    extract::{Query, Json, Host, Form},
    response::{Html, IntoResponse, Response},
    http::StatusCode, TypedHeader, headers::{Authorization, authorization::Bearer},
    Extension
};
#[cfg_attr(not(feature = "webauthn"), allow(unused_imports))]
use axum_extra::extract::cookie::{CookieJar, Cookie};
use crate::database::Storage;
use kittybox_indieauth::{
    Metadata, IntrospectionEndpointAuthMethod, RevocationEndpointAuthMethod,
    Scope, Scopes, PKCEMethod, Error, ErrorKind, ResponseType,
    AuthorizationRequest, AuthorizationResponse,
    GrantType, GrantRequest, GrantResponse, Profile,
    TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest, TokenData
};
use std::str::FromStr;
use std::ops::Deref;

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": "unauthorized"}))
            ).into_response()
        }
    }
}

#[async_trait::async_trait]
impl <B: Send, A: AuthBackend> axum::extract::FromRequest<B> for User<A> {
    type Rejection = IndieAuthResourceError;

    async fn from_request(req: &mut axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
        let TypedHeader(Authorization(token)) =
            TypedHeader::<Authorization<Bearer>>::from_request(req)
            .await
            .map_err(|_| IndieAuthResourceError::Unauthorized)?;

        let axum::Extension(auth) = axum::Extension::<A>::from_request(req)
            .await
            .unwrap();

        let Host(host) = Host::from_request(req)
            .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!(
        "{}://{}/",
        if cfg!(debug_assertions) {
            "http"
        } else {
            "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()),
    }
}

async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    Query(request): Query<AuthorizationRequest>,
    Extension(db): Extension<D>,
    Extension(http): Extension<reqwest::Client>,
    Extension(auth): Extension<A>
) -> Response {
    let me = format!("https://{}/", host).parse().unwrap();
    let h_app = {
        match http.get(request.client_id.clone()).send().await {
            Ok(response) => {
                let url = response.url().clone();
                let text = response.text().await.unwrap();
                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()
                        }

                        mf2.items.iter()
                            .cloned()
                            .find(|i| (**i).borrow().r#type.iter()
                                  .any(|i| *i == microformats::types::Class::from_str("h-app").unwrap()
                                       || *i == microformats::types::Class::from_str("h-x-app").unwrap()))
                            .map(|i| serde_json::to_value(i.borrow().deref()).unwrap())
                    },
                    Err(err) => {
                        tracing::error!("Error parsing application metadata: {}", err);
                        return (StatusCode::BAD_REQUEST,
                                [("Content-Type", "text/plain")],
                                "Parsing application metadata failed.").into_response()
                    }
                }
            },
            Err(err) => {
                tracing::error!("Error fetching application metadata: {}", err);
                return (StatusCode::INTERNAL_SERVER_ERROR,
                        [("Content-Type", "text/plain")],
                        "Fetching application metadata failed.").into_response()
            }
        }
    };

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

    Html(kittybox_frontend_renderer::Template {
        title: "Confirm sign-in via IndieAuth",
        blog_name: "Kittybox",
        feeds: vec![],
        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)
}

#[derive(Deserialize, Debug)]
struct AuthorizationConfirmation {
    authorization_method: Credential,
    request: AuthorizationRequest
}

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,
    Json(confirmation): Json<AuthorizationConfirmation>,
    Extension(backend): Extension<A>,
    cookies: CookieJar,
) -> 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::named(webauthn::CHALLENGE_ID_COOKIE))
    )
        .into_response()
}

async fn authorization_endpoint_post<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    Form(grant): Form<GrantRequest>,
    Extension(backend): Extension<A>,
    Extension(db): Extension<D>
) -> Response {
    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 dbg!(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) => dbg!(profile),
                    Err(err) => {
                        tracing::error!("Error retrieving profile from database: {}", err);

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

            GrantResponse::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,
    Form(grant): Form<GrantRequest>,
    Extension(backend): Extension<A>,
    Extension(db): Extension<D>
) -> 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)
            }.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)
            }.into_response()
        }
    }
}

async fn introspection_endpoint_post<A: AuthBackend>(
    Host(host): Host,
    Form(token_request): Form<TokenIntrospectionRequest>,
    TypedHeader(Authorization(auth_token)): TypedHeader<Authorization<Bearer>>,
    Extension(backend): Extension<A>
) -> 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,
    Form(revocation): Form<TokenRevocationRequest>,
    Extension(backend): Extension<A>
) -> 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
    }
}

async fn get_profile<D: Storage + 'static>(
    db: D,
    url: &str,
    email: bool
) -> crate::database::Result<Option<Profile>> {
    Ok(db.get_post(url).await?.map(|mut mf2| {
        // Ruthlessly manually destructure the MF2 document to save memory
        let name = match mf2["properties"]["name"][0].take() {
            serde_json::Value::String(s) => Some(s),
            _ => None
        };
        let url = match mf2["properties"]["uid"][0].take() {
            serde_json::Value::String(s) => s.parse().ok(),
            _ => None
        };
        let photo = match mf2["properties"]["photo"][0].take() {
            serde_json::Value::String(s) => s.parse().ok(),
            _ => None
        };
        let email = if email {
            match mf2["properties"]["email"][0].take() {
                serde_json::Value::String(s) => Some(s),
                _ => None
            }
        } else {
            None
        };

        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>>,
    Extension(backend): Extension<A>,
    Extension(db): Extension<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<A: AuthBackend, D: Storage + 'static>(backend: A, db: D, http: reqwest::Client) -> axum::Router {
    use axum::routing::{Router, get, post};

    Router::new()
        .nest(
            "/.kittybox/indieauth",
            Router::new()
                .route("/metadata",
                       get(metadata))
                .route(
                    "/auth",
                    get(authorization_endpoint_get::<A, D>)
                        .post(authorization_endpoint_post::<A, D>))
                .route(
                    "/auth/confirm",
                    post(authorization_endpoint_confirm::<A>))
                .route(
                    "/token",
                    post(token_endpoint_post::<A, D>))
                .route(
                    "/token_status",
                    post(introspection_endpoint_post::<A>))
                .route(
                    "/revoke_token",
                    post(revocation_endpoint_post::<A>))
                .route(
                    "/userinfo",
                    get(userinfo_endpoint_get::<A, D>))

                .route("/webauthn/pre_register",
                       get(
                           #[cfg(feature = "webauthn")] webauthn::webauthn_pre_register::<A, D>,
                           #[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))
                .layer(Extension(backend))
            // I don't really like the fact that I have to use the whole database
            // If I could, I would've designed a separate trait for getting profiles
            // And made databases implement it, for example
                .layer(Extension(db))
                .layer(Extension(http))
        )
        .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::{Credential, AuthorizationConfirmation};

        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");
    }
}