use axum::{
    extract::Query,
    http::StatusCode,
    response::{ErrorResponse, Html, IntoResponse, Redirect, Response},
    routing::{get, post},
    Extension, Form, Router, Json,
};
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;
use axum::extract::State;

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct DemoCredential {
    pub username: String,
    pub password: String,
    pub description: String,
    pub profile: ProfileInfo,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct ProfileInfo {
    pub me: String,
    pub name: String,
    pub email: Option<String>,
    pub photo: Option<String>,
}

#[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,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct SupportCheckResponse {
    pub supported: bool,
    pub metadata: Option<ServerMetadata>,
    pub discovery_method: Option<String>,
    pub error: Option<String>,
    pub demo_credentials: Option<Vec<DemoCredential>>,
}

#[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:18080/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) => {
            let error_str = e.to_string();
            let user_message = if error_str.contains("NoMetadataEndpoint") {
                "This site doesn't support IndieAuth. Please check IndieAuth support first."
            } else if error_str.contains("MetadataContentTypeInvalid") {
                "The site's metadata endpoint returned an invalid content type."
            } else if error_str.contains("ParseMetadataJson") {
                "The site's metadata endpoint returned invalid JSON data."
            } else if error_str.contains("ParseError") {
                "Invalid URL format. Please enter a complete URL including https://"
            } else if error_str.contains("json") || error_str.contains("JSON") {
                "Failed to parse JSON response from the site."
            } else if error_str.contains("network") || error_str.contains("connection") {
                "Network error occurred. Please check your connection."
            } else {
                "Failed to start authorization. Please verify the site supports IndieAuth."
            };
            
            (StatusCode::BAD_REQUEST, Html(user_message.to_string())).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:18080/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(),
    ))
}

#[axum::debug_handler]
pub async fn check_support(
    Extension(client_state): Extension<ClientState>,
    State(server_state): State<Arc<crate::state::AppState>>,
    Query(RequestParams { me }): Query<RequestParams>,
) -> Result<Json<SupportCheckResponse>, ErrorResponse> {
    match client_state.client.obtain_metadata(&me).await {
        Ok(metadata) => {
            // Check if this is the local server and provide demo credentials
            let demo_credentials = if me.as_str().starts_with("http://127.0.0.1:18080") {
                Some(server_state.users.iter().map(|(username, user)| {
                    DemoCredential {
                        username: username.clone(),
                        password: user.password.clone(),
                        description: match username.as_str() {
                            "alice" => "Basic profile (name only)",
                            "bob" => "Full profile with avatar",
                            "charlie" => "External URL profile",
                            "demo" => "Simple profile with bot avatar",
                            _ => "Demo user",
                        }.to_string(),
                        profile: ProfileInfo {
                            me: user.profile.me.to_string(),
                            name: user.profile.name.clone(),
                            email: user.profile.email.clone(),
                            photo: user.profile.photo.as_ref().map(|p| p.to_string()),
                        },
                    }
                }).collect::<Vec<DemoCredential>>())
            } else {
                None
            };
            
            Ok(Json(SupportCheckResponse {
                supported: true,
                metadata: Some(metadata),
                discovery_method: Some("metadata-endpoint".to_string()),
                error: None,
                demo_credentials,
            }))
        }
        Err(e) => {
            let error_str = e.to_string();
            let user_friendly_error = if error_str.contains("NoMetadataEndpoint") {
                "This site doesn't appear to support IndieAuth. No authorization server metadata was found.".to_string()
            } else if error_str.contains("MetadataContentTypeInvalid") {
                "The site's metadata endpoint returned an invalid content type.".to_string()
            } else if error_str.contains("ParseMetadataJson") {
                "The site's metadata endpoint returned invalid JSON data.".to_string()
            } else if error_str.contains("ParseError") {
                "Invalid URL format. Please enter a complete URL including https://".to_string()
            } else if error_str.contains("json") || error_str.contains("JSON") {
                "Failed to parse JSON response from the site.".to_string()
            } else if error_str.contains("network") || error_str.contains("connection") {
                "Network error occurred. Please check your connection.".to_string()
            } else {
                "This site doesn't appear to support IndieAuth. No authorization server was found.".to_string()
            };
            
            Ok(Json(SupportCheckResponse {
                supported: false,
                metadata: None,
                discovery_method: None,
                error: Some(user_friendly_error),
                demo_credentials: None,
            }))
        }
    }
}

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))
        .route("/check-support", get(check_support))
}
