//! Access token blacklist — stores hashes of revoked JWTs until natural expiry.
//!
//! # Schema
//!
//! See `migrations/002_token_blacklist.sql`.

use chrono::{DateTime, Utc};
use sqlx::PgPool;

use crate::hash::sha256_hex;

/// Row from the `token_blacklist` table.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct TokenBlacklist {
    pub id: i64,
    pub token_hash: String,
    pub expires_at: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
}

impl TokenBlacklist {
    /// Blacklist `raw_token`, expiring at `expires_at`.
    ///
    /// Silently no-ops on duplicate (the token may have been blacklisted twice
    /// on a race condition — both calls are idempotent).
    pub async fn insert(
        pool: &PgPool,
        raw_token: &str,
        expires_at: DateTime<Utc>,
    ) -> Result<(), sqlx::Error> {
        let hash = sha256_hex(raw_token);
        sqlx::query(
            "INSERT INTO token_blacklist (token_hash, expires_at)
             VALUES ($1, $2)
             ON CONFLICT (token_hash) DO NOTHING",
        )
        .bind(&hash)
        .bind(expires_at)
        .execute(pool)
        .await?;
        Ok(())
    }

    /// Return `true` if `raw_token`'s hash is in the blacklist and has not yet expired.
    pub async fn is_revoked(pool: &PgPool, raw_token: &str) -> bool {
        let hash = sha256_hex(raw_token);
        sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(
                SELECT 1 FROM token_blacklist
                WHERE token_hash = $1 AND expires_at > now()
             )",
        )
        .bind(&hash)
        .fetch_one(pool)
        .await
        .unwrap_or(false)
    }

    /// Purge expired entries — call periodically (e.g. daily cron or on startup).
    pub async fn purge_expired(pool: &PgPool) -> Result<u64, sqlx::Error> {
        let n = sqlx::query("DELETE FROM token_blacklist WHERE expires_at <= now()")
            .execute(pool)
            .await?
            .rows_affected();
        Ok(n)
    }
}
