use std::fmt::Display;

use derive_builder::Builder;
use phono_backends::client::WorkspaceClient;
use phono_models::{
    invite::Invite, invite_rel_permission::RelPermissionKind, service_cred::ServiceCred, user::User,
};

/// Permissions are not always granted to users, but may also be applied to
/// future users (via invites) or service credentials. This enum spans all
/// possible targets to provide a unified representation for
/// [`PermissionsEditor`].
#[derive(Clone, Debug)]
pub(crate) enum Grantee {
    User(User),
    Invite(Invite),
    ServiceCred(ServiceCred),
}

impl Display for Grantee {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::User(user) => write!(f, "usr_{0}", user.id.simple()),
            Self::Invite(invite) => write!(f, "invite_{0}", invite.id.simple()),
            Self::ServiceCred(cred) => write!(f, "svc_{0}", cred.id.simple()),
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("mismatched PermissionsTarget variant")]
pub(crate) struct InvalidVariantError;

impl TryFrom<Grantee> for User {
    type Error = InvalidVariantError;

    fn try_from(value: Grantee) -> Result<Self, Self::Error> {
        if let Grantee::User(user) = value {
            Ok(user)
        } else {
            Err(InvalidVariantError)
        }
    }
}

impl TryFrom<Grantee> for Invite {
    type Error = InvalidVariantError;

    fn try_from(value: Grantee) -> Result<Self, Self::Error> {
        if let Grantee::Invite(invite) = value {
            Ok(invite)
        } else {
            Err(InvalidVariantError)
        }
    }
}

impl TryFrom<Grantee> for ServiceCred {
    type Error = InvalidVariantError;

    fn try_from(value: Grantee) -> Result<Self, Self::Error> {
        if let Grantee::ServiceCred(cred) = value {
            Ok(cred)
        } else {
            Err(InvalidVariantError)
        }
    }
}

#[derive(Builder, Debug)]
#[builder(
    build_fn(error = "QueryError", vis = ""),
    pattern = "owned",
    vis = "pub(crate)"
)]
#[builder_struct_attr(doc = r"
A generalized mechanism for assigning relation permissions to a [`Grantee`].
It will make a best effort to resolve the delta between
`target.current_perms` and `desired_perms`.

Note that when grantee is not an invite, the permissions held by the
provided [`WorkspaceClient`] determine which actual Postgres roles may be
granted or revoked. For this reason, the requested permissions update may
only be partially applied, even if the [`PermissionsUpdateBuilder::execute`]
call succeeds.

The update operation does not double-check the authenticity of
`target.current_perms`, so it is technically valid to include only the
relevant subset of current relation permissions, so long as the difference
between `target.current_perms` and `desired_perms` remains as intended. For
example, if permissions are being granted only, the caller may leave
`target.current_perms` empty. This caller behavior is not recommended, but
it is permitted in the spirit of laziness.
")]
struct PermissionsUpdate<'a> {
    target: Grantee,
    desired_perms: HashSet<RelPermission>,
    grantor_id: Uuid,
    using_app_db: &'a mut AppDbClient,
    using_workspace_client: &'a mut WorkspaceClient,
}

impl<'a> PermissionsUpdateBuilder<'a> {
    pub(crate) async fn execute(self) -> Result<(), AppError> {
        let PermissionsUpdate {
            target,
            desired_perms: roles,
            grantor_id,
            using_app_db: app_db,
            using_workspace_client: workspace_client,
        } = self.build()?;

        let perms_to_revoke: HashSet<_> = target.current_perms.difference(&roles).collect();
        debug!(
            "revoking {roles} from {target}",
            roles = perms_to_revoke
                .iter()
                .map(|role| role.to_string())
                .collect::<Vec<_>>()
                .join(", "),
        );
        let perms_to_grant: HashSet<_> = roles.difference(&target.current_perms).collect();
        debug!(
            "granting {roles} to {target}",
            roles = perms_to_grant
                .iter()
                .map(|role| role.to_string())
                .collect::<Vec<_>>()
                .join(", "),
        );

        match target.kind {
            GranteeKind::Invite(invite) => {
                let ids_to_delete: Vec<Uuid> = InviteRelPermission::belonging_to_invite(invite.id)
                    .fetch_all(app_db)
                    .await?
                    .into_iter()
                    .filter(|invite_perm| {
                        perms_to_revoke.iter().any(|perm| {
                            perm.rel_oid == invite_perm.rel_oid
                                && perm.kind == invite_perm.permission
                        })
                    })
                    .map(|rel_invite| rel_invite.id)
                    .collect();
                debug!("deleting {0} invite rel permissions", ids_to_delete.len());
                InviteRelPermission::delete_by_ids(ids_to_delete)
                    .execute(app_db)
                    .await?;
                for perm in perms_to_grant {
                    debug!("granting {perm}");
                    InviteRelPermission::upsert()
                        .invite_id(invite.id)
                        .rel_oid(perm.rel_oid)
                        .permission(perm.kind)
                        .granted_by(grantor_id)
                        .execute(app_db)
                        .await?;
                }
            }
            GranteeKind::ServiceCred(ServiceCred { id, .. })
            | GranteeKind::User(User { id, .. }) => {
                let target_rolname = match target.kind {
                    GranteeKind::ServiceCred(cred) => cred.rolname.clone(),
                    GranteeKind::User(_) => format!("{ROLE_PREFIX_USER}{id}", id = id.simple()),
                    GranteeKind::Invite(_) => unreachable!("outer match arm excludes variant"),
                };
                for perm in perms_to_revoke {
                    let rel = PgClass::with_oid(perm.rel_oid)
                        .fetch_one(workspace_client)
                        .await?;
                    let target_rolname_esc = escape_identifier(&target_rolname);
                    let perm_rolname = match perm.kind {
                        RelPermissionKind::Owner => get_owner_role(&rel),
                        RelPermissionKind::Reader => get_reader_role(&rel),
                        RelPermissionKind::Writer => get_writer_role(&rel),
                    }?;
                    let perm_rolname_esc = escape_identifier(&perm_rolname);
                    query(&format!(
                        "revoke {perm_rolname_esc} from {target_rolname_esc}"
                    ))
                    .execute(workspace_client.get_conn())
                    .await?;
                }
                for perm in perms_to_grant {
                    let rel = PgClass::with_oid(perm.rel_oid)
                        .fetch_one(workspace_client)
                        .await?;
                    let target_rolname_esc = escape_identifier(&target_rolname);
                    let perm_rolname = match perm.kind {
                        RelPermissionKind::Owner => get_owner_role(&rel),
                        RelPermissionKind::Reader => get_reader_role(&rel),
                        RelPermissionKind::Writer => get_writer_role(&rel),
                    }?;
                    let perm_rolname_esc = escape_identifier(&perm_rolname);
                    query(&format!(
                        "grant {perm_rolname_esc} to {target_rolname_esc} with admin option"
                    ))
                    .execute(workspace_client.get_conn())
                    .await?;
                }
            }
        }

        Ok(())
    }
}
