use chrono::NaiveDateTime;

use super::*;

impl Database {
    /// Creates a new user.
    pub async fn create_user(
        &self,
        email_address: &str,
        name: Option<&str>,
        admin: bool,
        params: NewUserParams,
    ) -> Result<NewUserResult> {
        self.transaction(|tx| async {
            let tx = tx;
            let user = user::Entity::insert(user::ActiveModel {
                email_address: ActiveValue::set(Some(email_address.into())),
                name: ActiveValue::set(name.map(|s| s.into())),
                github_login: ActiveValue::set(params.github_login.clone()),
                github_user_id: ActiveValue::set(params.github_user_id),
                admin: ActiveValue::set(admin),
                ..Default::default()
            })
            .on_conflict(
                OnConflict::column(user::Column::GithubUserId)
                    .update_columns([
                        user::Column::Admin,
                        user::Column::EmailAddress,
                        user::Column::GithubLogin,
                    ])
                    .to_owned(),
            )
            .exec_with_returning(&*tx)
            .await?;

            Ok(NewUserResult { user_id: user.id })
        })
        .await
    }

    pub async fn update_or_create_user_by_github_account(
        &self,
        github_login: &str,
        github_user_id: i32,
        github_email: Option<&str>,
        github_name: Option<&str>,
        github_user_created_at: DateTimeUtc,
        initial_channel_id: Option<ChannelId>,
    ) -> Result<user::Model> {
        self.transaction(|tx| async move {
            self.update_or_create_user_by_github_account_tx(
                github_login,
                github_user_id,
                github_email,
                github_name,
                github_user_created_at.naive_utc(),
                initial_channel_id,
                &tx,
            )
            .await
        })
        .await
    }

    pub async fn update_or_create_user_by_github_account_tx(
        &self,
        github_login: &str,
        github_user_id: i32,
        github_email: Option<&str>,
        github_name: Option<&str>,
        github_user_created_at: NaiveDateTime,
        initial_channel_id: Option<ChannelId>,
        tx: &DatabaseTransaction,
    ) -> Result<user::Model> {
        if let Some(existing_user) = self
            .get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
            .await?
        {
            let mut existing_user = existing_user.into_active_model();
            existing_user.github_login = ActiveValue::set(github_login.into());
            existing_user.github_user_created_at = ActiveValue::set(Some(github_user_created_at));

            if let Some(github_email) = github_email {
                existing_user.email_address = ActiveValue::set(Some(github_email.into()));
            }

            if let Some(github_name) = github_name {
                existing_user.name = ActiveValue::set(Some(github_name.into()));
            }

            Ok(existing_user.update(tx).await?)
        } else {
            let user = user::Entity::insert(user::ActiveModel {
                email_address: ActiveValue::set(github_email.map(|email| email.into())),
                name: ActiveValue::set(github_name.map(|name| name.into())),
                github_login: ActiveValue::set(github_login.into()),
                github_user_id: ActiveValue::set(github_user_id),
                github_user_created_at: ActiveValue::set(Some(github_user_created_at)),
                admin: ActiveValue::set(false),
                ..Default::default()
            })
            .exec_with_returning(tx)
            .await?;
            if let Some(channel_id) = initial_channel_id {
                channel_member::Entity::insert(channel_member::ActiveModel {
                    id: ActiveValue::NotSet,
                    channel_id: ActiveValue::Set(channel_id),
                    user_id: ActiveValue::Set(user.id),
                    accepted: ActiveValue::Set(true),
                    role: ActiveValue::Set(ChannelRole::Guest),
                })
                .exec(tx)
                .await?;
            }
            Ok(user)
        }
    }

    /// Tries to retrieve a user, first by their GitHub user ID, and then by their GitHub login.
    ///
    /// Returns `None` if a user is not found with this GitHub user ID or GitHub login.
    pub async fn get_user_by_github_user_id_or_github_login(
        &self,
        github_user_id: i32,
        github_login: &str,
        tx: &DatabaseTransaction,
    ) -> Result<Option<user::Model>> {
        if let Some(user_by_github_user_id) = user::Entity::find()
            .filter(user::Column::GithubUserId.eq(github_user_id))
            .one(tx)
            .await?
        {
            return Ok(Some(user_by_github_user_id));
        }

        if let Some(user_by_github_login) = user::Entity::find()
            .filter(user::Column::GithubLogin.eq(github_login))
            .one(tx)
            .await?
        {
            return Ok(Some(user_by_github_login));
        }

        Ok(None)
    }

    /// get_all_users returns the next page of users. To get more call again with
    /// the same limit and the page incremented by 1.
    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<user::Model>> {
        self.transaction(|tx| async move {
            Ok(user::Entity::find()
                .order_by_asc(user::Column::GithubLogin)
                .limit(limit as u64)
                .offset(page as u64 * limit as u64)
                .all(&*tx)
                .await?)
        })
        .await
    }

    /// Sets "connected_once" on the user for analytics.
    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
        self.transaction(|tx| async move {
            user::Entity::update_many()
                .filter(user::Column::Id.eq(id))
                .set(user::ActiveModel {
                    connected_once: ActiveValue::set(connected_once),
                    ..Default::default()
                })
                .exec(&*tx)
                .await?;
            Ok(())
        })
        .await
    }
}
