use axum::{
    extract::{Path, State},
    response::IntoResponse,
};
use phono_backends::rolnames::{
    ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER, ROLE_PREFIX_USER,
};
use phono_models::accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor};
use phono_pestgros::escape_identifier;
use serde::Deserialize;
use sqlx::{Acquire as _, query};
use uuid::Uuid;

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

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

/// HTTP POST handler for creating a managed Postgres table within a workspace
/// database. Upon success, it redirects the client back to the workspace
/// homepage, which is expected to display a list of available tables including
/// the newly created one.
///
/// This handler expects 1 path parameter named `workspace_id` which should
/// deserialize to a UUID.
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>,
) -> Result<impl IntoResponse, AppError> {
    // FIXME: CSRF

    // TODO: Condition table creation on schema "CREATE" privileges, which will
    // allow this client to be configured with user-level permissions rather
    // than root-level.
    let mut root_client = pooler
        .acquire_for(workspace_id, RoleAssignment::Root)
        .await?;

    // For authorization only.
    WorkspaceAccessor::new()
        .id(workspace_id)
        .as_actor(Actor::User(user.id))
        .using_workspace_client(&mut root_client)
        .using_app_db(&mut app_db)
        .fetch_one()
        .await?;

    const NAME_LEN_WORDS: usize = 3;
    let table_name = phono_namegen::default_generator()
        .with_separator('_')
        .generate_name(NAME_LEN_WORDS);

    let user_rolname = format!("{ROLE_PREFIX_USER}{uid}", uid = user.id.simple());
    let rolname_uuid = Uuid::new_v4().simple();
    let rolname_table_owner = format!("{ROLE_PREFIX_TABLE_OWNER}{rolname_uuid}");
    let rolname_table_reader = format!("{ROLE_PREFIX_TABLE_READER}{rolname_uuid}");
    let rolname_table_writer = format!("{ROLE_PREFIX_TABLE_WRITER}{rolname_uuid}");
    for rolname in [
        &rolname_table_owner,
        &rolname_table_reader,
        &rolname_table_writer,
    ] {
        query(&format!("create role {0}", escape_identifier(rolname)))
            .execute(root_client.get_conn())
            .await?;
        query(&format!(
            "grant {0} to {1} with admin option",
            escape_identifier(rolname),
            escape_identifier(&user_rolname)
        ))
        .execute(root_client.get_conn())
        .await?;
    }

    query(&format!(
        r#"
create table {0}.{1} (
    _id uuid primary key not null default uuidv7(),
    _created_by text default current_user,
    _created_at timestamptz not null default now()
)
"#,
        escape_identifier(PHONO_TABLE_NAMESPACE),
        escape_identifier(&table_name),
    ))
    .execute(root_client.get_conn())
    .await?;

    // Postgres requires that a role have "CREATE" privileges on a schema when
    // it is given ownership of a relation in that schema. This is at odds with
    // our intent here, since the dedicated table owner role should never need
    // nor want to have or impart the ability to create unrelated tables.
    //
    // While not strictly necessary, in order to keep user permissions as clean
    // as possible, we run all three of the "GRANT", "ALTER", and "REVOKE"
    // commands within a transaction. At least as of Postgres 18, emperical
    // testing confirms that permissions updates behave similarly to
    // conventional commands and queries executed within transactions, so for
    // outside observers, the table owner role and its descendents should never
    // appear to actually receive schema "CREATE" privileges, even momentarily,
    // as a result of this code block.
    {
        let mut txn = root_client.get_conn().begin().await?;
        query(&format!(
            "grant create on schema {nsp} to {rol}",
            nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
            rol = escape_identifier(&rolname_table_owner)
        ))
        .execute(&mut *txn)
        .await?;
        query(&format!(
            "alter table {nsp}.{tbl} owner to {rol}",
            nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
            tbl = escape_identifier(&table_name),
            rol = escape_identifier(&rolname_table_owner),
        ))
        .execute(&mut *txn)
        .await?;
        query(&format!(
            "revoke create on schema {nsp} from {rol}",
            nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
            rol = escape_identifier(&rolname_table_owner)
        ))
        .execute(&mut *txn)
        .await?;
        txn.commit().await?;
    }

    query(&format!(
        "grant select on {nsp}.{tbl} to {rol}",
        nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
        tbl = escape_identifier(&table_name),
        rol = escape_identifier(&rolname_table_reader),
    ))
    .execute(root_client.get_conn())
    .await?;
    query(&format!(
        "grant delete, truncate on {nsp}.{tbl} to {rol}",
        nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
        tbl = escape_identifier(&table_name),
        rol = escape_identifier(&rolname_table_writer),
    ))
    .execute(root_client.get_conn())
    .await?;

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