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

// Row shape for `session`, populated by sqlx from `RETURNING *`. Session validation is done in SQL
// (expiry/last-seen predicates), so no field is read in Rust — the struct is the row's shape.
#[allow(dead_code)]
#[derive(Debug, sqlx::FromRow)]
pub struct Session {
    pub id: Uuid,
    pub user_id: Uuid,
    pub token_hash: String,
    pub expires_at: DateTime<Utc>,
    pub last_seen_at: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
}

impl Session {
    pub async fn create(
        pool: &PgPool,
        user_id: Uuid,
        token_hash: &str,
        expires_at: DateTime<Utc>,
    ) -> Result<Self, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "INSERT INTO session (user_id, token_hash, expires_at)
             VALUES ($1, $2, $3)
             RETURNING *",
        )
        .bind(user_id)
        .bind(token_hash)
        .bind(expires_at)
        .fetch_one(pool)
        .await
    }

    pub async fn find_by_token_hash(
        pool: &PgPool,
        token_hash: &str,
    ) -> Result<Option<Self>, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "SELECT * FROM session WHERE token_hash = $1 AND expires_at > NOW()",
        )
        .bind(token_hash)
        .fetch_optional(pool)
        .await
    }

    /// Resolve a session token straight to its live user row in one round-trip.
    ///
    /// The auth extractor runs on every authenticated request and used to do two
    /// sequential queries (session lookup, then user lookup); this collapses them into
    /// a single `session JOIN "user"`. The `expires_at` guard mirrors
    /// [`Self::find_by_token_hash`], and the user row is read live (role/ban are never
    /// trusted from the JWT). Returns `None` when the session is missing/expired.
    pub async fn find_user_by_token_hash(
        pool: &PgPool,
        token_hash: &str,
    ) -> Result<Option<crate::models::user::User>, sqlx::Error> {
        sqlx::query_as::<_, crate::models::user::User>(
            "SELECT u.*
             FROM session s
             JOIN \"user\" u ON u.id = s.user_id
             WHERE s.token_hash = $1 AND s.expires_at > NOW()",
        )
        .bind(token_hash)
        .fetch_optional(pool)
        .await
    }

    /// Touch `last_seen_at` AND slide `expires_at` forward by `expiry_days` from now, so
    /// an actively-used session never hits the fixed 30-day cliff (it renews on every
    /// authenticated request). An idle session still expires `expiry_days` after its last
    /// activity. Keyed by token hash so the auth extractor needs no prior id lookup.
    pub async fn touch_and_renew(
        pool: &PgPool,
        token_hash: &str,
        expiry_days: i64,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            "UPDATE session
             SET last_seen_at = NOW(),
                 expires_at = NOW() + ($2 || ' days')::interval
             WHERE token_hash = $1",
        )
        .bind(token_hash)
        .bind(expiry_days.to_string())
        .execute(pool)
        .await?;
        Ok(())
    }

    pub async fn delete_by_token_hash(pool: &PgPool, token_hash: &str) -> Result<(), sqlx::Error> {
        sqlx::query("DELETE FROM session WHERE token_hash = $1")
            .bind(token_hash)
            .execute(pool)
            .await?;
        Ok(())
    }

    /// Revoke every session for a user. Backs "sign out everywhere" and the forced
    /// re-auth after a host PIN-reset or a ban. Returns the number of sessions cleared.
    pub async fn delete_all_for_user(pool: &PgPool, user_id: Uuid) -> Result<u64, sqlx::Error> {
        let r = sqlx::query("DELETE FROM session WHERE user_id = $1")
            .bind(user_id)
            .execute(pool)
            .await?;
        Ok(r.rows_affected())
    }
}
