//! CRUD over `app_user_password_resets` (v1.1.8 commit 6).
//!
//! Same shape and one-shot semantics as `app_user_verification_repo`.
//! Kept as a separate repo so the distinct lifecycles (consume then
//! revoke-sessions for reset; consume then mark-email-verified for
//! verification) stay obvious at the call site. A future v1.2 cleanup
//! may merge them behind a `kind` discriminator if the duplication
//! becomes load-bearing.

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use picloud_shared::{AppId, AppUserId};
use sqlx::PgPool;

#[derive(Debug, thiserror::Error)]
pub enum AppUserPasswordResetRepoError {
    #[error("database error: {0}")]
    Db(#[from] sqlx::Error),
}

#[async_trait]
pub trait AppUserPasswordResetRepo: Send + Sync {
    async fn create(
        &self,
        app_id: AppId,
        user_id: AppUserId,
        token_hash: &str,
        expires_at: DateTime<Utc>,
    ) -> Result<(), AppUserPasswordResetRepoError>;

    /// Atomic consume. Returns `Some(user_id)` on success, `None` on
    /// missing / already-consumed / expired / wrong app.
    async fn consume(
        &self,
        app_id: AppId,
        token_hash: &str,
    ) -> Result<Option<AppUserId>, AppUserPasswordResetRepoError>;

    async fn gc(&self, batch_size: i64) -> Result<u64, AppUserPasswordResetRepoError>;
}

pub struct PostgresAppUserPasswordResetRepo {
    pool: PgPool,
}

impl PostgresAppUserPasswordResetRepo {
    #[must_use]
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

#[async_trait]
impl AppUserPasswordResetRepo for PostgresAppUserPasswordResetRepo {
    async fn create(
        &self,
        app_id: AppId,
        user_id: AppUserId,
        token_hash: &str,
        expires_at: DateTime<Utc>,
    ) -> Result<(), AppUserPasswordResetRepoError> {
        sqlx::query(
            "INSERT INTO app_user_password_resets \
                 (token_hash, app_id, user_id, expires_at) \
             VALUES ($1, $2, $3, $4)",
        )
        .bind(token_hash)
        .bind(app_id.into_inner())
        .bind(user_id.into_inner())
        .bind(expires_at)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    async fn consume(
        &self,
        app_id: AppId,
        token_hash: &str,
    ) -> Result<Option<AppUserId>, AppUserPasswordResetRepoError> {
        let row: Option<(uuid::Uuid,)> = sqlx::query_as(
            "UPDATE app_user_password_resets \
             SET consumed_at = NOW() \
             WHERE token_hash = $1 \
               AND app_id = $2 \
               AND consumed_at IS NULL \
               AND expires_at > NOW() \
             RETURNING user_id",
        )
        .bind(token_hash)
        .bind(app_id.into_inner())
        .fetch_optional(&self.pool)
        .await?;
        Ok(row.map(|(uid,)| uid.into()))
    }

    async fn gc(&self, batch_size: i64) -> Result<u64, AppUserPasswordResetRepoError> {
        let res = sqlx::query(
            "DELETE FROM app_user_password_resets WHERE token_hash IN ( \
                 SELECT token_hash FROM app_user_password_resets \
                 WHERE expires_at <= NOW() OR consumed_at IS NOT NULL \
                 LIMIT $1 \
                 FOR UPDATE SKIP LOCKED \
             )",
        )
        .bind(batch_size)
        .execute(&self.pool)
        .await?;
        Ok(res.rows_affected())
    }
}
