//! Per-user reading-progress persistence.

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

use crate::domain::read_progress::{
    ReadProgress, ReadProgressForManga, ReadProgressSummary,
};
use crate::error::{AppError, AppResult};

/// Insert-or-overwrite the user's progress row for this manga.
/// Progress can move backwards (re-reading) — we accept the
/// simplification that the last write wins.
///
/// FK violations (manga or chapter deleted between the handler's
/// existence check and this write) are mapped to `NotFound` so the
/// API returns 404 rather than 500.
pub async fn upsert(
    pool: &PgPool,
    user_id: Uuid,
    manga_id: Uuid,
    chapter_id: Option<Uuid>,
    page: i32,
) -> AppResult<ReadProgress> {
    sqlx::query_as::<_, ReadProgress>(
        r#"
        INSERT INTO read_progress (user_id, manga_id, chapter_id, page, updated_at)
        VALUES ($1, $2, $3, $4, now())
        ON CONFLICT (user_id, manga_id) DO UPDATE
          SET chapter_id = EXCLUDED.chapter_id,
              page       = EXCLUDED.page,
              updated_at = now()
        RETURNING user_id, manga_id, chapter_id, page, updated_at
        "#,
    )
    .bind(user_id)
    .bind(manga_id)
    .bind(chapter_id)
    .bind(page)
    .fetch_one(pool)
    .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),
    })
}

pub async fn get(
    pool: &PgPool,
    user_id: Uuid,
    manga_id: Uuid,
) -> AppResult<ReadProgress> {
    sqlx::query_as::<_, ReadProgress>(
        r#"
        SELECT user_id, manga_id, chapter_id, page, updated_at
          FROM read_progress
         WHERE user_id = $1 AND manga_id = $2
        "#,
    )
    .bind(user_id)
    .bind(manga_id)
    .fetch_optional(pool)
    .await?
    .ok_or(AppError::NotFound)
}

/// Same lookup as `get`, but resolves `chapter_number` in one round-
/// trip so the manga detail page's "Continue reading" CTA can render
/// without having to find the chapter in the paged chapters list.
pub async fn get_for_manga(
    pool: &PgPool,
    user_id: Uuid,
    manga_id: Uuid,
) -> AppResult<ReadProgressForManga> {
    sqlx::query_as::<_, ReadProgressForManga>(
        r#"
        SELECT rp.manga_id,
               rp.chapter_id,
               c.number AS chapter_number,
               rp.page,
               rp.updated_at,
               -- Distinct chapter numbers past the reader's last-read chapter
               -- (see list_for_user for the non-unique-number rationale). 0
               -- when the last-read chapter is unknown.
               (
                   SELECT count(DISTINCT c2.number)
                     FROM chapters c2
                    WHERE c2.manga_id = rp.manga_id
                      AND c.number IS NOT NULL
                      AND c2.number > c.number
               ) AS new_chapters_count
          FROM read_progress rp
          LEFT JOIN chapters c ON c.id = rp.chapter_id
         WHERE rp.user_id = $1 AND rp.manga_id = $2
        "#,
    )
    .bind(user_id)
    .bind(manga_id)
    .fetch_optional(pool)
    .await?
    .ok_or(AppError::NotFound)
}

/// Cross-link guard. Returns true when `chapter_id` belongs to
/// `manga_id`. The upsert handler calls this before writing to refuse
/// PUT bodies that pair a chapter from one manga with another manga
/// — the FK alone can't catch that because both ids resolve
/// individually.
pub async fn chapter_belongs_to_manga(
    pool: &PgPool,
    manga_id: Uuid,
    chapter_id: Uuid,
) -> AppResult<bool> {
    let (matches,): (bool,) = sqlx::query_as(
        r#"
        SELECT EXISTS(
            SELECT 1 FROM chapters
             WHERE id = $1 AND manga_id = $2
        )
        "#,
    )
    .bind(chapter_id)
    .bind(manga_id)
    .fetch_one(pool)
    .await?;
    Ok(matches)
}

pub async fn list_for_user(
    pool: &PgPool,
    user_id: Uuid,
    limit: i64,
    offset: i64,
) -> AppResult<(Vec<ReadProgressSummary>, i64)> {
    let rows = sqlx::query_as::<_, ReadProgressSummary>(
        r#"
        SELECT rp.manga_id,
               m.title             AS manga_title,
               m.cover_image_path  AS manga_cover_image_path,
               rp.chapter_id,
               c.number            AS chapter_number,
               c.page_count        AS chapter_page_count,
               rp.page,
               rp.updated_at,
               -- Personal "new since last read": how many distinct chapter
               -- numbers sit past the reader's last-read chapter.
               -- COUNT(DISTINCT number) — not COUNT(*) — because
               -- (manga_id, number) is non-unique (scanlations share a
               -- number, migration 0013), so raw rows would over-count. When
               -- `c.number` is NULL (manga-level progress or a deleted
               -- chapter) the predicate matches no rows, yielding 0.
               (
                   SELECT count(DISTINCT c2.number)
                     FROM chapters c2
                    WHERE c2.manga_id = rp.manga_id
                      AND c.number IS NOT NULL
                      AND c2.number > c.number
               )                   AS new_chapters_count
          FROM read_progress rp
          JOIN mangas m ON m.id = rp.manga_id
          LEFT JOIN chapters c ON c.id = rp.chapter_id
         WHERE rp.user_id = $1
         ORDER BY rp.updated_at DESC, rp.manga_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 read_progress WHERE user_id = $1")
            .bind(user_id)
            .fetch_one(pool)
            .await?;
    Ok((rows, total))
}

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