use super::*;
use time::OffsetDateTime;

impl PostgresRegistry {
    pub async fn upsert_bootstrap_user(
        &self,
        email: &str,
        display_name: &str,
        password_hash: &str,
    ) -> Result<UserId, RegistryError> {
        let user_id = format!("user_{}", uuid::Uuid::now_v7().simple());
        let row = sqlx::query!(
            "insert into users (
                id,
                email,
                display_name,
                password_hash,
                status,
                created_at
             ) values (
                $1, $2, $3, $4, 'active', now()
             )
             on conflict (email) do update
             set display_name = excluded.display_name,
                 password_hash = excluded.password_hash,
                 status = 'active'
             returning id",
            user_id,
            email,
            display_name,
            password_hash,
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(UserId::new(row.id))
    }

    pub async fn ensure_membership(
        &self,
        workspace_id: &WorkspaceId,
        user_id: &UserId,
        role: MembershipRole,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into memberships (
                workspace_id,
                user_id,
                role,
                created_at
             ) values (
                $1, $2, $3, now()
             )
             on conflict (workspace_id, user_id) do update
             set role = excluded.role",
        )
        .bind(workspace_id.as_str())
        .bind(user_id.as_str())
        .bind(serialize_enum_text(&role, "role")?)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_auth_user_by_email(
        &self,
        email: &str,
    ) -> Result<Option<AuthUserRecord>, RegistryError> {
        let row = sqlx::query!(
            "select
                id,
                email,
                display_name,
                password_hash as \"password_hash!\",
                status,
                created_at as \"created_at!: OffsetDateTime\"
             from users
             where email = $1
             limit 1",
            email,
        )
        .fetch_optional(&self.pool)
        .await?;

        row.map(|row| {
            Ok(AuthUserRecord {
                user: User {
                    id: UserId::new(row.id),
                    email: row.email,
                    display_name: row.display_name,
                    status: deserialize_enum_text(&row.status, "status")?,
                    created_at: row.created_at,
                },
                password_hash: row.password_hash,
            })
        })
        .transpose()
    }

    pub async fn get_auth_user_by_id(
        &self,
        user_id: &UserId,
    ) -> Result<Option<AuthUserRecord>, RegistryError> {
        let row = sqlx::query!(
            "select
                id,
                email,
                display_name,
                password_hash as \"password_hash!\",
                status,
                created_at as \"created_at!: OffsetDateTime\"
             from users
             where id = $1
             limit 1",
            user_id.as_str(),
        )
        .fetch_optional(&self.pool)
        .await?;

        row.map(|row| {
            Ok(AuthUserRecord {
                user: User {
                    id: UserId::new(row.id),
                    email: row.email,
                    display_name: row.display_name,
                    status: deserialize_enum_text(&row.status, "status")?,
                    created_at: row.created_at,
                },
                password_hash: row.password_hash,
            })
        })
        .transpose()
    }

    pub async fn update_user_profile(
        &self,
        user_id: &UserId,
        email: &str,
        display_name: &str,
    ) -> Result<User, RegistryError> {
        let row = sqlx::query!(
            "update users
             set email = $2,
                 display_name = $3
             where id = $1
             returning
                id,
                email,
                display_name,
                status,
                created_at as \"created_at!: OffsetDateTime\"",
            user_id.as_str(),
            email,
            display_name,
        )
        .fetch_one(&self.pool)
        .await
        .map_err(|error| map_user_update_error(error, user_id, email))?;

        build_user(
            row.id,
            row.email,
            row.display_name,
            row.status,
            row.created_at,
        )
    }

