//! Admin manga/chapter overview with derived sync state.
//!
//! Sync state comes from `repo::admin_view`, which joins the manga /
//! chapter tables with the crawler signals at query time — there is no
//! persisted sync_state column. See [`repo::admin_view`] for the
//! derivation priority order.

use axum::extract::{Path, Query, State};
use axum::routing::get;
use axum::{Json, Router};
use serde::Deserialize;
use uuid::Uuid;

use crate::api::pagination::PagedResponse;
use crate::app::AppState;
use crate::auth::extractor::RequireAdmin;
use crate::domain::MangaSyncState;
use crate::error::{AppError, AppResult};
use crate::repo;
use crate::repo::admin_view::{AdminChapterRow, AdminMangaRow};

pub fn routes() -> Router<AppState> {
    Router::new()
        .route("/admin/mangas", get(list_mangas))
        .route("/admin/mangas/:id/chapters", get(list_chapters))
}

#[derive(Debug, Deserialize, Default)]
pub struct ListChaptersParams {
    #[serde(default = "default_chapter_limit")]
    pub limit: i64,
    #[serde(default)]
    pub offset: i64,
}

fn default_chapter_limit() -> i64 {
    200
}

#[derive(Debug, Deserialize, Default)]
pub struct ListMangasParams {
    #[serde(default)]
    pub search: Option<String>,
    /// `in_progress` | `dropped` | `synced`. Unrecognised values are a 400.
    #[serde(default)]
    pub sync_state: Option<String>,
    #[serde(default = "default_limit")]
    pub limit: i64,
    #[serde(default)]
    pub offset: i64,
}

fn default_limit() -> i64 {
    50
}

async fn list_mangas(
    State(state): State<AppState>,
    _admin: RequireAdmin,
    Query(params): Query<ListMangasParams>,
) -> AppResult<Json<PagedResponse<AdminMangaRow>>> {
    let limit = params.limit.clamp(1, 200);
    let offset = params.offset.max(0);

    let sync_state = match params.sync_state.as_deref() {
        None | Some("") => None,
        Some("in_progress") => Some(MangaSyncState::InProgress),
        Some("dropped") => Some(MangaSyncState::Dropped),
        Some("synced") => Some(MangaSyncState::Synced),
        Some(other) => {
            return Err(AppError::InvalidInput(format!(
                "sync_state must be one of in_progress|dropped|synced (got {other:?})"
            )));
        }
    };

    let q = repo::admin_view::ListAdminMangasQuery {
        search: params.search.filter(|s| !s.trim().is_empty()),
        sync_state,
        limit,
        offset,
    };
    let (items, total) = repo::admin_view::list_mangas_with_sync_state(&state.db, &q).await?;
    Ok(Json(PagedResponse::with_total(items, limit, offset, total)))
}

async fn list_chapters(
    State(state): State<AppState>,
    _admin: RequireAdmin,
    Path(manga_id): Path<Uuid>,
    Query(params): Query<ListChaptersParams>,
) -> AppResult<Json<PagedResponse<AdminChapterRow>>> {
    // Explicit existence check so a typo / deleted manga returns 404
    // rather than a misleading "no chapters" 200.
    if !repo::manga::exists(&state.db, manga_id).await? {
        return Err(AppError::NotFound);
    }
    // Cap at 500 to bound the per-row scalar-subquery cost on
    // long-runners with thousands of chapters; default 200 covers
    // typical browsing without paging round-trips.
    let limit = params.limit.clamp(1, 500);
    let offset = params.offset.max(0);
    let q = repo::admin_view::ListAdminChaptersQuery {
        manga_id,
        limit,
        offset,
    };
    let (items, total) = repo::admin_view::list_chapters_with_sync_state(&state.db, &q).await?;
    Ok(Json(PagedResponse::with_total(items, limit, offset, total)))
}
