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

// Row shape for `comment`: every field is populated by sqlx from `SELECT *` / `RETURNING *`.
// `deleted_at` is not read in Rust today (the soft-delete filter lives in SQL), but it is part of
// the row and stays here so the struct keeps mirroring the table.
#[allow(dead_code)]
#[derive(Debug, sqlx::FromRow)]
pub struct Comment {
    pub id: Uuid,
    pub upload_id: Uuid,
    pub user_id: Uuid,
    pub body: String,
    pub created_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct CommentDto {
    pub id: Uuid,
    pub upload_id: Uuid,
    pub user_id: Uuid,
    pub uploader_name: String,
    pub body: String,
    pub created_at: DateTime<Utc>,
}

impl Comment {
    /// Takes any executor so the caller can insert the comment and link its
    /// hashtags inside a single transaction.
    pub async fn create<'e, E>(
        executor: E,
        upload_id: Uuid,
        user_id: Uuid,
        body: &str,
    ) -> Result<Self, sqlx::Error>
    where
        E: sqlx::PgExecutor<'e>,
    {
        sqlx::query_as::<_, Self>(
            "INSERT INTO comment (upload_id, user_id, body) VALUES ($1, $2, $3) RETURNING *",
        )
        .bind(upload_id)
        .bind(user_id)
        .bind(body)
        .fetch_one(executor)
        .await
    }

    /// Paginated comment listing — returns up to `limit` rows in chronological
    /// order (oldest first). If `before` is set, only comments older than that
    /// timestamp are returned, enabling backward cursor pagination ("load
    /// earlier"). Without the LIMIT a hot post with thousands of comments could
    /// OOM the server on a single GET.
    pub async fn list_for_upload(
        pool: &PgPool,
        upload_id: Uuid,
        before: Option<DateTime<Utc>>,
        limit: i64,
    ) -> Result<Vec<CommentDto>, sqlx::Error> {
        // Two-step: pick the newest `limit` rows older than `before`, then flip
        // them back into ascending order so the caller can render top-to-bottom.
        // `AND NOT u.is_banned` — the filter that was missing (H11).
        //
        // Only `deleted_at` was checked, so a banned guest's comments stayed on the live feed
        // forever: the host bans somebody for an abusive comment, watches every photo of theirs
        // vanish, and the comment is still sitting there on the most-viewed photo of the evening.
        // Nothing on the client evicted them either.
        //
        // The tell that this was an oversight rather than a decision: the EXPORT query already
        // filters `is_banned`, so the comment disappeared from the keepsake but not from the app —
        // the two views of the same moderation action disagreed. Migration 021 did the same for
        // hashtag counts. This brings the live read path in line with both.
        //
        // A ban is reversible and this is derived at read time, so `unban_user` restores the
        // comments with no extra work.
        sqlx::query_as::<_, CommentDto>(
            "SELECT * FROM (
               SELECT c.id, c.upload_id, c.user_id, u.display_name AS uploader_name,
                      c.body, c.created_at
               FROM comment c
               JOIN \"user\" u ON u.id = c.user_id
               WHERE c.upload_id = $1 AND c.deleted_at IS NULL AND NOT u.is_banned
                 AND ($2::timestamptz IS NULL OR c.created_at < $2)
               ORDER BY c.created_at DESC
               LIMIT $3
             ) page
             ORDER BY created_at ASC",
        )
        .bind(upload_id)
        .bind(before)
        .bind(limit)
        .fetch_all(pool)
        .await
    }

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

    /// Event-scoped soft delete. Returns `false` if the comment doesn't exist or belongs to a
    /// different event.
    /// Executor-generic so the delete and the keepsake regeneration can share one transaction
    /// (see `Upload::soft_delete_in_event` for why that must be atomic).
    pub async fn soft_delete_in_event(
        conn: &mut sqlx::PgConnection,
        id: Uuid,
        event_id: Uuid,
    ) -> Result<bool, sqlx::Error> {
        let result = sqlx::query(
            "UPDATE comment
             SET deleted_at = NOW()
             WHERE id = $1
               AND deleted_at IS NULL
               AND upload_id IN (SELECT id FROM upload WHERE event_id = $2)",
        )
        .bind(id)
        .bind(event_id)
        .execute(conn)
        .await?;
        Ok(result.rows_affected() > 0)
    }
}
