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

// Row shape for `upload`: every field is populated by sqlx from `RETURNING *` in `create`. Callers
// mostly use `id` and hand the rest to the compression/feed queries, so most fields are never read
// through this struct — they stay here so it keeps mirroring the table.
#[allow(dead_code)]
#[derive(Debug, sqlx::FromRow)]
pub struct Upload {
    pub id: Uuid,
    pub event_id: Uuid,
    pub user_id: Uuid,
    pub original_path: String,
    pub preview_path: Option<String>,
    pub thumbnail_path: Option<String>,
    pub mime_type: String,
    pub original_size_bytes: i64,
    pub caption: Option<String>,
    pub compression_status: String,
    pub created_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize)]
pub struct UploadDto {
    pub id: Uuid,
    pub user_id: Uuid,
    pub uploader_name: String,
    pub preview_url: Option<String>,
    pub thumbnail_url: Option<String>,
    pub mime_type: String,
    pub caption: Option<String>,
    pub hashtags: Vec<String>,
    pub like_count: i64,
    pub comment_count: i64,
    pub liked_by_me: bool,
    pub created_at: DateTime<Utc>,
}

/// Minimal projection of an upload's on-disk file paths, used by the visibility-gated
/// media aliases so they don't hydrate the entire `Upload` row per request.
#[derive(Debug, sqlx::FromRow)]
pub struct VisibleMedia {
    pub original_path: String,
    pub preview_path: Option<String>,
    pub thumbnail_path: Option<String>,
    pub display_path: Option<String>,
    pub mime_type: String,
}

