use std::str::FromStr as _;

use chrono::{DateTime, Utc};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use sqlx::{Decode, Postgres, postgres::types::Oid, query, query_as};
use uuid::Uuid;

use crate::{
    client::AppDbClient,
    errors::{QueryError, QueryResult},
};

/// Represents a pending role grant for an email address for which a user
/// account has not yet been created.
///
/// Immediately after a new user account is created, all [`WorkspaceInvite`] and
/// [`RelInvite`] records pertaining to the relevant email address are evaluated
/// and removed.
#[derive(Clone, Debug, PartialEq)]
pub struct RelInvite {
    /// Unique identifier.
    pub id: Uuid,

    /// Email address of the target user (or future user).
    pub email: String,

    /// Workspace on which permissions are to be granted.
    pub workspace_id: Uuid,

    /// OID of the relation on which permissions are to be granted.
    pub rel_oid: Oid,

    /// Type of permission to be granted.
    pub privilege: RelInvitePrivilege,

    /// ID of user who created the invitation. The user must have authority to
    /// grant the relevant permissions at the time that the invite is accepted,
    /// or else the invite will be discarded.
    pub granted_by: Uuid,

    /// Optional timestamp after which this invite will be discarded. This
    /// should typically be set to an appropriate value each time the invitation
    /// is updated (for example, when `granted_by` is changed).
    pub expires_at: Option<DateTime<Utc>>,
}

impl RelInvite {
    pub fn belonging_to_workspace(workspace_id: Uuid) -> BelongingToWorkspaceQuery {
        BelongingToWorkspaceQuery { workspace_id }
    }

    /// Construct a query to fetch an invite by email address. Note that because
    /// [destination email servers have great latitude in how they normalize
    /// addresses (or don't)](
    /// https://www.rfc-editor.org/rfc/rfc5321#section-2.3.11), email addresses
    /// as identifiers are inherently somewhat ambiguous. Effectively all modern
    /// email hosts treat addresses as case-insensitive, consistent with the
    /// standard [treatment of the domain part](
    /// https://www.rfc-editor.org/rfc/rfc1035#section-3.1), and this is the
    /// only form of normalization reflected in the evaluation of this query.
    pub fn belonging_to_email<'a>(email: &'a str) -> BelongingToEmailQuery<'a> {
        BelongingToEmailQuery { email }
    }

    pub fn upsert() -> UpsertBuilder {
        UpsertBuilder::default()
    }

    pub fn delete() -> DeleteBuilder {
        DeleteBuilder::default()
    }
}

#[derive(
    Clone,
    Debug,
    Deserialize,
    Eq,
    Hash,
    PartialEq,
    Serialize,
    strum::Display,
    strum::EnumIs,
    strum::EnumIter,
    strum::EnumString,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum RelInvitePrivilege {
    Owner,
    Reader,
    Writer,
}

impl Decode<'_, Postgres> for RelInvitePrivilege {
    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)?)
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BelongingToWorkspaceQuery {
    workspace_id: Uuid,
}

impl BelongingToWorkspaceQuery {
    /// Specify email address in addition to workspace ID. Note that because
    /// [destination email servers have great latitude in how they normalize
    /// addresses (or don't)](
    /// https://www.rfc-editor.org/rfc/rfc5321#section-2.3.11), email addresses
    /// as identifiers are inherently somewhat ambiguous. Effectively all modern
    /// email hosts treat addresses as case-insensitive, consistent with the
    /// standard [treatment of the domain part](
    /// https://www.rfc-editor.org/rfc/rfc1035#section-3.1), and this is the
    /// only form of normalization reflected in the evaluation of this query.
    pub fn belonging_to_email<'a>(self, email: &'a str) -> BelongingToWorkspaceAndEmailQuery<'a> {
        BelongingToWorkspaceAndEmailQuery {
            workspace_id: self.workspace_id,
            email,
        }
    }

    pub async fn fetch_all(self, app_db: &mut AppDbClient) -> QueryResult<Vec<RelInvite>> {
        Ok(query_as!(
            RelInvite,
            r#"
select
    id,
    email,
    workspace_id,
    rel_oid,
    privilege as "privilege: RelInvitePrivilege",
    granted_by,
    expires_at
from rel_invites
where workspace_id = $1
"#,
            self.workspace_id,
        )
        .fetch_all(app_db.get_conn())
        .await?)
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct BelongingToWorkspaceAndEmailQuery<'a> {
    workspace_id: Uuid,
    email: &'a str,
}

