use axum::{extract::State, response::IntoResponse};
use phono_backends::{client::WorkspaceClient, rolnames::ROLE_PREFIX_USER};
use phono_models::{
    client::AppDbClient, cluster::Cluster, user::User, workspace::Workspace,
    workspace_user_perm::WorkspaceMembership,
};
use phono_pestgros::escape_identifier;
use sqlx::{Connection as _, PgConnection, query};

use crate::{
    app::AppDbConn,
    errors::AppError,
    navigator::{Navigator, NavigatorPage as _},
    user::CurrentUser,
    workspace_pooler::{RoleAssignment, WorkspacePooler},
    workspaces::PHONO_TABLE_NAMESPACE,
};

/// HTTP POST handler for creating a new workspace. This handler does not expect
/// any arguments, as the backing database name is generated pseudo-randomly and
/// the human-friendly name may be changed later rather than specified
/// permenantly upon creation.
pub(super) async fn post(
    State(mut pooler): State<WorkspacePooler>,
    AppDbConn(mut app_db): AppDbConn,
    navigator: Navigator,
    CurrentUser(user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
    // FIXME: csrf

    let cluster = Cluster::fetch_only(&mut app_db).await?;

    const NAME_LEN_WORDS: usize = 3;
    // WARNING: `db_name` is injected directly into the `create database` SQL
    // command. It **must not** contain spaces or any other unsafe characters.
    // Additionally, it **must** be URL safe without percent encoding.
    let db_name = phono_namegen::default_generator()
        .with_separator('_')
        .generate_name(NAME_LEN_WORDS);
    {
        // No need to pool these connections, since we don't expect to be using them
        // often. One less thing to keep track of in application state.
        let mut workspace_creator_conn = PgConnection::connect(
            cluster
                .conn_str_for_db("postgres", None)?
                .expose_secret()
                .as_str(),
        )
        .await?;
        query(&format!(
            // `db_name` is an underscore-separated sequence of alphabetical words,
            // which should be safe to inject directly into the SQL statement.
            "create database {db_name}"
        ))
        .execute(&mut workspace_creator_conn)
        .await?;
        query(&format!("revoke connect on database {db_name} from public"))
            .execute(&mut workspace_creator_conn)
            .await?;

        // Close and drop `workspace_creator_conn` at end of block, then
        // reconnect using a pooled connection.
        workspace_creator_conn.close().await?;
    }

    let workspace = Workspace::insert()
        .owner_id(user.id)
        .cluster_id(cluster.id)
        .db_name(&db_name)
        .build()?
        .insert(&mut app_db)
        .await?;

    let mut workspace_root_conn = pooler
        .acquire_for(workspace.id, RoleAssignment::Root)
        .await?;

    // Initialize database user. Connection is not used and may be dropped
    // immediately.
    pooler
        .acquire_for(workspace.id, RoleAssignment::User(user.id))
        .await?;
    query(&format!(
        "create schema {nsp}",
        nsp = escape_identifier(PHONO_TABLE_NAMESPACE)
    ))
    .execute(workspace_root_conn.get_conn())
    .await?;
    grant_workspace_membership(
        &db_name,
        &mut app_db,
        &mut workspace_root_conn,
        &user,
        &workspace,
    )
    .await?;

    Ok(navigator
        .workspace_page()
        .workspace_id(workspace.id)
        .build()?
        .redirect_to())
}

async fn grant_workspace_membership(
    db_name: &str,
    app_db_client: &mut AppDbClient,
    workspace_root_client: &mut WorkspaceClient,
    user: &User,
    workspace: &Workspace,
) -> Result<(), AppError> {
    let rolname = format!("{ROLE_PREFIX_USER}{uid}", uid = user.id.simple());
    query(&format!(
        "grant connect on database {db_name} to {db_user} with grant option",
        db_user = escape_identifier(&rolname),
    ))
    .execute(workspace_root_client.get_conn())
    .await?;

    // TODO: There may be cases in which we will want to grant granular
    // workspace access which excludes privileges to create tables.
    query(&format!(
        "grant usage, create on schema {nsp} to {rolname} with grant option",
        nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
        rolname = escape_identifier(&rolname)
    ))
    .execute(workspace_root_client.get_conn())
    .await?;

    WorkspaceMembership::upsert()
        .workspace_id(workspace.id)
        .user_id(user.id)
        .execute(app_db_client)
        .await?;

    Ok(())
}
