use std::sync::Arc;

use axum::Form;
use axum::body::Bytes;
use axum::extract::{MatchedPath, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::CookieJar;
use configinator::ActionTrigger;
use flexbuffers::BuilderOptions;
use notatype::{
    Kind, flexbuffer_reader_to_json, flexbuffer_reader_to_path_component, json_to_flexbuffer_vec,
};

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

pub async fn invoke(
    State(state): State<Arc<crate::server::AppState>>,
    body: Bytes,
) -> impl IntoResponse {
    // todo: check origin for valid template.actions routes

    let span = tracing::info_span!(
        "action",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
        tgr = "Ordinary",
    );

    span.in_scope(|| {
        if let Ok(root) = flexbuffers::Reader::get_root(&body[..]) {
            let payload = root.as_vector();

            let idx = payload.idx(0).as_u8();

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

                let mut builder = flexbuffers::Builder::new(BuilderOptions::SHARE_NONE);
                let mut payload_vec = builder.start_vector();

                if let Err(err) = action
                    .config
                    .accepts
                    .copy_to(&payload.idx(1), &mut payload_vec)
                {
                    tracing::error!(%err);
                    return (StatusCode::INTERNAL_SERVER_ERROR, Bytes::new());
                }

                if let Some(_check) = &action.config.protected {
                    let token = payload
                        .idx(2)
                        .as_vector()
                        .iter()
                        .map(|v| v.as_u8())
                        .collect::<Vec<u8>>();
                    match state.auth.verify_access_token(&token) {
                        Ok((_account, claims_root)) => {
                            let mut claims_vec = payload_vec.start_vector();

                            let empty_system_claims = claims_vec.start_vector();
                            empty_system_claims.end_vector();

                            for field in &state.auth.config.access_token.claims {
                                // todo: use the check to validate any claims

                                if let Err(err) = field
                                    .kind
                                    .copy_to(&claims_root.idx(field.idx as usize), &mut claims_vec)
                                {
                                    tracing::error!(%err);
                                    return (StatusCode::INTERNAL_SERVER_ERROR, Bytes::new());
                                }
                            }

                            claims_vec.end_vector();
                        }
                        Err(err) => {
                            tracing::error!("{err}");
                            return (StatusCode::UNAUTHORIZED, Bytes::new());
                        }
                    }
                }

                payload_vec.end_vector();

                if let Ok(res) = action.call(builder.view(), &state.actions) {
                    return (StatusCode::OK, res);
                }
            }
        }

        (StatusCode::INTERNAL_SERVER_ERROR, Bytes::new())
    })
}