impl<'a> BelongingToWorkspaceAndEmailQuery<'a> {
    pub async fn fetch_all(self, app_db: &mut AppDbClient) -> QueryResult<Vec<RelInvite>> {
        Ok(query_as!(
            RelInvite,
            r#"
select
    id,
    email,
    workspace_id,
    rel_oid,
    privilege as "privilege: RelInvitePrivilege",
    granted_by,
    expires_at
from rel_invites
where workspace_id = $1 and lower(email) = lower($2)
"#,
            self.workspace_id,
            self.email,
        )
        .fetch_all(app_db.get_conn())
        .await?)
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct BelongingToEmailQuery<'a> {
    email: &'a str,
}

impl<'a> BelongingToEmailQuery<'a> {
    pub async fn fetch_all(self, app_db: &mut AppDbClient) -> QueryResult<Vec<RelInvite>> {
        Ok(query_as!(
            RelInvite,
            r#"
select
    id,
    email,
    workspace_id,
    rel_oid,
    privilege as "privilege: RelInvitePrivilege",
    granted_by,
    expires_at
from rel_invites
where lower(email) = lower($1)
"#,
            self.email,
        )
        .fetch_all(app_db.get_conn())
        .await?)
    }
}

#[derive(Builder, Clone, Debug)]
#[builder(
    build_fn(error = "QueryError", vis = ""),
    pattern = "owned",
    vis = "pub"
)]
struct Upsert {
    email: String,

    workspace_id: Uuid,

    rel_oid: Oid,

    privilege: RelInvitePrivilege,

    granted_by: Uuid,

    #[builder(default)]
    expires_at: Option<DateTime<Utc>>,
}

impl UpsertBuilder {
    pub async fn execute(self, app_db: &mut AppDbClient) -> QueryResult<RelInvite> {
        let spec = self.build()?;
        Ok(query_as!(
            RelInvite,
            r#"
insert into rel_invites (
    email,
    workspace_id,
    rel_oid,
    privilege,
    granted_by,
    expires_at
) values (
    $1,
    $2,
    $3,
    $4,
    $5,
    $6
)
on conflict (email, workspace_id, rel_oid, privilege)
    do update set
        granted_by = excluded.granted_by,
        expires_at = excluded.expires_at
returning
    id,
    email,
    workspace_id,
    rel_oid,
    privilege as "privilege: RelInvitePrivilege",
    granted_by,
    expires_at
"#,
            spec.email,
            spec.workspace_id,
            spec.rel_oid,
            spec.privilege.to_string(),
            spec.granted_by,
            spec.expires_at,
        )
        .fetch_one(app_db.get_conn())
        .await?)
    }
}

#[derive(Builder, Clone, Debug)]
#[builder(
    build_fn(error = "QueryError", vis = ""),
    pattern = "owned",
    vis = "pub"
)]
struct Delete {
    #[builder(setter(into))]
    ids: Vec<Uuid>,
}

impl DeleteBuilder {
    pub async fn execute(self, app_db: &mut AppDbClient) -> QueryResult<()> {
        let spec = self.build()?;
        query!(
            "delete from rel_invites where id = any($1)",
            spec.ids.as_slice(),
        )
        .execute(app_db.get_conn())
        .await?;
        Ok(())
    }
}