impl Upload {
    /// Takes any executor so the caller can run it inside a transaction (atomic
    /// quota + insert) or standalone against the pool.
    // Eight arguments, one per column the INSERT writes, with exactly one call site. A params
    // struct here would restate the column list a second time and buy nothing.
    #[allow(clippy::too_many_arguments)]
    pub async fn create<'e, E>(
        executor: E,
        event_id: Uuid,
        user_id: Uuid,
        original_path: &str,
        mime_type: &str,
        original_size_bytes: i64,
        caption: Option<&str>,
        client_upload_id: Option<Uuid>,
    ) -> Result<Option<Self>, sqlx::Error>
    where
        E: sqlx::PgExecutor<'e>,
    {
        // `Ok(None)` means this exact `client_upload_id` is already stored — the caller's request
        // is a retry of one that already succeeded, and it must replay the original row rather
        // than create a second. Letting the unique index raise instead would work, but only after
        // the whole transaction had aborted, and it would arrive as an opaque database error the
        // caller would have to string-match to recognise.
        //
        // The conflict target repeats the index's `WHERE` clause because it is a partial index;
        // without it Postgres cannot prove which index to use and rejects the statement.
        //
        // KEEP THIS IN LOCKSTEP WITH `upload_client_upload_id_key` (migrations 026 and 031). The
        // predicate here must match the index's, or the arbiter cannot be inferred and every
        // upload that carries a `client_upload_id` fails as a runtime 500 — queries in this
        // codebase are not compile-time checked, so nothing catches a drift at build time.
        //
        // `deleted_at IS NULL` is what makes a retry-after-delete work instead of 409ing forever:
        // the key is claimed only while a LIVE row holds it, which is what
        // `find_by_client_upload_id` below has always assumed. `OR taken_down_by_host` carves the
        // moderation case back out — see migration 031: releasing the key for a HOST takedown let
        // a late retry resurrect a photo the host had deliberately removed.
        sqlx::query_as::<_, Self>(
            "INSERT INTO upload (event_id, user_id, original_path, mime_type, original_size_bytes, caption, client_upload_id)
             VALUES ($1, $2, $3, $4, $5, $6, $7)
             ON CONFLICT (client_upload_id) WHERE client_upload_id IS NOT NULL AND (deleted_at IS NULL OR taken_down_by_host) DO NOTHING
             RETURNING *",
        )
        .bind(event_id)
        .bind(user_id)
        .bind(original_path)
        .bind(mime_type)
        .bind(original_size_bytes)
        .bind(caption)
        .bind(client_upload_id)
        .fetch_optional(executor)
        .await
    }

    /// Look up a live upload by the idempotency key its client sent.
    ///
    /// Scoped to the user as well as the key: the key alone is unique, but a lookup that ignored
    /// ownership would let one guest's retry return another guest's row if a key ever repeated.
    /// Soft-deleted rows are excluded on purpose — if the guest deleted the photo and their queue
    /// later retries, they should get a fresh upload rather than a resurrection of a deleted one.
    pub async fn find_by_client_upload_id(
        pool: &sqlx::PgPool,
        user_id: Uuid,
        client_upload_id: Uuid,
    ) -> Result<Option<Self>, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "SELECT * FROM upload
             WHERE client_upload_id = $1 AND user_id = $2 AND deleted_at IS NULL",
        )
        .bind(client_upload_id)
        .bind(user_id)
        .fetch_optional(pool)
        .await
    }

    /// Was this key claimed by a row the HOST took down?
    ///
    /// Only used to answer a refused retry honestly. Without it the guest's queue shows
    /// "Dieser Upload wurde bereits verarbeitet." for a photo that was in fact removed by the
    /// hosts — technically true, actively misleading, and it invites them to try again.
    pub async fn taken_down_by_client_upload_id(
        pool: &sqlx::PgPool,
        user_id: Uuid,
        client_upload_id: Uuid,
    ) -> Result<bool, sqlx::Error> {
        sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS (
                 SELECT 1 FROM upload
                 WHERE client_upload_id = $1 AND user_id = $2
                   AND deleted_at IS NOT NULL AND taken_down_by_host
             )",
        )
        .bind(client_upload_id)
        .bind(user_id)
        .fetch_one(pool)
        .await
    }

    /// Lean lookup for the public media aliases (`get_original`/`get_preview`/
    /// `get_thumbnail`): returns ONLY the file paths + mime for a visible upload —
    /// excluding soft-deleted rows, hidden owners (`uploads_hidden`), and banned owners
    /// (`is_banned`) — the same filter `v_feed` applies. So moderation that removes a post
    /// from the feed also stops its original/preview/thumbnail from being pulled by UUID.
    ///
    /// Selects four columns instead of the whole `Upload` row: this runs once per image
    /// per cache-miss on the media hot path, so we avoid hydrating fields the response
    /// never uses.
    pub async fn find_visible_media(
        pool: &PgPool,
        id: Uuid,
    ) -> Result<Option<VisibleMedia>, sqlx::Error> {
        sqlx::query_as::<_, VisibleMedia>(
            "SELECT up.original_path, up.preview_path, up.thumbnail_path, up.display_path, up.mime_type
             FROM upload up
             JOIN \"user\" u ON u.id = up.user_id
             WHERE up.id = $1 AND up.deleted_at IS NULL
               AND u.uploads_hidden = false AND u.is_banned = false",
        )
        .bind(id)
        .fetch_optional(pool)
        .await
    }

    /// Event-scoped lookup used by host endpoints so a host of event A cannot
    /// reach uploads belonging to event B.
    pub async fn find_by_id_and_event(
        pool: &PgPool,
        id: Uuid,
        event_id: Uuid,
    ) -> Result<Option<Self>, sqlx::Error> {
        sqlx::query_as::<_, Self>(
            "SELECT * FROM upload
             WHERE id = $1 AND event_id = $2 AND deleted_at IS NULL",
        )
        .bind(id)
        .bind(event_id)
        .fetch_optional(pool)
        .await
    }

    pub async fn set_preview_path(
        pool: &PgPool,
        id: Uuid,
        preview_path: &str,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE upload SET preview_path = $2 WHERE id = $1")
            .bind(id)
            .bind(preview_path)
            .execute(pool)
            .await?;
        Ok(())
    }

    pub async fn set_display_path(
        pool: &PgPool,
        id: Uuid,
        display_path: &str,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE upload SET display_path = $2 WHERE id = $1")
            .bind(id)
            .bind(display_path)
            .execute(pool)
            .await?;
        Ok(())
    }

    /// Stamp which revision of the derivative pipeline produced this row's preview/display,
    /// so the startup backfill can find rows generated by an older one exactly once.
    ///
    /// Also clears the attempt counter: success is the only thing that resets it, and folding
    /// the reset in here means both the live path and the backfill get it with no extra call
    /// site to forget.
    pub async fn set_derivatives_rev(pool: &PgPool, id: Uuid, rev: i16) -> Result<(), sqlx::Error> {
        sqlx::query(
            "UPDATE upload
                SET derivatives_rev = $2, derivative_attempts = 0, derivative_last_error = NULL
              WHERE id = $1",
        )
        .bind(id)
        .bind(rev)
        .execute(pool)
        .await?;
        Ok(())
    }

    /// Record that derivative processing is ABOUT to be attempted, returning the new count.
    ///
    /// WRITE-AHEAD ON PURPOSE. The failure this bounds is a cgroup SIGKILL: the process
    /// vanishes mid-work, so no `Err` is returned, no error handler runs and no `Drop` fires.
    /// A counter incremented after a failure would increment zero times per crash and the
    /// boot loop would be unchanged. Counting the ATTEMPT is the only thing that survives the
    /// process dying. The cost is that a genuinely transient failure also burns an attempt —
    /// acceptable, because the retry budget is per-boot-loop, not per-request, and success
    /// resets it to zero.
    /// `None` when the row no longer exists (hard-deleted, or an e2e TRUNCATE landed while the
    /// task waited on the semaphore) — the caller should abandon quietly rather than treat a
    /// missing row as a processing failure.
    pub async fn begin_derivative_attempt(
        pool: &PgPool,
        id: Uuid,
    ) -> Result<Option<i16>, sqlx::Error> {
        sqlx::query_scalar(
            "UPDATE upload
                SET derivative_attempts = derivative_attempts + 1
              WHERE id = $1
          RETURNING derivative_attempts",
        )
        .bind(id)
        .fetch_optional(pool)
        .await
    }

    /// Read the lifetime derivative-attempt counter WITHOUT charging it.
    ///
    /// Used by the in-request retries after the first: those re-enter `do_process` but must not
    /// spend the lifetime budget again (see `charge_lifetime_attempt`). `None` still means the
    /// row vanished, so the caller's "nothing to do" branch keeps working unchanged.
    pub async fn derivative_attempts(pool: &PgPool, id: Uuid) -> Result<Option<i16>, sqlx::Error> {
        sqlx::query_scalar("SELECT derivative_attempts FROM upload WHERE id = $1")
            .bind(id)
            .fetch_optional(pool)
            .await
    }

    /// Store why the last derivative attempt failed. Diagnostics only — nothing branches on it.
    pub async fn record_derivative_failure(
        pool: &PgPool,
        id: Uuid,
        error: &str,
    ) -> Result<(), sqlx::Error> {
        // Bounded: an anyhow chain can be long, and this is written on a failure path that may
        // repeat across every row of a bad batch.
        let truncated: String = error.chars().take(500).collect();
        sqlx::query("UPDATE upload SET derivative_last_error = $2 WHERE id = $1")
            .bind(id)
            .bind(truncated)
            .execute(pool)
            .await?;
        Ok(())
    }

    pub async fn set_thumbnail_path(
        pool: &PgPool,
        id: Uuid,
        thumbnail_path: &str,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE upload SET thumbnail_path = $2 WHERE id = $1")
            .bind(id)
            .bind(thumbnail_path)
            .execute(pool)
            .await?;
        Ok(())
    }

    /// Soft-deletes an upload within its event and refunds the uploader's
    /// `total_upload_bytes`, in one transaction. Returns `false` if no row
    /// matched (already deleted, wrong event, or unknown id) so host handlers
    /// can return a clean 404 instead of silently no-op'ing.
    /// Executor-generic so a caller can run the delete and the keepsake regeneration in ONE
    /// transaction. They must be atomic: if the delete commits and the regeneration doesn't (a
    /// dropped handler future, a failed second tx), the taken-down photo stays in the downloadable
    /// archive forever, and recovery can't tell — the keepsake still looks complete at the current
    /// epoch, and the host can no longer even find the upload to retry.
    ///
    /// `by_host` records WHO removed it, which decides whether the row keeps holding its
    /// idempotency key — see migration 031. A host takedown holds it, so a late retry from the
    /// uploader's queue cannot bring the photo back; a guest deleting their own photo releases it,
    /// so their next upload of the same queue item succeeds.
    pub async fn soft_delete_in_event(
        conn: &mut sqlx::PgConnection,
        id: Uuid,
        event_id: Uuid,
        by_host: bool,
    ) -> Result<bool, sqlx::Error> {
        let tx = conn;
        let row: Option<(Uuid, i64)> = sqlx::query_as(
            "UPDATE upload
             SET deleted_at = NOW(), taken_down_by_host = $3
             WHERE id = $1 AND event_id = $2 AND deleted_at IS NULL
             RETURNING user_id, original_size_bytes",
        )
        .bind(id)
        .bind(event_id)
        .bind(by_host)
        .fetch_optional(&mut *tx)
        .await?;
        let deleted = if let Some((user_id, bytes)) = row {
            sqlx::query(
                "UPDATE \"user\"
                 SET total_upload_bytes = GREATEST(0, total_upload_bytes - $2)
                 WHERE id = $1",
            )
            .bind(user_id)
            .bind(bytes)
            .execute(&mut *tx)
            .await?;
            true
        } else {
            false
        };
        Ok(deleted)
    }

    pub async fn update_caption<'e, E>(
        executor: E,
        id: Uuid,
        caption: Option<&str>,
    ) -> Result<(), sqlx::Error>
    where
        E: sqlx::PgExecutor<'e>,
    {
        sqlx::query("UPDATE upload SET caption = $2 WHERE id = $1")
            .bind(id)
            .bind(caption)
            .execute(executor)
            .await?;
        Ok(())
    }

    pub async fn set_compression_status(
        pool: &PgPool,
        id: Uuid,
        status: &str,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE upload SET compression_status = $2 WHERE id = $1")
            .bind(id)
            .bind(status)
            .execute(pool)
            .await?;
        Ok(())
    }
}
