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

use anyhow::anyhow;
use axum::{
    debug_handler,
    extract::{Path, State},
    response::IntoResponse,
};
use axum_extra::extract::Form;
use futures::{lock::Mutex, prelude::*, stream};
use phono_backends::{pg_class::PgClass, pg_role::RoleTree, rolnames::ROLE_PREFIX_USER};
use phono_models::{
    accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor},
    user::User,
};
use serde::Deserialize;
use sqlx::postgres::types::Oid;
use uuid::Uuid;

use crate::{
    app::AppDbConn,
    errors::{AppError, bad_request},
    navigator::{Navigator, NavigatorPage},
    permissions::{Grantee, RelPermission, grant_rel_permissions, revoke_rel_permissions},
    user::CurrentUser,
    workspace_pooler::{RoleAssignment, WorkspacePooler},
};

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

/// HTTP POST handler for assigning relation permissions to an invite.
///
/// The form body is expected to encode a one-to-many mapping of relation OID to
/// [`RelPermissionKind`], as well as exactly one `user_id` field.
/// For example:
/// ```text
/// user_id=5c2eec2fc05e415888a9c2f79216ad56&
/// 123=reader&
/// 123=writer&
/// 123=owner&
/// 234=reader&
/// 345=writer
/// ```
#[debug_handler(state = crate::app::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 }): Path<PathParams>,
    Form(form): Form<HashMap<String, Vec<String>>>,
) -> Result<impl IntoResponse, AppError> {
    // FIXME: csrf

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

    WorkspaceAccessor::new()
        .id(workspace_id)
        .as_actor(Actor::User(user.id))
        .using_app_db(&mut app_db)
        .using_workspace_client(&mut workspace_client)
        .fetch_one()
        .await?;

    let user_id = form
        .get("user_id")
        .and_then(|id_strs| id_strs.first())
        .map_or(Err(bad_request!("expected user_id")), |id_str| {
            Uuid::parse_str(id_str).map_err(|_| bad_request!("expected user_id to be a UUID"))
        })?;

    let target_user = User::with_id(user_id)
        .fetch_optional(&mut app_db)
        .await?
        .ok_or(bad_request!("no user with that ID"))?;
    workspace_client
        .ensure_role(&format!("{ROLE_PREFIX_USER}{id}", id = user_id.simple()))
        .await?;

    let current_roles =
        RoleTree::granted_to_rolname(&format!("{ROLE_PREFIX_USER}{id}", id = user_id.simple()))
            .fetch_tree(&mut workspace_client)
            .await?
            .ok_or(anyhow!("unable to fetch role tree"))?
            .flatten_inherited();

    // From this point on we may need to use `workspace_client` in async
    // closures, so wrap it in a mutex to safely use it in control flow which
    // could in theory attempt concurrent access.
    let workspace_client_mutex = Mutex::new(workspace_client);

    let current_perms: HashSet<RelPermission> = stream::iter(current_roles)
        .then(async |role| {
            let mut workspace_client = workspace_client_mutex.lock().await;
            RelPermission::from_rolname(&role.role.rolname, &mut workspace_client).await
        })
        .collect::<Vec<sqlx::Result<_>>>()
        .await
        .into_iter()
        .collect::<sqlx::Result<Vec<_>>>()?
        .into_iter()
        .flatten()
        .collect();

    let desired_perms: HashSet<RelPermission> =
        stream::iter(form.into_iter().flat_map(|(oid_str, perm_strs)| {
            perm_strs
                .into_iter()
                .flat_map(|perm_str| {
                    if oid_str == "user_id" {
                        None
                    } else {
                        Some((oid_str.clone(), perm_str))
                    }
                })
                .collect::<Vec<_>>()
        }))
        .then(async |(oid_str, perm_str)| -> Result<_, AppError> {
            let rel_oid = Oid(oid_str.parse()?);
            let mut workspace_client = workspace_client_mutex.lock().await;
            let rel = PgClass::with_oid(rel_oid)
                .fetch_one(&mut workspace_client)
                .await?;
            Ok(RelPermission {
                kind: perm_str.as_str().try_into()?,
                rel_oid,
                relname: rel.relname,
            })
        })
        .collect::<Vec<Result<_, _>>>()
        .await
        .into_iter()
        .collect::<Result<_, _>>()?;

    let mut root_client = pooler
        .acquire_for(workspace_id, RoleAssignment::Root)
        .await?;

    revoke_rel_permissions(
        Grantee::User(target_user.clone()),
        current_perms
            .difference(&desired_perms)
            .cloned()
            .collect::<HashSet<_>>(),
        &mut root_client,
    )
    .await?;
    grant_rel_permissions(
        Grantee::User(target_user),
        Actor::User(user.id),
        desired_perms,
        &mut root_client,
    )
    .await?;

    Ok(navigator
        .workspace_page()
        .workspace_id(workspace_id)
        .suffix("settings/")
        .build()?
        .redirect_to())
}
