//! Collection persistence.
//!
//! Same plain-function pattern as `repo::bookmark`. Ownership is
//! tracked via `collections.user_id`; handlers call `find_owner`
//! before mutations to keep 403/404 honest.

use sqlx::PgPool;
use uuid::Uuid;

use crate::domain::collection::{Collection, CollectionPageItem, CollectionSummary};
use crate::domain::manga::Manga;
use crate::error::{AppError, AppResult};

pub async fn create(
    pool: &PgPool,
    user_id: Uuid,
    name: &str,
    description: Option<&str>,
) -> AppResult<Collection> {
    let row = sqlx::query_as::<_, Collection>(
        r#"
        INSERT INTO collections (user_id, name, description)
        VALUES ($1, $2, $3)
        RETURNING id, user_id, name, description, created_at, updated_at
        "#,
    )
    .bind(user_id)
    .bind(name.trim())
    .bind(description)
    .fetch_one(pool)
    .await
    .map_err(|e| match e {
        sqlx::Error::Database(ref db_err) if db_err.is_unique_violation() => {
            AppError::Conflict("a collection with this name already exists".into())
        }
        other => AppError::Database(other),
    })?;
    Ok(row)
}

pub async fn get(pool: &PgPool, id: Uuid) -> AppResult<Collection> {
    sqlx::query_as::<_, Collection>(
        r#"
        SELECT id, user_id, name, description, created_at, updated_at
        FROM collections
        WHERE id = $1
        "#,
    )
    .bind(id)
    .fetch_optional(pool)
    .await?
    .ok_or(AppError::NotFound)
}

pub async fn find_owner(pool: &PgPool, id: Uuid) -> AppResult<Option<Uuid>> {
    let row: Option<(Uuid,)> =
        sqlx::query_as("SELECT user_id FROM collections WHERE id = $1")
            .bind(id)
            .fetch_optional(pool)
            .await?;
    Ok(row.map(|(u,)| u))
}

/// Paged list of one user's collections. Includes `manga_count` and up
/// to three sample cover image keys (newest-added first) so a card can
/// render without a follow-up fetch.
pub async fn list_for_user(
    pool: &PgPool,
    user_id: Uuid,
    limit: i64,
    offset: i64,
) -> AppResult<(Vec<CollectionSummary>, i64)> {
    let rows = sqlx::query_as::<_, CollectionSummary>(
        r#"
        SELECT
            c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at,
            (SELECT count(*) FROM collection_mangas cm WHERE cm.collection_id = c.id)
                AS manga_count,
            COALESCE(
                (
                    -- `array_agg(... ORDER BY ...)` is the only
                    -- spec-guaranteed way to preserve element order;
                    -- a subquery's ORDER BY isn't a contract the
                    -- outer aggregate has to honour. Adding manga_id
                    -- as a tiebreaker keeps the order stable when
                    -- multiple rows share `added_at` (bulk imports).
                    SELECT array_agg(cover_image_path ORDER BY added_at DESC, manga_id)
                    FROM (
                        SELECT m.cover_image_path, cm2.added_at, cm2.manga_id
                        FROM collection_mangas cm2
                        JOIN mangas m ON m.id = cm2.manga_id
                        WHERE cm2.collection_id = c.id
                          AND m.cover_image_path IS NOT NULL
                        ORDER BY cm2.added_at DESC, cm2.manga_id
                        LIMIT 3
                    ) p
                ),
                ARRAY[]::text[]
            ) AS sample_covers
        FROM collections c
        WHERE c.user_id = $1
        ORDER BY c.updated_at DESC, c.id
        LIMIT $2 OFFSET $3
        "#,
    )
    .bind(user_id)
    .bind(limit)
    .bind(offset)
    .fetch_all(pool)
    .await?;

    let (total,): (i64,) =
        sqlx::query_as("SELECT count(*) FROM collections WHERE user_id = $1")
            .bind(user_id)
            .fetch_one(pool)
            .await?;
    Ok((rows, total))
}

