use std::collections::{HashMap, HashSet};

use axum::{
    debug_handler,
    extract::{Path, State},
    response::IntoResponse,
};
use axum_extra::extract::Form;
use futures::{lock::Mutex, prelude::*, stream};
use phono_backends::{escape_identifier, pg_class::PgClass};
use phono_models::service_cred::ServiceCred;
use serde::Deserialize;
use sqlx::query;
use uuid::Uuid;

use crate::{
    app::{App, AppDbConn},
    errors::AppError,
    navigator::{Navigator, NavigatorPage},
    roles::{get_reader_role, get_writer_role},
    user::CurrentUser,
    workspace_pooler::{RoleAssignment, WorkspacePooler},
    workspace_utils::PHONO_TABLE_NAMESPACE,
};

#[derive(Clone, Debug, Deserialize)]
pub(super) struct PathParams {
    workspace_id: Uuid,
    service_cred_id: Uuid,
}

/// HTTP POST handler for assigning
#[debug_handler(state = App)]
pub(super) async fn post(
    State(mut pooler): State<WorkspacePooler>,
    AppDbConn(mut app_db): AppDbConn,
    CurrentUser(user): CurrentUser,
    navigator: Navigator,
    Path(PathParams {
        workspace_id,
        service_cred_id,
    }): Path<PathParams>,
    Form(form): Form<HashMap<String, Vec<String>>>,
) -> Result<impl IntoResponse, AppError> {
    // FIXME: csrf, auth

    let workspace_client = Mutex::new(
        pooler
            .acquire_for(workspace_id, RoleAssignment::User(user.id))
            .await?,
    );

    let all_rels = {
        let mut locked_client = workspace_client.lock().await;
        PgClass::belonging_to_namespace(PHONO_TABLE_NAMESPACE)
            .fetch_all(&mut locked_client)
            .await?
            .into_iter()
            .filter(|rel| rel.relkind == 'r' as i8)
    };

    let cred = ServiceCred::with_id(service_cred_id)
        .fetch_one(&mut app_db)
        .await?;

    stream::iter(all_rels)
        .then(async |rel| -> Result<(), AppError> {
            let rolname_reader = get_reader_role(&rel)?;
            let rolname_writer = get_writer_role(&rel)?;

            let roles_to_grant: HashSet<_> = form
                .get(&rel.oid.0.to_string())
                .unwrap_or(&vec![])
                .iter()
                .flat_map(|value| match value.as_str() {
                    "reader" => Some(&rolname_reader),
                    "writer" => Some(&rolname_writer),
                    _ => None,
                })
                .collect();
            let all_roles = HashSet::from([&rolname_reader, &rolname_writer]);
            let roles_to_revoke: HashSet<_> = all_roles.difference(&roles_to_grant).collect();

            if !roles_to_revoke.is_empty() {
                let mut locked_client = workspace_client.lock().await;
                let roles_to_revoke_snippet = roles_to_revoke
                    .into_iter()
                    .map(|value| escape_identifier(value))
                    .collect::<Vec<_>>()
                    .join(", ");
                query(&format!(
                    "revoke {roles_to_revoke_snippet} from {rolname_esc}",
                    rolname_esc = escape_identifier(&cred.rolname),
                ))
                .execute(locked_client.get_conn())
                .await?;
            }

            if !roles_to_grant.is_empty() {
                let mut locked_client = workspace_client.lock().await;
                let roles_to_grant_snippet = roles_to_grant
                    .into_iter()
                    .map(|value| escape_identifier(value))
                    .collect::<Vec<_>>()
                    .join(", ");
                query(&format!(
                    "grant {roles_to_grant_snippet} to {rolname_esc}",
                    rolname_esc = escape_identifier(&cred.rolname),
                ))
                .execute(locked_client.get_conn())
                .await?;
            }

            Ok(())
        })
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<Result<Vec<_>, AppError>>()?;

    Ok(navigator
        .workspace_page()
        .workspace_id(workspace_id)
        .suffix("service-credentials/")
        .build()?
        .redirect_to())
}
