use std::collections::HashSet;

use anyhow::anyhow;
use askama::Template;
use phono_backends::{
    client::WorkspaceClient,
    pg_acl::{PgAclItem, PgPrivilegeType},
    pg_class::PgClass,
    rolnames::{ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER},
};
use serde::{Deserialize, Serialize};
use sqlx::{postgres::types::Oid, prelude::FromRow, query_as};

use crate::errors::AppError;

// TODO: custom error type
// TODO: make params and result references
fn get_table_role(
    relacl: Option<Vec<PgAclItem>>,
    required_privileges: HashSet<PgPrivilegeType>,
    disallowed_privileges: HashSet<PgPrivilegeType>,
    role_prefix: &str,
) -> Result<String, AppError> {
    let mut roles: Vec<String> = vec![];
    dbg!(&relacl);
    for acl_item in relacl.ok_or(anyhow!("acl not present on class"))? {
        if acl_item.grantee.starts_with(role_prefix) {
            let privileges_set: HashSet<PgPrivilegeType> = acl_item
                .privileges
                .iter()
                .map(|privilege| privilege.privilege)
                .collect();
            assert!(
                privileges_set.intersection(&required_privileges).count()
                    == required_privileges.len()
            );
            assert!(privileges_set.intersection(&disallowed_privileges).count() == 0);
            roles.push(acl_item.grantee)
        }
    }
    assert!(roles.len() == 1);
    Ok(roles
        .first()
        .expect("already asserted that `roles` has len 1")
        .clone())
}

/// Returns the name of the "table_reader" role created by Phonograph for a
/// particular workspace table. The role is assessed based on its name and the
/// table permissions directly granted to it. Returns an error if no matching
/// role is found, and panics if a role is found with excess permissions
/// granted to it directly.
pub(crate) fn get_reader_role(rel: &PgClass) -> Result<String, AppError> {
    get_table_role(
        rel.relacl.clone(),
        [PgPrivilegeType::Select].into(),
        [
            PgPrivilegeType::Insert,
            PgPrivilegeType::Update,
            PgPrivilegeType::Delete,
            PgPrivilegeType::Truncate,
        ]
        .into(),
        ROLE_PREFIX_TABLE_READER,
    )
}

/// Returns the name of the "table_writer" role created by Phonograph for a
/// particular workspace table. The role is assessed based on its name and the
/// table permissions directly granted to it. Returns an error if no matching
/// role is found, and panics if a role is found with excess permissions
/// granted to it directly.
pub(crate) fn get_writer_role(rel: &PgClass) -> Result<String, AppError> {
    get_table_role(
        rel.relacl.clone(),
        [PgPrivilegeType::Delete, PgPrivilegeType::Truncate].into(),
        [PgPrivilegeType::Select].into(),
        ROLE_PREFIX_TABLE_WRITER,
    )
}

#[derive(Clone, Debug, Deserialize, Template, Serialize)]
#[template(path = "role_display.html")]
pub(crate) enum RoleDisplay {
    TableOwner { oid: Oid, relname: String },
    TableReader { oid: Oid, relname: String },
    TableWriter { oid: Oid, relname: String },
    Unknown { rolname: String },
}

impl RoleDisplay {
    /// Attempt to infer value from a role name in a specific workspace. If the
    /// role corresponds specifically to a relation and that relation is not
    /// present in the current workspace, the returned value is `None`.
    pub async fn from_rolname(
        rolname: &str,
        client: &mut WorkspaceClient,
    ) -> sqlx::Result<Option<Self>> {
        if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER)
            || rolname.starts_with(ROLE_PREFIX_TABLE_READER)
            || rolname.starts_with(ROLE_PREFIX_TABLE_WRITER)
        {
            #[derive(FromRow)]
            struct RelInfo {
                oid: Oid,
                relname: String,
            }
            // TODO: Consider moving this to [`phono_backends`].
            let mut rels: Vec<RelInfo> = query_as(
                r#"
select oid, any_value(relname) as relname
from (
    select
        oid,
        relname,
        (aclexplode(relacl)).grantee as grantee
    from pg_class
)
where grantee = $1::regrole::oid
group by oid
"#,
            )
            .bind(rolname)
            .fetch_all(client.get_conn())
            .await?;
            assert!(rels.len() <= 1);
            Ok(rels.pop().map(|rel| {
                if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER) {
                    Self::TableOwner {
                        oid: rel.oid,
                        relname: rel.relname,
                    }
                } else if rolname.starts_with(ROLE_PREFIX_TABLE_READER) {
                    Self::TableReader {
                        oid: rel.oid,
                        relname: rel.relname,
                    }
                } else {
                    Self::TableWriter {
                        oid: rel.oid,
                        relname: rel.relname,
                    }
                }
            }))
        } else {
            Ok(Some(Self::Unknown {
                rolname: rolname.to_owned(),
            }))
        }
    }
}