pub async fn update(
    pool: &PgPool,
    id: Uuid,
    name: Option<&str>,
    description_provided: bool,
    description: Option<&str>,
) -> AppResult<Collection> {
    let row = sqlx::query_as::<_, Collection>(
        r#"
        UPDATE collections
           SET name        = COALESCE($2, name),
               description = CASE WHEN $3::boolean THEN $4 ELSE description END,
               updated_at  = now()
         WHERE id = $1
        RETURNING id, user_id, name, description, created_at, updated_at
        "#,
    )
    .bind(id)
    .bind(name.map(str::trim))
    .bind(description_provided)
    .bind(description)
    .fetch_optional(pool)
    .await
    .map_err(|e| match e {
        sqlx::Error::Database(ref db_err) if db_err.is_unique_violation() => {
            AppError::Conflict("a collection with this name already exists".into())
        }
        other => AppError::Database(other),
    })?
    .ok_or(AppError::NotFound)?;
    Ok(row)
}

pub async fn delete(pool: &PgPool, id: Uuid) -> AppResult<()> {
    sqlx::query("DELETE FROM collections WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Add a manga to a collection. Returns `true` if a new attachment was
/// created (handler picks 201), `false` if the manga was already in
/// the collection (handler picks 200). Touches `updated_at` so the
/// "recent collections" sort reflects activity.
///
/// FK violations (manga deleted between the handler's `exists` check
/// and this insert — a race the API can't fully close from the
/// outside) are remapped to `NotFound` so the handler returns 404
/// rather than 500.
pub async fn add_manga(
    pool: &PgPool,
    collection_id: Uuid,
    manga_id: Uuid,
) -> AppResult<bool> {
    let mut tx = pool.begin().await?;
    let inserted = sqlx::query(
        r#"
        INSERT INTO collection_mangas (collection_id, manga_id)
        VALUES ($1, $2)
        ON CONFLICT DO NOTHING
        "#,
    )
    .bind(collection_id)
    .bind(manga_id)
    .execute(&mut *tx)
    .await
    .map_err(|e| match e {
        sqlx::Error::Database(ref db_err) if db_err.is_foreign_key_violation() => {
            AppError::NotFound
        }
        other => AppError::Database(other),
    })?;
    let rows_affected = inserted.rows_affected();
    if rows_affected > 0 {
        sqlx::query("UPDATE collections SET updated_at = now() WHERE id = $1")
            .bind(collection_id)
            .execute(&mut *tx)
            .await?;
    }
    tx.commit().await?;
    Ok(rows_affected > 0)
}

pub async fn remove_manga(
    pool: &PgPool,
    collection_id: Uuid,
    manga_id: Uuid,
) -> AppResult<()> {
    let mut tx = pool.begin().await?;
    let rows_affected = sqlx::query(
        "DELETE FROM collection_mangas WHERE collection_id = $1 AND manga_id = $2",
    )
    .bind(collection_id)
    .bind(manga_id)
    .execute(&mut *tx)
    .await?
    .rows_affected();
    if rows_affected > 0 {
        sqlx::query("UPDATE collections SET updated_at = now() WHERE id = $1")
            .bind(collection_id)
            .execute(&mut *tx)
            .await?;
    }
    tx.commit().await?;
    Ok(())
}

pub async fn list_mangas(
    pool: &PgPool,
    collection_id: Uuid,
    limit: i64,
    offset: i64,
) -> AppResult<(Vec<Manga>, i64)> {
    let rows = sqlx::query_as::<_, Manga>(
        r#"
        SELECT m.id, m.title, m.status, m.alt_titles, m.description,
               m.cover_image_path, m.created_at, m.updated_at
          FROM collection_mangas cm
          JOIN mangas m ON m.id = cm.manga_id
         WHERE cm.collection_id = $1
         ORDER BY cm.added_at DESC, m.id
         LIMIT $2 OFFSET $3
        "#,
    )
    .bind(collection_id)
    .bind(limit)
    .bind(offset)
    .fetch_all(pool)
    .await?;
    let (total,): (i64,) =
        sqlx::query_as("SELECT count(*) FROM collection_mangas WHERE collection_id = $1")
            .bind(collection_id)
            .fetch_one(pool)
            .await?;
    Ok((rows, total))
}

/// Which of `user_id`'s collections currently contain `manga_id`?
/// Used by the "Add to collection" modal to pre-check the boxes.
pub async fn list_collections_containing(
    pool: &PgPool,
    user_id: Uuid,
    manga_id: Uuid,
) -> AppResult<Vec<Uuid>> {
    let rows: Vec<(Uuid,)> = sqlx::query_as(
        r#"
        SELECT c.id
          FROM collections c
          JOIN collection_mangas cm ON cm.collection_id = c.id
         WHERE c.user_id = $1
           AND cm.manga_id = $2
         ORDER BY c.updated_at DESC
        "#,
    )
    .bind(user_id)
    .bind(manga_id)
    .fetch_all(pool)
    .await?;
    Ok(rows.into_iter().map(|(id,)| id).collect())
}

/// Add a page to a collection. Same `(true → 201, false → 200)`
/// idempotency contract as `add_manga`. FK violations (page deleted
/// between the handler's existence check and this insert) surface as
/// `NotFound`, not a 500.
pub async fn add_page(
    pool: &PgPool,
    collection_id: Uuid,
    page_id: Uuid,
) -> AppResult<bool> {
    let mut tx = pool.begin().await?;
    let inserted = sqlx::query(
        r#"
        INSERT INTO collection_pages (collection_id, page_id)
        VALUES ($1, $2)
        ON CONFLICT DO NOTHING
        "#,
    )
    .bind(collection_id)
    .bind(page_id)
    .execute(&mut *tx)
    .await
    .map_err(|e| match e {
        sqlx::Error::Database(ref db_err) if db_err.is_foreign_key_violation() => {
            AppError::NotFound
        }
        other => AppError::Database(other),
    })?;
    let rows_affected = inserted.rows_affected();
    if rows_affected > 0 {
        sqlx::query("UPDATE collections SET updated_at = now() WHERE id = $1")
            .bind(collection_id)
            .execute(&mut *tx)
            .await?;
    }
    tx.commit().await?;
    Ok(rows_affected > 0)
}

pub async fn remove_page(
    pool: &PgPool,
    collection_id: Uuid,
    page_id: Uuid,
) -> AppResult<()> {
    let mut tx = pool.begin().await?;
    let rows_affected = sqlx::query(
        "DELETE FROM collection_pages WHERE collection_id = $1 AND page_id = $2",
    )
    .bind(collection_id)
    .bind(page_id)
    .execute(&mut *tx)
    .await?
    .rows_affected();
    if rows_affected > 0 {
        sqlx::query("UPDATE collections SET updated_at = now() WHERE id = $1")
            .bind(collection_id)
            .execute(&mut *tx)
            .await?;
    }
    tx.commit().await?;
    Ok(())
}

/// Paged list of `collection_id`'s pages, JOINed through chapters and
/// mangas so each row carries the breadcrumb the detail view needs.
pub async fn list_pages(
    pool: &PgPool,
    collection_id: Uuid,
    limit: i64,
    offset: i64,
) -> AppResult<(Vec<CollectionPageItem>, i64)> {
    let rows = sqlx::query_as::<_, CollectionPageItem>(
        r#"
        SELECT
            p.id           AS page_id,
            p.chapter_id   AS chapter_id,
            ch.manga_id    AS manga_id,
            p.page_number  AS page_number,
            ch.number      AS chapter_number,
            ch.title       AS chapter_title,
            m.title        AS manga_title,
            p.storage_key  AS storage_key,
            cp.added_at    AS added_at
          FROM collection_pages cp
          JOIN pages    p  ON p.id = cp.page_id
          JOIN chapters ch ON ch.id = p.chapter_id
          JOIN mangas   m  ON m.id = ch.manga_id
         WHERE cp.collection_id = $1
         ORDER BY cp.added_at DESC, p.id
         LIMIT $2 OFFSET $3
        "#,
    )
    .bind(collection_id)
    .bind(limit)
    .bind(offset)
    .fetch_all(pool)
    .await?;
    let (total,): (i64,) =
        sqlx::query_as("SELECT count(*) FROM collection_pages WHERE collection_id = $1")
            .bind(collection_id)
            .fetch_one(pool)
            .await?;
    Ok((rows, total))
}

/// Which of `user_id`'s collections currently contain `page_id`?
/// Powers the reader context menu's "In N collections" line and the
/// "Add to collection" pre-check.
pub async fn list_collections_containing_page(
    pool: &PgPool,
    user_id: Uuid,
    page_id: Uuid,
) -> AppResult<Vec<Uuid>> {
    let rows: Vec<(Uuid,)> = sqlx::query_as(
        r#"
        SELECT c.id
          FROM collections c
          JOIN collection_pages cp ON cp.collection_id = c.id
         WHERE c.user_id = $1
           AND cp.page_id = $2
         ORDER BY c.updated_at DESC
        "#,
    )
    .bind(user_id)
    .bind(page_id)
    .fetch_all(pool)
    .await?;
    Ok(rows.into_iter().map(|(id,)| id).collect())
}
