//! CRUD over the `admin_sessions` table.
//!
//! The token never appears in this module — only its SHA-256 hash. The
//! raw value lives in `auth::GeneratedToken` long enough to hit the
//! cookie and the JSON response, then is forgotten. Lookups also filter
//! expired rows at query time so a delayed prune sweep can never extend
//! a session's life.

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

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

/// Result of a session lookup. Includes the user id (for auth context),
/// the existing `expires_at` (so the middleware can decide whether the
/// sliding-window bump is worth a write), and the `absolute_expires_at`
/// hard cap the bump must clamp at (C1).
#[derive(Debug, Clone)]
pub struct AdminSessionLookup {
    pub user_id: AdminUserId,
    pub expires_at: DateTime<Utc>,
    pub absolute_expires_at: DateTime<Utc>,
}

#[async_trait]
pub trait AdminSessionRepository: Send + Sync {
    async fn create(
        &self,
        user_id: AdminUserId,
        token_hash: &str,
        expires_at: DateTime<Utc>,
        absolute_expires_at: DateTime<Utc>,
    ) -> Result<(), AdminSessionRepositoryError>;
    /// Look up a session by token hash. Returns `None` for missing or
    /// already-expired rows — either the sliding `expires_at` OR the
    /// absolute cap having passed (the query filters both).
    async fn lookup(
        &self,
        token_hash: &str,
    ) -> Result<Option<AdminSessionLookup>, AdminSessionRepositoryError>;
    /// Sliding-window bump. Sets `last_used_at = NOW()` and `expires_at`
    /// to the supplied value.
    async fn touch(
        &self,
        token_hash: &str,
        new_expires_at: DateTime<Utc>,
    ) -> Result<(), AdminSessionRepositoryError>;
    async fn delete(&self, token_hash: &str) -> Result<(), AdminSessionRepositoryError>;
    /// Delete every session belonging to a user. Used when the user is
    /// deactivated or has their password reset out-of-band — both
    /// invalidate all current logins for that account.
    async fn delete_for_user(
        &self,
        user_id: AdminUserId,
    ) -> Result<u64, AdminSessionRepositoryError>;
    /// Sweep expired rows. The auth middleware filters expired rows on
    /// lookup, so this is just bounded-growth hygiene, not correctness.
    async fn prune_expired(&self) -> Result<u64, AdminSessionRepositoryError>;
}

pub struct PostgresAdminSessionRepository {
    pool: PgPool,
}

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

#[async_trait]
impl AdminSessionRepository for PostgresAdminSessionRepository {
    async fn create(
        &self,
        user_id: AdminUserId,
        token_hash: &str,
        expires_at: DateTime<Utc>,
        absolute_expires_at: DateTime<Utc>,
    ) -> Result<(), AdminSessionRepositoryError> {
        sqlx::query(
            "INSERT INTO admin_sessions (token_hash, user_id, expires_at, absolute_expires_at) \
             VALUES ($1, $2, $3, $4)",
        )
        .bind(token_hash)
        .bind(user_id.into_inner())
        .bind(expires_at)
        .bind(absolute_expires_at)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    async fn lookup(
        &self,
        token_hash: &str,
    ) -> Result<Option<AdminSessionLookup>, AdminSessionRepositoryError> {
        let row: Option<(uuid::Uuid, DateTime<Utc>, DateTime<Utc>)> = sqlx::query_as(
            "SELECT user_id, expires_at, absolute_expires_at FROM admin_sessions \
             WHERE token_hash = $1 AND expires_at > NOW() AND absolute_expires_at > NOW()",
        )
        .bind(token_hash)
        .fetch_optional(&self.pool)
        .await?;
        Ok(row.map(|(uid, exp, abs)| AdminSessionLookup {
            user_id: uid.into(),
            expires_at: exp,
            absolute_expires_at: abs,
        }))
    }

    async fn touch(
        &self,
        token_hash: &str,
        new_expires_at: DateTime<Utc>,
    ) -> Result<(), AdminSessionRepositoryError> {
        sqlx::query(
            "UPDATE admin_sessions SET last_used_at = NOW(), expires_at = $2 \
             WHERE token_hash = $1",
        )
        .bind(token_hash)
        .bind(new_expires_at)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    async fn delete(&self, token_hash: &str) -> Result<(), AdminSessionRepositoryError> {
        sqlx::query("DELETE FROM admin_sessions WHERE token_hash = $1")
            .bind(token_hash)
            .execute(&self.pool)
            .await?;
        Ok(())
    }

    async fn delete_for_user(
        &self,
        user_id: AdminUserId,
    ) -> Result<u64, AdminSessionRepositoryError> {
        let res = sqlx::query("DELETE FROM admin_sessions WHERE user_id = $1")
            .bind(user_id.into_inner())
            .execute(&self.pool)
            .await?;
        Ok(res.rows_affected())
    }

    async fn prune_expired(&self) -> Result<u64, AdminSessionRepositoryError> {
        let res = sqlx::query(
            "DELETE FROM admin_sessions \
             WHERE expires_at <= NOW() OR absolute_expires_at <= NOW()",
        )
        .execute(&self.pool)
        .await?;
        Ok(res.rows_affected())
    }
}
