//! Per-page persistence. Mirrors the rows that `pages` holds.

use sqlx::{PgExecutor, PgPool};
use uuid::Uuid;

use crate::domain::Page;
use crate::error::AppResult;

pub async fn create<'e, E: PgExecutor<'e>>(
    executor: E,
    chapter_id: Uuid,
    page_number: i32,
    storage_key: &str,
    content_type: &str,
    size_bytes: i64,
) -> AppResult<Page> {
    let row = sqlx::query_as::<_, Page>(
        r#"
        INSERT INTO pages (chapter_id, page_number, storage_key, content_type, size_bytes)
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id, chapter_id, page_number, storage_key, content_type
        "#,
    )
    .bind(chapter_id)
    .bind(page_number)
    .bind(storage_key)
    .bind(content_type)
    .bind(size_bytes)
    .fetch_one(executor)
    .await?;
    Ok(row)
}

/// Fetch a single page by id. `None` when it doesn't exist (e.g. deleted
/// between an analysis job's enqueue and its dispatch).
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> AppResult<Option<Page>> {
    let row = sqlx::query_as::<_, Page>(
        "SELECT id, chapter_id, page_number, storage_key, content_type \
           FROM pages WHERE id = $1",
    )
    .bind(id)
    .fetch_optional(pool)
    .await?;
    Ok(row)
}

/// Resolve a page's breadcrumb `(manga_id, chapter_id, page_number)` for
/// live event payloads. `None` if the page doesn't exist.
pub async fn locate(
    pool: &PgPool,
    page_id: Uuid,
) -> AppResult<Option<(Uuid, Uuid, i32)>> {
    let row: Option<(Uuid, Uuid, i32)> = sqlx::query_as(
        "SELECT c.manga_id, p.chapter_id, p.page_number \
           FROM pages p JOIN chapters c ON c.id = p.chapter_id WHERE p.id = $1",
    )
    .bind(page_id)
    .fetch_optional(pool)
    .await?;
    Ok(row)
}

/// Like [`locate`] but also resolves the manga title and chapter number, so
/// live analysis events can carry human labels for the "now analyzing"
/// banner without the dashboard having to look them up. Returns
/// `(manga_id, manga_title, chapter_id, chapter_number, page_number)`.
pub async fn locate_labeled(
    pool: &PgPool,
    page_id: Uuid,
) -> AppResult<Option<(Uuid, String, Uuid, i32, i32)>> {
    let row: Option<(Uuid, String, Uuid, i32, i32)> = sqlx::query_as(
        "SELECT c.manga_id, m.title, p.chapter_id, c.number, p.page_number \
           FROM pages p \
           JOIN chapters c ON c.id = p.chapter_id \
           JOIN mangas m   ON m.id = c.manga_id \
          WHERE p.id = $1",
    )
    .bind(page_id)
    .fetch_optional(pool)
    .await?;
    Ok(row)
}

pub async fn list_for_chapter(pool: &PgPool, chapter_id: Uuid) -> AppResult<Vec<Page>> {
    let rows = sqlx::query_as::<_, Page>(
        r#"
        SELECT id, chapter_id, page_number, storage_key, content_type
        FROM pages
        WHERE chapter_id = $1
        ORDER BY page_number ASC
        "#,
    )
    .bind(chapter_id)
    .fetch_all(pool)
    .await?;
    Ok(rows)
}
