//! # ACL — Access Control List pre-flight checks
//!
//! Query helpers call `check_table_acl()` before reading source-table rows or
//! returning hydrated data. Write helpers call `check_table_insert_acl()`,
//! `check_table_update_acl()`, or `check_table_delete_acl()` before modifying
//! mapped rows.
//!
//! See: `docs/contributor_guide/safety-security.mdx`

use crate::safety::{GraphError, GraphResult};

/// Check if the current user has SELECT privilege on the given table OID.
///
/// Uses PostgreSQL's native relation ACL checker.
///
/// # Errors
/// Returns `GraphError::AclDenied` if the user lacks SELECT on the table.
pub fn check_table_acl(table_oid: u32) -> GraphResult<()> {
    check_table_acl_mode(table_oid, pgrx::pg_sys::ACL_SELECT as pgrx::pg_sys::AclMode)
}

/// Check if the current user has INSERT privilege on the given table OID.
///
/// Uses PostgreSQL's native relation ACL checker.
///
/// # Errors
/// Returns `GraphError::AclDenied` if the user lacks INSERT on the table.
pub fn check_table_insert_acl(table_oid: u32) -> GraphResult<()> {
    check_table_acl_mode(table_oid, pgrx::pg_sys::ACL_INSERT as pgrx::pg_sys::AclMode)
}

/// Check if the current user has UPDATE privilege on the given table OID.
///
/// Uses PostgreSQL's native relation ACL checker.
///
/// # Errors
/// Returns `GraphError::AclDenied` if the user lacks UPDATE on the table.
pub fn check_table_update_acl(table_oid: u32) -> GraphResult<()> {
    check_table_acl_mode(table_oid, pgrx::pg_sys::ACL_UPDATE as pgrx::pg_sys::AclMode)
}

/// Check if the current user has DELETE privilege on the given table OID.
///
/// Uses PostgreSQL's native relation ACL checker.
///
/// # Errors
/// Returns `GraphError::AclDenied` if the user lacks DELETE on the table.
pub fn check_table_delete_acl(table_oid: u32) -> GraphResult<()> {
    check_table_acl_mode(table_oid, pgrx::pg_sys::ACL_DELETE as pgrx::pg_sys::AclMode)
}

fn check_table_acl_mode(table_oid: u32, mode: pgrx::pg_sys::AclMode) -> GraphResult<()> {
    let role_oid = current_acl_role_oid()?;
    let acl_result = unsafe {
        // SAFETY: This runs inside a PostgreSQL backend process. `table_oid` is
        // an OID supplied by callers that already resolved catalog objects.
        // `role_oid` is resolved from PostgreSQL session state, and the ACL
        // checker does not take ownership of Rust-managed memory.
        pgrx::pg_sys::pg_class_aclcheck(pgrx::pg_sys::Oid::from_u32(table_oid), role_oid, mode)
    };

    if acl_result != pgrx::pg_sys::AclResult::ACLCHECK_OK {
        return Err(GraphError::AclDenied {
            table: format!("OID {}", table_oid),
        });
    }
    Ok(())
}

fn current_acl_role_oid() -> GraphResult<pgrx::pg_sys::Oid> {
    pgrx::Spi::get_one::<pgrx::pg_sys::Oid>(
        "SELECT COALESCE(NULLIF(NULLIF(current_setting('role', true), ''), 'none'), current_user)::regrole::oid",
    )
    .map_err(|err| GraphError::Internal(format!("current ACL role read failed: {err}")))?
    .ok_or_else(|| GraphError::Internal("current ACL role oid was null".to_string()))
}