#[allow(clippy::too_many_lines)]
pub async fn form(
    State(state): State<Arc<crate::server::AppState>>,
    path: MatchedPath,
    jar: CookieJar,
    body: Form<serde_json::Value>,
) -> Result<Redirect, StatusCode> {
    // todo: check origin for valid template.actions routes

    let span = tracing::info_span!(
        "action",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
        tgr = "Form",
    );

    span.in_scope(|| {
        if let Some(idx) = state.action_route_map.get(path.as_str())
            && let Some(action) = state.actions.get(*idx)
        {
            span.record("i", action.config.idx);
            span.record("nm", &action.config.name);

            let mut builder = flexbuffers::Builder::new(BuilderOptions::SHARE_NONE);
            let mut payload_vec = builder.start_vector();

            if let Err(err) =
                json_to_flexbuffer_vec(&action.config.accepts, &body, &mut payload_vec)
            {
                tracing::error!("{err}");
                return Err(StatusCode::INTERNAL_SERVER_ERROR);
            }

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

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

                match state.auth.verify_access_token(&token) {
                    Ok((_account, claims_root)) => {
                        let mut claims_vec = payload_vec.start_vector();

                        let empty_system_claims = claims_vec.start_vector();
                        empty_system_claims.end_vector();

                        for field in &state.auth.config.access_token.claims {
                            // todo: use the check to validate any claims

                            if let Err(err) = field
                                .kind
                                .copy_to(&claims_root.idx(field.idx as usize), &mut claims_vec)
                            {
                                tracing::error!(%err);
                                return Err(StatusCode::INTERNAL_SERVER_ERROR);
                            }
                        }

                        claims_vec.end_vector();
                    }
                    Err(err) => {
                        tracing::error!("{err}");
                        return Err(StatusCode::UNAUTHORIZED);
                    }
                }
            }

            payload_vec.end_vector();

            match action.call(builder.view(), &state.actions) {
                Ok(res) => match flexbuffers::Reader::get_root(&res[..]) {
                    Ok(root) => {
                        for trigger in &action.config.triggered_by {
                            if let ActionTrigger::Form {
                                route: _,
                                method: _,
                                redirect,
                            } = trigger
                            {
                                let mut redirect = redirect.clone();

                                if let Ok(payload_root) =
                                    flexbuffers::Reader::get_root(builder.view())
                                {
                                    if redirect.contains("{accepts}") {
                                        redirect = redirect.replace(
                                            "{accepts}",
                                            &match flexbuffer_reader_to_path_component(
                                                &action.config.returns,
                                                &payload_root.as_vector().idx(0),
                                            ) {
                                                Ok(v) => v,
                                                Err(err) => {
                                                    tracing::error!(%err);
                                                    return Err(StatusCode::INTERNAL_SERVER_ERROR);
                                                }
                                            },
                                        );
                                    }

                                    if redirect.contains("{accepts.")
                                        && let Kind::Object { name: _, fields } =
                                            &action.config.accepts
                                    {
                                        let accepts_vec =
                                            payload_root.as_vector().idx(0).as_vector();

                                        for field in fields {
                                            redirect = redirect.replace(
                                                &format!("{{accepts.{}}}", field.name),
                                                &match flexbuffer_reader_to_path_component(
                                                    &field.kind,
                                                    &accepts_vec.idx(field.idx as usize),
                                                ) {
                                                    Ok(v) => v,
                                                    Err(err) => {
                                                        tracing::error!(%err);
                                                        return Err(
                                                            StatusCode::INTERNAL_SERVER_ERROR,
                                                        );
                                                    }
                                                },
                                            );
                                        }
                                    }

                                    // todo: handle `accepts` lists
                                }

                                if redirect.contains("{returns}") {
                                    redirect = redirect.replace(
                                        "{returns}",
                                        &match flexbuffer_reader_to_path_component(
                                            &action.config.returns,
                                            &root,
                                        ) {
                                            Ok(v) => v,
                                            Err(err) => {
                                                tracing::error!(%err);
                                                return Err(StatusCode::INTERNAL_SERVER_ERROR);
                                            }
                                        },
                                    );
                                }

                                if redirect.contains("{returns.")
                                    && let Kind::Object { name: _, fields } = &action.config.returns
                                {
                                    let root_vec = root.as_vector();

                                    for field in fields {
                                        redirect = redirect.replace(
                                            &format!("{{returns.{}}}", field.name),
                                            &match flexbuffer_reader_to_path_component(
                                                &field.kind,
                                                &root_vec.idx(field.idx as usize),
                                            ) {
                                                Ok(v) => v,
                                                Err(err) => {
                                                    tracing::error!(%err);
                                                    return Err(StatusCode::INTERNAL_SERVER_ERROR);
                                                }
                                            },
                                        );
                                    }
                                }

                                // todo: handle `returns` lists

                                return Ok(Redirect::to(&redirect));
                            }
                        }
                    }
                    Err(err) => tracing::error!("{err}"),
                },
                Err(err) => {
                    tracing::error!("{err}");
                }
            }
        }

        Err(StatusCode::INTERNAL_SERVER_ERROR)
    })
}

