use std::sync::Arc;

use axum::body::Bytes;
use axum::extract::{MatchedPath, Path, Request, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::{HeaderName, StatusCode};
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::CookieJar;

use base64::{Engine as B64Engine, engine::general_purpose::STANDARD as b64};

pub async fn get(
    State(state): State<Arc<crate::server::AppState>>,
    jar: CookieJar,
    matched_path: MatchedPath,
    req: Request,
) -> Result<(StatusCode, [(HeaderName, String); 1], Bytes), (StatusCode, Redirect)> {
    let span = tracing::info_span!(
        "template",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
    );

    span.in_scope(|| {
        let params = req.uri().query().map(ToString::to_string);

        let token: Vec<u8>;
        let mut claims = None;

        if let Some(idx) = state.template_route_map.get(matched_path.as_str()) {
            if let Some(template) = state.templates.get(*idx) {
                span.record("i", template.config.idx);
                span.record("nm", &template.config.name);

                if let Some(_check) = &template.config.protected {
                    let cookie_name = if state.secure_cookies {
                        "__Host-ORDINARY-ACCESS-TOKEN"
                    } else {
                        "ORDINARY-ACCESS-TOKEN"
                    };

                    token = if let Some(token) = jar.get(cookie_name) {
                        if let Ok(token) = b64.decode(token.value()) {
                            token
                        } else {
                            return Err((StatusCode::UNAUTHORIZED, Redirect::to("/access")));
                        }
                    } else {
                        return Err((StatusCode::UNAUTHORIZED, Redirect::to("/access")));
                    };

                    match state.auth.verify_access_token(&token) {
                        Ok((_account, real_claims)) => {
                            claims = Some(real_claims);
                        }
                        Err(_) => {
                            return Err((StatusCode::UNAUTHORIZED, Redirect::to("/access")));
                        }
                    }
                }

                match template.render(req.uri().path().into(), params.clone(), None, None, &claims)
                {
                    Ok((mime, res)) => {
                        return Ok((StatusCode::OK, [(CONTENT_TYPE, mime)], res));
                    }
                    Err(err) => tracing::error!("{err}"),
                }
            }
        } else {
            tracing::warn!("no template for route '{:?}'", matched_path);
        }

        if let Some(idx) = state.error_template_idx
            && let Some(err_template) = state.templates.get(idx as usize)
        {
            match err_template.render(
                "/error".into(),
                params,
                Some(("no template for this route".into(), 404)),
                None,
                &claims,
            ) {
                Ok((mime, res)) => {
                    return Ok((StatusCode::NOT_FOUND, [(CONTENT_TYPE, mime)], res));
                }
                Err(err) => tracing::error!("{err}"),
            }
        }

        Ok((
            StatusCode::NOT_FOUND,
            [(CONTENT_TYPE, "text/plain".to_string())],
            Bytes::copy_from_slice(b"no template for the specified route"),
        ))
    })
}

pub async fn query(
    State(state): State<Arc<crate::server::AppState>>,
    Path((idx, path)): Path<(u8, String)>,
    req: Request,
) -> impl IntoResponse {
    let span = tracing::info_span!(
        "template",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
    );

    span.in_scope(|| {
        let token: Vec<u8>;
        let mut claims = None;

        if let Some(template) = state.templates.get(idx as usize) {
            span.record("i", template.config.idx);
            span.record("nm", &template.config.name);

            if let Some(_check) = &template.config.protected
                && let Some(val) = req.headers().get("authorization")
                && let Ok(str_val) = val.to_str()
                && let Some(b64_token) = str_val.strip_prefix("Ordinary ")
                && let Ok(t) = b64.decode(b64_token)
            {
                token = t;
                match state.auth.verify_access_token(&token) {
                    Ok((_account, real_claims)) => {
                        // todo: validate claims against the `_check`
                        claims = Some(real_claims);
                    }
                    Err(err) => {
                        tracing::error!("{err}");
                        return (
                            StatusCode::UNAUTHORIZED,
                            Bytes::copy_from_slice(b"unauthorized"),
                        );
                    }
                }
            }

            let params = req.uri().query().map(ToString::to_string);

            match template.query(path, params, None, None, &claims) {
                Ok(res) => return (StatusCode::OK, res),
                Err(err) => {
                    tracing::error!("{err}");
                }
            }
        } else {
            tracing::warn!("no template for index '{idx}'");
        }
        (StatusCode::NOT_FOUND, Bytes::copy_from_slice(b"not found"))
    })
}
