use derive_builder::Builder;
use sqlx::query_as;
use uuid::Uuid;

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

#[derive(Clone, Debug)]
pub struct User {
    /// Phonograph ID of the user. This is the unique identifier which should be
    /// used in most cases.
    pub id: Uuid,

    /// External ID assigned by the OAuth provider via the OIDC `sub` claim.
    /// This should only be used in connection with authentication. To reference
    /// the user in other contexts, use the `id` field.
    ///
    /// If this field is `None`, it indicates that the user has never signed in.
    /// This may be the case only if the user has been created as a result of
    /// another user inviting an email address to collaborate.
    pub uid: Option<String>,

    /// Email address as provided to Phonograph by the OAuth provider on first
    /// login, lowercased.
    ///
    /// Note that the OAuth provider is expected to require email confirmation
    /// as a prerequisite to authenticating.
    pub email: String,
}

impl User {
    /// Construct an upsert statement. Note that [`User`] follows certain rules
    /// when updating:
    /// - `email` may not be updated after creation.
    /// - `uid` may not be updated from `Some` to `None`.
    ///
    /// These rules are enforced implicitly and will be reflected in the
    /// returned [`User`] object.
    ///
    /// This operation is intended to be called during session authentication or
    /// permissions updates, after performing a query to tentatively check that
    /// a matching record does not already exist. It should not be used for
    /// general updates in other contexts, as there is no other relevant updates
    /// possible.
    pub fn upsert<'a>() -> UpsertBuilder<'a> {
        UpsertBuilder::default()
    }

    /// Construct a query by ID.
    pub fn with_id(id: Uuid) -> WithIdQuery {
        WithIdQuery { id }
    }

    /// Construct a query for users matching any of the given IDs.
    pub fn with_id_in<I: IntoIterator<Item = Uuid>>(ids: I) -> WithIdInQuery {
        let ids: Vec<Uuid> = ids.into_iter().collect();
        WithIdInQuery { ids }
    }

    /// Construct a query by the `uid` field. This should only be used in
    /// connection with authentication.
    pub fn with_uid<'a>(uid: &'a str) -> WithUidQuery<'a> {
        WithUidQuery { uid }
    }

    /// Construct a query to fetch a user 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 with_email<'a>(email: &'a str) -> WithEmailQuery<'a> {
        WithEmailQuery { email }
    }
}

#[derive(Builder, Clone, Debug)]
#[builder(
    build_fn(error = "QueryError", vis = ""),
    pattern = "owned",
    vis = "pub"
)]
struct Upsert<'a> {
    #[builder(default, setter(strip_option))]
    uid: Option<&'a str>,
    email: &'a str,
}

impl<'a> UpsertBuilder<'a> {
    pub async fn execute(self, app_db: &mut AppDbClient) -> QueryResult<User> {
        let Upsert { uid, email } = self.build()?;
        Ok(
            if let Some(user) = query_as!(
                User,
                "
insert into users (uid, email)
values ($1, lower($2))
on conflict do nothing
returning id, uid, email
",
                uid,
                email,
            )
            .fetch_optional(app_db.get_conn())
            .await?
            {
                user
            } else {
                // Conflict may have been on either or both of `email` and
                // `uid`.
                //
                // If the conflict was on `email`, we want to ensure that `uid`
                // is updated if and only if it is currently null. This can be
                // accomplished using `COALESCE()` with the current `uid` value
                // as the first argument. The parameterized value will only be
                // used if the current value is null.
                //
                // If the conflict was on `uid`, we merely want to return the
                // record with the matching `uid`.
                //
                // If both fields conflicted then the `UPDATE` query will be
                // sufficient, but if the operation is using a different email
                // address than the one already stored then we will need one
                // final `SELECT` query to guarantee that we obtain a result.
                // Assuming that the caller queries by `uid` prior to starting
                // the upsert, this last query should only be hit when two HTTP
                // requests are running the auth flow concurrently.
                if let Some(user) = query_as!(
                    User,
                    "
update users
set uid = coalesce(uid, $1)
where email = lower($2)
returning id, uid, email
",
                    uid,
                    email,
                )
                .fetch_optional(app_db.get_conn())
                .await?
                {
                    user
                } else {
                    User::with_uid(
                        // TODO: This should hold true *unless* the database is
                        // "tampered with" by an outside actor (such as a
                        // developer performing manual maintenance). While such
                        // an edge case is beyond the scope of the server's
                        // responsibilities, panicking may be considered an
                        // overreaction.
                        uid.expect("uid must be non-null to cause a database conflict"),
                    )
                    .fetch_optional(app_db)
                    .await?
                    .ok_or(sqlx::Error::RowNotFound)?
                }
            },
        )
    }
}

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

impl WithIdQuery {
    pub async fn fetch_optional(self, app_db: &mut AppDbClient) -> sqlx::Result<Option<User>> {
        query_as!(
            User,
            "select id, uid, email from users where id = $1",
            self.id
        )
        .fetch_optional(app_db.get_conn())
        .await
    }
}

#[derive(Clone, Debug)]
pub struct WithIdInQuery {
    ids: Vec<Uuid>,
}

impl WithIdInQuery {
    pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<User>, sqlx::Error> {
        query_as!(
            User,
            "select id, uid, email from users where id = any($1)",
            self.ids.as_slice()
        )
        .fetch_all(&mut *app_db.conn)
        .await
    }
}

#[derive(Debug)]
pub struct WithUidQuery<'a> {
    uid: &'a str,
}

impl WithUidQuery<'_> {
    pub async fn fetch_optional(self, app_db: &mut AppDbClient) -> QueryResult<Option<User>> {
        Ok(query_as!(
            User,
            "select id, uid, email from users where uid = $1",
            self.uid,
        )
        .fetch_optional(app_db.get_conn())
        .await?)
    }
}

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

impl WithEmailQuery<'_> {
    pub async fn fetch_optional(self, app_db: &mut AppDbClient) -> QueryResult<Option<User>> {
        Ok(query_as!(
            User,
            "select id, uid, email from users where email = lower($1)",
            self.email,
        )
        .fetch_optional(app_db.get_conn())
        .await?)
    }
}