#[allow(clippy::too_many_lines)]
pub async fn rest(
    State(state): State<Arc<crate::server::AppState>>,
    path: MatchedPath,
    headers: HeaderMap,
    body: Bytes,
) -> impl IntoResponse {
    // todo: check origin for valid template.actions routes

    let span = tracing::info_span!(
        "action",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
        tgr = "Rest",
    );

    span.in_scope(|| {
        if let Some(idx) = state.action_route_map.get(path.as_str()) {
            return if let Some(action) = state.actions.get(*idx) {
                span.record("i", action.config.idx);
                span.record("nm", &action.config.name);

                let mut builder = flexbuffers::Builder::new(BuilderOptions::SHARE_NONE);
                let mut payload_vec = builder.start_vector();

                let json_body = if body.is_empty() {
                    serde_json::Value::Null
                } else {
                    match serde_json::from_slice(&body[..]) {
                        Ok(b) => b,
                        Err(err) => {
                            tracing::error!("{err}");
                            return (StatusCode::INTERNAL_SERVER_ERROR, "error!".to_string());
                        }
                    }
                };

                if let Err(err) =
                    json_to_flexbuffer_vec(&action.config.accepts, &json_body, &mut payload_vec)
                {
                    tracing::error!("{err}");
                    return (StatusCode::INTERNAL_SERVER_ERROR, "error!".to_string());
                }

                if let Some(_check) = &action.config.protected {
                    if let Some(val) = headers.get("authorization") {
                        if let Ok(str_val) = val.to_str() {
                            if let Some(b64_token) = str_val.strip_prefix("Ordinary ") {
                                if let Ok(token) = b64.decode(b64_token) {
                                    match state.auth.verify_access_token(&token) {
                                        Ok((_account, claims_root)) => {
                                            let mut claims_vec = payload_vec.start_vector();

                                            let empty_system_claims = claims_vec.start_vector();
                                            empty_system_claims.end_vector();

                                            for field in &state.auth.config.access_token.claims {
                                                // todo: use the check to validate any claims

                                                if let Err(err) = field.kind.copy_to(
                                                    &claims_root.idx(field.idx as usize),
                                                    &mut claims_vec,
                                                ) {
                                                    tracing::error!(%err);
                                                    return (
                                                        StatusCode::INTERNAL_SERVER_ERROR,
                                                        String::new(),
                                                    );
                                                }
                                            }

                                            claims_vec.end_vector();
                                        }
                                        Err(err) => {
                                            tracing::error!("{err}");
                                            return (
                                                StatusCode::UNAUTHORIZED,
                                                "unauthorized".into(),
                                            );
                                        }
                                    }
                                } else {
                                    return (StatusCode::UNAUTHORIZED, "unauthorized".into());
                                }
                            } else {
                                return (StatusCode::UNAUTHORIZED, "unauthorized".into());
                            }
                        } else {
                            return (StatusCode::UNAUTHORIZED, "unauthorized".into());
                        }
                    } else {
                        return (StatusCode::UNAUTHORIZED, "unauthorized".into());
                    }
                }

                payload_vec.end_vector();

                match action.call(builder.view(), &state.actions) {
                    Ok(res) => match flexbuffers::Reader::get_root(&res[..]) {
                        Ok(root) => {
                            let json_res =
                                match flexbuffer_reader_to_json(&action.config.returns, &root) {
                                    Ok(res) => res,
                                    Err(err) => {
                                        tracing::error!(%err);
                                        return (StatusCode::INTERNAL_SERVER_ERROR, String::new());
                                    }
                                };

                            (StatusCode::OK, json_res.to_string())
                        }
                        Err(err) => {
                            tracing::error!("{err}");
                            (
                                StatusCode::INTERNAL_SERVER_ERROR,
                                format!("error deserializing the result of action {idx}"),
                            )
                        }
                    },
                    Err(err) => {
                        tracing::error!("{err}");
                        (
                            StatusCode::INTERNAL_SERVER_ERROR,
                            format!("error when calling action {idx}"),
                        )
                    }
                }
            } else {
                tracing::error!("no action at for idx {}", idx);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("no action at for idx {idx}"),
                )
            };
        }

        tracing::error!("no action at path {}", path.as_str());
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("no action at path {}", path.as_str()),
        )
    })
}
