use derive_builder::Builder;
use phono_backends::{
    client::WorkspaceClient, pg_acl::PgPrivilegeType, pg_database::PgDatabase,
    rolnames::ROLE_PREFIX_USER,
};
use tracing::{Instrument as _, debug, info_span};
use uuid::Uuid;

use crate::{
    client::AppDbClient,
    errors::{AccessError, QueryResult},
    workspace::Workspace,
    workspace_user_perm::WorkspaceMembership,
};

use super::{Accessor, Actor};

/// Utility for fetching a [`Workspace`], with authorization.
#[derive(Builder, Debug)]
#[builder(
    // Build fn should only be called internally, via `fetch_optional()`.
    build_fn(private, error = "AccessError"),
    // Callers interact primarily with the generated builder struct.
    name = "WorkspaceAccessor",
    vis = "pub",
    // "Owned" pattern circumvents the `Clone` trait bound on fields.
    pattern = "owned",
)]
struct GeneratedWorkspaceAccessor<'a> {
    /// Required. ID of the workspace to be accessed.
    id: Uuid,

    /// Required. Identity against which to evaluate authorization checks.
    as_actor: Actor,

    /// Required. Client for the backing database. Providing a client authorized
    /// as the root Phonograph user will not compromise the integrity of the
    /// authorization checks.
    using_workspace_client: &'a mut WorkspaceClient,

    /// Required. Client for the application database.
    using_app_db: &'a mut AppDbClient,
}

impl<'a> Accessor<Workspace> for WorkspaceAccessor<'a> {
    async fn fetch_one(self) -> Result<Workspace, AccessError> {
        let spec = self.build()?;
        async {
            debug!("accessing workspace");
            let workspace = if let Some(value) = Workspace::with_id(spec.id)
                .fetch_optional(spec.using_app_db)
                .await?
            {
                value
            } else {
                debug!("workspace access denied: workspace not found");
                clear_workspace_membership(spec.id, spec.as_actor, spec.using_app_db).await?;
                return Err(AccessError::NotFound);
            };
            debug!("workspace found");

            let actor_rolname = match spec.as_actor {
                Actor::Bypass => None,
                Actor::User(user_id) => Some(format!(
                    "{ROLE_PREFIX_USER}{user_id}",
                    user_id = user_id.simple()
                )),
            };

            if let Some(actor_rolname) = actor_rolname {
                // Verify database CONNECT permissions.
                let pg_db = PgDatabase::current()
                    .fetch_one(spec.using_workspace_client)
                    .await?;
                if !pg_db.datacl.unwrap_or_default().iter().any(|acl| {
                    // Currently database connect permissions are always granted
                    // directly, though this may change in the future.
                    // TODO: Generalize to inherited roles
                    acl.grantee == actor_rolname
                        && acl
                            .privileges
                            .iter()
                            .any(|privilege| privilege.privilege == PgPrivilegeType::Connect)
                }) {
                    debug!("workspace access denied: actor lacks postgres connect privilege");
                    clear_workspace_membership(spec.id, spec.as_actor, spec.using_app_db).await?;
                    return Err(AccessError::NotFound);
                }
            }

            debug!("workspace access approved");
            cache_workspace_membership(spec.id, spec.as_actor, spec.using_app_db).await?;
            Ok(workspace)
        }
        .instrument(info_span!(
            "WorkspaceAccessor::fetch_one()",
            workspace_id = spec.id.to_string(),
            actor = spec.as_actor.to_string(),
        ))
        .await
    }
}

async fn clear_workspace_membership(
    workspace_id: Uuid,
    actor: Actor,
    app_db: &mut AppDbClient,
) -> QueryResult<()> {
    if let Actor::User(user_id) = actor {
        WorkspaceMembership::delete()
            .user_id(user_id)
            .workspace_id(workspace_id)
            .execute(app_db)
            .await?;
        debug!("cleared workspace membership cache entry if present");
    } else {
        debug!("no action for workspace membership cache: actor is \"{actor}\"");
    }
    Ok(())
}

async fn cache_workspace_membership(
    workspace_id: Uuid,
    actor: Actor,
    app_db: &mut AppDbClient,
) -> QueryResult<()> {
    if let Actor::User(user_id) = actor {
        WorkspaceMembership::upsert()
            .user_id(user_id)
            .workspace_id(workspace_id)
            .execute(app_db)
            .await?;
        debug!("cached workspace membership");
    } else {
        debug!("no action for workspace membership cache: actor is \"{actor}\"");
    }
    Ok(())
}
