//! Session persistence. `token_hash` is sha256 of the raw cookie value;
//! the raw value never hits the DB.

use chrono::{DateTime, Utc};
use sqlx::{PgExecutor, PgPool};
use uuid::Uuid;

use crate::domain::Session;
use crate::error::AppResult;

/// Accepts any `PgExecutor` so callers can pass `&PgPool` for simple
/// inserts or `&mut *tx` to run inside a transaction (e.g., password
/// change rotates the user's sessions atomically with the hash UPDATE).
pub async fn create<'e, E: PgExecutor<'e>>(
    executor: E,
    user_id: Uuid,
    token_hash: &[u8],
    expires_at: DateTime<Utc>,
) -> AppResult<Session> {
    let row = sqlx::query_as::<_, Session>(
        r#"
        INSERT INTO sessions (user_id, token_hash, expires_at)
        VALUES ($1, $2, $3)
        RETURNING id, user_id, token_hash, created_at, expires_at
        "#,
    )
    .bind(user_id)
    .bind(token_hash)
    .bind(expires_at)
    .fetch_one(executor)
    .await?;
    Ok(row)
}

pub async fn delete_all_for_user<'e, E: PgExecutor<'e>>(
    executor: E,
    user_id: Uuid,
) -> AppResult<()> {
    sqlx::query("DELETE FROM sessions WHERE user_id = $1")
        .bind(user_id)
        .execute(executor)
        .await?;
    Ok(())
}

/// Returns the session iff `token_hash` matches and it hasn't expired.
pub async fn find_active(pool: &PgPool, token_hash: &[u8]) -> AppResult<Option<Session>> {
    let row = sqlx::query_as::<_, Session>(
        r#"
        SELECT id, user_id, token_hash, created_at, expires_at
        FROM sessions
        WHERE token_hash = $1 AND expires_at > now()
        "#,
    )
    .bind(token_hash)
    .fetch_optional(pool)
    .await?;
    Ok(row)
}

pub async fn delete_by_token_hash(pool: &PgPool, token_hash: &[u8]) -> AppResult<()> {
    sqlx::query("DELETE FROM sessions WHERE token_hash = $1")
        .bind(token_hash)
        .execute(pool)
        .await?;
    Ok(())
}
