//! Email verification — single-use tokens for confirming a user's email address.
//!
//! # Schema
//!
//! See `migrations/003_email_verification.sql`.
//!
//! # Flow
//!
//! 1. On register (or explicit request): call [`EmailVerification::issue`] and
//!    email the returned `plain_token` to the user.
//! 2. User clicks the link: call [`EmailVerification::verify`] with the token.
//! 3. On success, update `users.email_verified_at = now()`.

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

use crate::hash::sha256_hex;

const TOKEN_TTL_HOURS: i64 = 24;

/// A newly-issued verification token.
pub struct IssuedVerification {
    /// Plain-text token — include in the email link, never store.
    pub plain_token: String,
    pub expires_at: DateTime<Utc>,
}

/// Row from `email_verification_tokens`.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct EmailVerification {
    pub id: i64,
    pub user_id: i64,
    pub token_hash: String,
    pub expires_at: DateTime<Utc>,
    pub used_at: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
}

impl EmailVerification {
    /// Generate a new verification token for `user_id`.
    ///
    /// Invalidates any unused tokens for the same user (replace-on-resend).
    pub async fn issue(pool: &PgPool, user_id: i64) -> Result<IssuedVerification, sqlx::Error> {
        let plain_token = generate_token();
        let hash = sha256_hex(&plain_token);
        let expires_at = Utc::now() + Duration::hours(TOKEN_TTL_HOURS);

        // Invalidate previous unused tokens for this user.
        sqlx::query(
            "UPDATE email_verification_tokens
             SET used_at = now()
             WHERE user_id = $1 AND used_at IS NULL",
        )
        .bind(user_id)
        .execute(pool)
        .await?;

        sqlx::query(
            "INSERT INTO email_verification_tokens (user_id, token_hash, expires_at)
             VALUES ($1, $2, $3)",
        )
        .bind(user_id)
        .bind(&hash)
        .bind(expires_at)
        .execute(pool)
        .await?;

        Ok(IssuedVerification {
            plain_token,
            expires_at,
        })
    }

    /// Verify `plain_token`.
    ///
    /// On success, marks the token as used and returns the `user_id`.
    /// Returns `None` when the token is invalid, expired, or already used.
    pub async fn verify(pool: &PgPool, plain_token: &str) -> Result<Option<i64>, sqlx::Error> {
        let hash = sha256_hex(plain_token);
        let row = sqlx::query_as::<_, Self>(
            "SELECT * FROM email_verification_tokens
             WHERE token_hash = $1
               AND used_at IS NULL
               AND expires_at > now()",
        )
        .bind(&hash)
        .fetch_optional(pool)
        .await?;

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

        sqlx::query("UPDATE email_verification_tokens SET used_at = now() WHERE id = $1")
            .bind(row.id)
            .execute(pool)
            .await?;

        // Mark email as verified on the users table.
        sqlx::query("UPDATE users SET email_verified_at = now() WHERE id = $1")
            .bind(row.user_id)
            .execute(pool)
            .await?;

        Ok(Some(row.user_id))
    }

    /// Return `true` if the user with `user_id` has a verified email.
    pub async fn is_verified(pool: &PgPool, user_id: i64) -> bool {
        sqlx::query_scalar::<_, bool>(
            "SELECT email_verified_at IS NOT NULL FROM users WHERE id = $1",
        )
        .bind(user_id)
        .fetch_one(pool)
        .await
        .unwrap_or(false)
    }
}

fn generate_token() -> String {
    use rand::RngCore;
    let mut bytes = [0u8; 32];
    rand::thread_rng().fill_bytes(&mut bytes);
    hex::encode(bytes)
}

// ── hex encoding helper (avoids new dep) ─────────────────────────────────────

mod hex {
    pub fn encode(bytes: impl AsRef<[u8]>) -> String {
        bytes.as_ref().iter().map(|b| format!("{b:02x}")).collect()
    }
}
