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

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[serde(rename_all = "lowercase")]
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
pub enum UserRole {
    Guest,
    Host,
    Admin,
}

impl UserRole {
    pub fn as_str(&self) -> &'static str {
        match self {
            UserRole::Guest => "guest",
            UserRole::Host => "host",
            UserRole::Admin => "admin",
        }
    }
}

// Row shape for `user`: every field is populated by sqlx from `SELECT *` / `RETURNING *`.
// `uploads_hidden`, `failed_pin_attempts` and `created_at` are enforced/updated in SQL rather than
// read in Rust, but they are part of the row and stay here so the struct keeps mirroring the table.
#[allow(dead_code)]
#[derive(Debug, sqlx::FromRow)]
pub struct User {
    pub id: Uuid,
    pub event_id: Uuid,
    pub display_name: String,
    pub role: UserRole,
    pub is_banned: bool,
    pub uploads_hidden: bool,
    pub recovery_pin_hash: String,
    pub total_upload_bytes: i64,
    pub failed_pin_attempts: i16,
    pub pin_locked_until: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
}

impl User {
    pub async fn create(
        pool: &PgPool,
        event_id: Uuid,
        display_name: &str,
        pin_hash: &str,
        client_join_id: Option<Uuid>,
    ) -> Result<Self, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "INSERT INTO \"user\" (event_id, display_name, recovery_pin_hash, client_join_id)
             VALUES ($1, $2, $3, $4)
             RETURNING *",
        )
        .bind(event_id)
        .bind(display_name)
        .bind(pin_hash)
        .bind(client_join_id)
        .fetch_one(pool)
        .await
    }

    /// Look up a join that already succeeded, by the idempotency key its client sent.
    ///
    /// The retry path for H16: the account was created but the response never arrived, so the
    /// client re-sends the same `client_join_id`. Finding a row here means "this join already
    /// happened" — the caller rotates the PIN and answers with a usable one rather than 409ing
    /// on a name the caller itself owns.
    pub async fn find_by_client_join_id(
        pool: &PgPool,
        event_id: Uuid,
        client_join_id: Uuid,
    ) -> Result<Option<Self>, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "SELECT * FROM \"user\" WHERE event_id = $1 AND client_join_id = $2",
        )
        .bind(event_id)
        .bind(client_join_id)
        .fetch_optional(pool)
        .await
    }

    /// Create a user with an explicit role, in ONE statement.
    ///
    /// `create` + a separate `UPDATE ... SET role` is not equivalent: a crash or a pool error
    /// between the two leaves a GUEST row holding a reserved name, which is exactly the
    /// poisoned state that bricked admin login — now self-inflicted, and invisible to a
    /// role-based lookup, so the next login would create yet another.
    pub async fn create_with_role(
        pool: &PgPool,
        event_id: Uuid,
        display_name: &str,
        pin_hash: &str,
        role: UserRole,
    ) -> Result<Self, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "INSERT INTO \"user\" (event_id, display_name, recovery_pin_hash, role)
             VALUES ($1, $2, $3, $4)
             RETURNING *",
        )
        .bind(event_id)
        .bind(display_name)
        .bind(pin_hash)
        .bind(role)
        .fetch_one(pool)
        .await
    }

    /// The event's admin, looked up BY ROLE.
    ///
    /// The name is not the identity and never was. Looking the admin up by `display_name`
    /// meant any guest who joined as "Admin" first made the lookup miss, and the fallback
    /// `create` then violated the case-insensitive unique index from migration 007 — a
    /// permanent 500 on admin login, recoverable only by hand-editing the database.
    ///
    /// `ORDER BY created_at` so a database that somehow acquired two admin rows resolves to a
    /// stable one rather than alternating between them.
    pub async fn find_admin_for_event(
        pool: &PgPool,
        event_id: Uuid,
    ) -> Result<Option<Self>, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "SELECT * FROM \"user\" WHERE event_id = $1 AND role = 'admin'
             ORDER BY created_at ASC LIMIT 1",
        )
        .bind(event_id)
        .fetch_optional(pool)
        .await
    }

    pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
        sqlx::query_as::<_, Self>("SELECT * FROM \"user\" WHERE id = $1")
            .bind(id)
            .fetch_optional(pool)
            .await
    }

    pub async fn find_by_event_and_name(
        pool: &PgPool,
        event_id: Uuid,
        display_name: &str,
    ) -> Result<Vec<Self>, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "SELECT * FROM \"user\" WHERE event_id = $1 AND LOWER(display_name) = LOWER($2)",
        )
        .bind(event_id)
        .bind(display_name)
        .fetch_all(pool)
        .await
    }

    pub async fn name_taken(
        pool: &PgPool,
        event_id: Uuid,
        display_name: &str,
    ) -> Result<bool, sqlx::Error> {
        let row: (bool,) = sqlx::query_as(
            "SELECT EXISTS(SELECT 1 FROM \"user\" WHERE event_id = $1 AND LOWER(display_name) = LOWER($2))",
        )
        .bind(event_id)
        .bind(display_name)
        .fetch_one(pool)
        .await?;
        Ok(row.0)
    }

    /// Window after which a failed-PIN streak is forgotten. Matches the lockout duration, so
    /// "wait out the cooldown" and "start clean" are the same interval to a guest.
    const PIN_ATTEMPT_DECAY_MINUTES: i64 = 15;

    /// Record a wrong PIN and return the CURRENT streak length.
    ///
    /// The counter decays: before this, it only ever cleared on a successful recovery or after
    /// a lockout expired, so ordinary typos accumulated across days and a guest could arrive at
    /// an event already most of the way to being locked out by mistakes made the night before.
    /// Decay is what makes the raised lock threshold safe rather than merely lenient.
    pub async fn increment_failed_pin(pool: &PgPool, id: Uuid) -> Result<i16, sqlx::Error> {
        let row: (i16,) = sqlx::query_as(
            "UPDATE \"user\"
             SET failed_pin_attempts = CASE
                     WHEN last_failed_pin_at IS NULL
                       OR last_failed_pin_at < NOW() - ($2 || ' minutes')::interval
                     THEN 1
                     ELSE failed_pin_attempts + 1
                 END,
                 last_failed_pin_at = NOW()
             WHERE id = $1
             RETURNING failed_pin_attempts",
        )
        .bind(id)
        .bind(Self::PIN_ATTEMPT_DECAY_MINUTES.to_string())
        .fetch_one(pool)
        .await?;
        Ok(row.0)
    }

    pub async fn lock_pin(
        pool: &PgPool,
        id: Uuid,
        until: DateTime<Utc>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE \"user\" SET pin_locked_until = $2 WHERE id = $1")
            .bind(id)
            .bind(until)
            .execute(pool)
            .await?;
        Ok(())
    }

    pub async fn reset_pin_attempts(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
        sqlx::query(
            "UPDATE \"user\"
                SET failed_pin_attempts = 0, pin_locked_until = NULL, last_failed_pin_at = NULL
              WHERE id = $1",
        )
        .bind(id)
        .execute(pool)
        .await?;
        Ok(())
    }
}