    pub async fn update_user_password(
        &self,
        user_id: &UserId,
        password_hash: &str,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update users
             set password_hash = $2
             where id = $1",
        )
        .bind(user_id.as_str())
        .bind(password_hash)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::UserNotFound {
                user_id: user_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn create_user_session(
        &self,
        session_id: &UserSessionId,
        user_id: &UserId,
        current_workspace_id: Option<&WorkspaceId>,
        secret_hash: &str,
        expires_at: &OffsetDateTime,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into user_sessions (
                id,
                user_id,
                current_workspace_id,
                secret_hash,
                status,
                expires_at,
                last_seen_at,
                created_at
             ) values (
                $1,
                $2,
                $3,
                $4,
                'active',
                $5::timestamptz,
                now(),
                now()
             )",
        )
        .bind(session_id.as_str())
        .bind(user_id.as_str())
        .bind(current_workspace_id.map(|id| id.as_str()))
        .bind(secret_hash)
        .bind(*expires_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_user_session(
        &self,
        session_id: &UserSessionId,
        secret_hash: &str,
    ) -> Result<Option<SessionRecord>, RegistryError> {
        let row = sqlx::query!(
            "select
                s.id,
                s.user_id,
                s.current_workspace_id,
                u.email,
                u.display_name,
                u.status,
                u.created_at as \"created_at!: OffsetDateTime\"
             from user_sessions s
             join users u on u.id = s.user_id
             where s.id = $1
               and s.secret_hash = $2
               and s.status = 'active'
               and s.expires_at > now()
             limit 1",
            session_id.as_str(),
            secret_hash,
        )
        .fetch_optional(&self.pool)
        .await?;

        let Some(row) = row else {
            return Ok(None);
        };

        let user_id = UserId::new(row.user_id);
        let user = User {
            id: user_id.clone(),
            email: row.email,
            display_name: row.display_name,
            status: deserialize_enum_text(&row.status, "status")?,
            created_at: row.created_at,
        };
        let memberships = self.list_workspaces_for_user(&user_id).await?;
        let stored_workspace_id = row.current_workspace_id.map(WorkspaceId::new);
        let current_workspace_id = stored_workspace_id
            .filter(|workspace_id| {
                memberships
                    .iter()
                    .any(|membership| membership.workspace.id == *workspace_id)
            })
            .or_else(|| {
                memberships
                    .first()
                    .map(|membership| membership.workspace.id.clone())
            });

        Ok(Some(SessionRecord {
            session_id: UserSessionId::new(row.id),
            user,
            memberships,
            current_workspace_id,
        }))
    }

    pub async fn touch_user_session(
        &self,
        session_id: &UserSessionId,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "update user_sessions
             set last_seen_at = now()
             where id = $1",
        )
        .bind(session_id.as_str())
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn revoke_user_session(
        &self,
        session_id: &UserSessionId,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "update user_sessions
             set status = 'revoked'
             where id = $1",
        )
        .bind(session_id.as_str())
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn set_user_session_current_workspace(
        &self,
        session_id: &UserSessionId,
        workspace_id: &WorkspaceId,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "update user_sessions
             set current_workspace_id = $2
             where id = $1",
        )
        .bind(session_id.as_str())
        .bind(workspace_id.as_str())
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn user_has_workspace_access(
        &self,
        user_id: &UserId,
        workspace_id: &WorkspaceId,
    ) -> Result<bool, RegistryError> {
        let row = sqlx::query!(
            "select exists(
                select 1
                from memberships
                where user_id = $1
                  and workspace_id = $2
            ) as \"allowed!\"",
            user_id.as_str(),
            workspace_id.as_str(),
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(row.allowed)
    }

    pub async fn save_auth_profile(
        &self,
        request: SaveAuthProfileRequest<'_>,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into auth_profiles (
                id,
                workspace_id,
                name,
                kind,
                config_json,
                created_at,
                updated_at
             ) values ($1, $2, $3, $4, $5, $6::timestamptz, $7::timestamptz)
             on conflict(id) do update set
                workspace_id = excluded.workspace_id,
                name = excluded.name,
                kind = excluded.kind,
                config_json = excluded.config_json,
                updated_at = excluded.updated_at",
        )
        .bind(request.profile.id.as_str())
        .bind(request.workspace_id.as_str())
        .bind(&request.profile.name)
        .bind(serialize_enum_text(&request.profile.kind, "kind")?)
        .bind(Json(serialize_json_value(&request.profile.config)?))
        .bind(request.profile.created_at)
        .bind(request.profile.updated_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_auth_profile(
        &self,
        workspace_id: &WorkspaceId,
        auth_profile_id: &crank_core::AuthProfileId,
    ) -> Result<Option<AuthProfile>, RegistryError> {
        let row = sqlx::query!(
            "select
                id,
                workspace_id,
                name,
                kind,
                config_json,
                created_at as \"created_at!: OffsetDateTime\",
                updated_at as \"updated_at!: OffsetDateTime\"
             from auth_profiles
             where workspace_id = $1 and id = $2",
            workspace_id.as_str(),
            auth_profile_id.as_str(),
        )
        .fetch_optional(&self.pool)
        .await?;

        row.map(|row| {
            build_auth_profile(
                row.id,
                row.workspace_id,
                row.name,
                row.kind,
                row.config_json,
                row.created_at,
                row.updated_at,
            )
        })
        .transpose()
    }

    pub async fn list_auth_profiles(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<AuthProfile>, RegistryError> {
        let rows = sqlx::query!(
            "select
                id,
                workspace_id,
                name,
                kind,
                config_json,
                created_at as \"created_at!: OffsetDateTime\",
                updated_at as \"updated_at!: OffsetDateTime\"
             from auth_profiles
             where workspace_id = $1
             order by name asc",
            workspace_id.as_str(),
        )
        .fetch_all(&self.pool)
        .await?;

        rows.into_iter()
            .map(|row| {
                build_auth_profile(
                    row.id,
                    row.workspace_id,
                    row.name,
                    row.kind,
                    row.config_json,
                    row.created_at,
                    row.updated_at,
                )
            })
            .collect()
    }

    pub async fn list_auth_profiles_referencing_secret(
        &self,
        workspace_id: &WorkspaceId,
        secret_id: &SecretId,
    ) -> Result<Vec<AuthProfile>, RegistryError> {
        let profiles = self.list_auth_profiles(workspace_id).await?;

        Ok(profiles
            .into_iter()
            .filter(|profile| {
                profile
                    .config
                    .secret_ids()
                    .into_iter()
                    .any(|candidate| candidate == secret_id)
            })
            .collect())
    }
}
