use axum::{
    extract::Query,
    http::StatusCode,
    response::{ErrorResponse, Html, IntoResponse, Redirect, Response},
    routing::{get, post},
    Extension, Form, Router,
};
use axum_extra::extract::{cookie::Cookie, CookieJar};
use indieweb::standards::indieauth::{
    self, AuthorizationRequestFields, CommonRedemptionFields, RedemptionClaim, RedemptionFields,
    RedirectErrorFields, RedirectUri, ServerMetadata, SignedRedirectFields,
};
use miette::IntoDiagnostic;
use minijinja::context;
use std::sync::Arc;
use url::Url;

use crate::engine;
use crate::ClientState;

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct RequestSession {
    me: Url,
    metadata: ServerMetadata,
    state: String,
    verifier: String,
}

impl RequestSession {
    const COOKIE_NAME: &str = "request-session";
    fn from_cookie(jar: &CookieJar) -> Option<Self> {
        serde_json::from_str(
            &jar.get(Self::COOKIE_NAME)
                .map(|v| v.value().to_string())
                .unwrap_or_default(),
        )
        .ok()
    }

    fn store_in_cookie(&self, jar: CookieJar) -> CookieJar {
        jar.add(Cookie::new(
            Self::COOKIE_NAME,
            serde_json::json!(self).to_string(),
        ))
    }
}

#[tracing::instrument]
pub async fn landing(jar: CookieJar) -> impl IntoResponse {
    let me = RequestSession::from_cookie(&jar)
        .map(|rs| rs.me.to_string())
        .unwrap_or_default();
    Html(
        engine()
            .get_template("client.html")
            .unwrap()
            .render(context! { me })
            .unwrap(),
    )
}

#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub struct RequestParams {
    me: Url,
}

#[axum::debug_handler]
pub async fn begin_authorization(
    Extension(client_state): Extension<ClientState>,
    Query(RequestParams { me }): Query<RequestParams>,
    jar: CookieJar,
) -> Response {
    let resp: miette::Result<_> = async move {
        let mut req_session = RequestSession {
            metadata: client_state.client.obtain_metadata(&me).await?,
            me,
            state: format!("st_{}", nanoid::nanoid!(16)),
            verifier: Default::default(),
        };
        let request = AuthorizationRequestFields::new(
            &client_state.client_id,
            &"http://127.0.0.1:18000/client/redirect"
                .parse()
                .into_diagnostic()?,
            req_session.state.clone(),
        )?;
        req_session.verifier = request.challenge.verifier().to_string();
        let jar = req_session.store_in_cookie(jar);
        let signed_url = req_session.metadata.new_authorization_request_url(
            request,
            vec![("me".to_string(), req_session.me.to_string())],
        )?;

        Ok((jar, Redirect::to(signed_url.as_str())))
    }
    .await;

    match resp {
        Ok(resp) => resp.into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:#?}")).into_response(),
    }
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(untagged, rename_all = "snake_case")]
pub enum RedirectQuery {
    Error(RedirectErrorFields),
    Response(SignedRedirectFields),
}

#[tracing::instrument]
pub async fn complete_authorization(Query(resp): Query<RedirectQuery>, jar: CookieJar) -> Response {
    let verifier = if let Some(RequestSession { verifier, .. }) = RequestSession::from_cookie(&jar)
    {
        verifier
    } else {
        return (
            StatusCode::BAD_REQUEST,
            Html("You might need to try this request again; no cookie was found.".to_string()),
        )
            .into_response();
    };
    Html(
        engine()
            .get_template("client-redirect.html")
            .unwrap()
            .render(context! { resp, verifier })
            .unwrap(),
    )
    .into_response()
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(rename_all = "snake_case")]
enum RedeemVia {
    Profile,
    Token,
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub struct RedemptionForm {
    redeem: RedeemVia,
    code: String,
}

#[axum::debug_handler]
#[tracing::instrument(skip(client_state))]
pub async fn redeem_authorization_code(
    Extension(client_state): Extension<ClientState>,
    jar: CookieJar,
    Form(RedemptionForm { redeem, code }): Form<RedemptionForm>,
) -> Result<Html<String>, ErrorResponse> {
    let req_session = if let Some(rq) = RequestSession::from_cookie(&jar) {
        rq
    } else {
        return Err((
            StatusCode::BAD_REQUEST,
            Html("No session information for this request"),
        )
            .into_response()
            .into());
    };

    let fields = RedemptionFields {
        code,
        client_id: client_state.client_id.clone(),
        redirect_uri: RedirectUri::from(
            "http://127.0.0.1:18000/client/redirect"
                .parse::<Url>()
                .unwrap(),
        ),
        verifier: req_session.verifier,
    };

    let ctx = || async {
        match redeem {
            RedeemVia::Profile => {
                let resp = match client_state.client
                    .redeem::<CommonRedemptionFields>(
                        &req_session.metadata.authorization_endpoint,
                        fields,
                    )
                    .await
                {
                    Ok(r) => r,
                    Err(e) => {
                        return context! {
                            error => format!("Failed to redeem code at authorization endpoint: {e:#?}")
                        };
                    }
                };

                if let indieauth::RedemptionResponse::Claim(RedemptionClaim {
                    access_token,
                    refresh_token,
                    scope,
                    me,
                    expires_in,
                    payload,
                    ..
                }) = resp
                {
                    context! {
                        token => access_token,
                        scope,
                        expires_in,
                        me,
                        refresh_token,
                        profile => payload
                    }
                } else if let indieauth::RedemptionResponse::Error(error) = resp {
                    context! { error => "Failed to complete the redemption request for a profile.", resp => error }
                } else {
                    context! { error => "The claim was not for a profile; you probably clicked the wrong button!" }
                }
            }
            RedeemVia::Token => {
                let resp = match client_state.client
                    .redeem::<CommonRedemptionFields>(&req_session.metadata.token_endpoint, fields)
                    .await
                {
                    Ok(r) => r,
                    Err(e) => {
                        return context! {
                            error => format!("Failed to redeem the code at the token endpoint: {e:#?}")
                        };
                    }
                };

                if let indieauth::RedemptionResponse::Claim(RedemptionClaim {
                    access_token,
                    scope,
                    me,
                    refresh_token,
                    expires_in,
                    ..
                }) = resp
                {
                    context! {
                        token => access_token,
                        scope,
                        expires_in,
                        refresh_token,
                        me
                    }
                } else if let indieauth::RedemptionResponse::Error(error) = resp {
                    context! { error => "Failed to complete the redemption request for a token.", resp => error }
                } else {
                    context! { error => "The claim was not for a token; you probably clicked the wrong button!" }
                }
            }
        }
    };
    Ok(Html(
        engine()
            .get_template("client-redeem.html")
            .unwrap()
            .render(ctx().await)
            .unwrap(),
    ))
}

pub(crate) fn router() -> Router<Arc<crate::state::AppState>> {
    Router::new()
        .route("/", get(landing))
        .route("/redirect", get(complete_authorization))
        .route("/redeem", post(redeem_authorization_code))
        .route("/auth", get(begin_authorization))
}
