use std::str::FromStr;

use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use sqlx::{Decode, Postgres, query_as};
use strum::EnumString;
use uuid::Uuid;

use crate::client::AppDbClient;

/// Assigns an access control permission on a workspace to a user. These are
/// derived from the permission grants of the workspace's backing database.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct WorkspaceUserPerm {
    /// Primary key (defaults to UUIDv7).
    pub id: Uuid,

    /// Workspace to which the permission belongs.
    pub workspace_id: Uuid,

    /// **Synthesized field** generated by joining to the `workspaces` table.
    pub workspace_name: String,

    /// User to which the permission belongs.
    pub user_id: Uuid,

    /// Permission assigned to the user (currently only "connect").
    pub perm: PermissionValue,
}

impl WorkspaceUserPerm {
    /// Construct a single-field query to fetch workspace permissions assigned
    /// to a user.
    pub fn belonging_to_user(id: Uuid) -> BelongingToUserQuery {
        BelongingToUserQuery { id }
    }

    /// Build an insert statement to create a new object.
    pub fn insert() -> InsertBuilder {
        InsertBuilder::default()
    }
}

#[derive(Clone, Debug)]
pub struct BelongingToUserQuery {
    id: Uuid,
}

impl BelongingToUserQuery {
    pub async fn fetch_all(
        self,
        app_db: &mut AppDbClient,
    ) -> Result<Vec<WorkspaceUserPerm>, sqlx::Error> {
        query_as!(
            WorkspaceUserPerm,
            r#"
select
    p.id as id,
    p.workspace_id as workspace_id,
    p.user_id as user_id,
    p.perm as "perm: PermissionValue",
    w.name as workspace_name
from workspace_user_perms as p
    inner join workspaces as w
        on w.id = p.workspace_id
where p.user_id = $1
"#,
            self.id,
        )
        .fetch_all(app_db.get_conn())
        .await
    }
}

#[derive(Builder, Clone, Debug)]
pub struct Insert {
    workspace_id: Uuid,
    user_id: Uuid,
    perm: PermissionValue,
}

impl Insert {
    pub async fn execute(self, app_db: &mut AppDbClient) -> Result<WorkspaceUserPerm, sqlx::Error> {
        query_as!(
            WorkspaceUserPerm,
            r#"
with p as (
    insert into workspace_user_perms (workspace_id, user_id, perm) values ($1, $2, $3)
    returning
        id,
        workspace_id,
        user_id,
        perm
)
select
    p.id as id,
    p.workspace_id as workspace_id,
    p.user_id as user_id,
    p.perm as "perm: PermissionValue",
    w.name as workspace_name
from workspace_user_perms as p
    inner join workspaces as w
        on w.id = p.workspace_id
"#,
            self.workspace_id,
            self.user_id,
            self.perm.to_string(),
        )
        .fetch_one(app_db.get_conn())
        .await
    }
}

// TODO: The sqlx::Decode derive macro doesn't follow the strum serialization.
// Does sqlx::Encode?
#[derive(Clone, Debug, Deserialize, EnumString, PartialEq, Serialize, strum::Display)]
#[serde(rename = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PermissionValue {
    Connect,
}

impl Decode<'_, Postgres> for PermissionValue {
    fn decode(
        value: <Postgres as sqlx::Database>::ValueRef<'_>,
    ) -> Result<Self, sqlx::error::BoxDynError> {
        let value = <&str as Decode<Postgres>>::decode(value)?;
        Ok(Self::from_str(value)?)
    }
}
