//! Bookmarks — owned by a `CurrentUser`. Reads + writes both require
//! auth; the listing endpoint is scoped under `/me/bookmarks` so the
//! URL itself can't be reused to peek at another user's bookmarks.

use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::routing::{delete, get, post};
use axum::{Json, Router};
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;

use crate::api::pagination::PagedResponse;
use crate::app::AppState;
use crate::auth::extractor::CurrentUser;
use crate::crawler::pipeline;
use crate::domain::{Bookmark, BookmarkSummary};
use crate::error::{AppError, AppResult};
use crate::repo;

pub fn routes() -> Router<AppState> {
    Router::new()
        .route("/bookmarks", post(create))
        .route("/bookmarks/:id", delete(delete_one))
        .route("/me/bookmarks", get(list_me))
}

#[derive(Debug, Deserialize)]
pub struct NewBookmark {
    pub manga_id: Uuid,
    #[serde(default)]
    pub chapter_id: Option<Uuid>,
    #[serde(default)]
    pub page: Option<i32>,
}

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

fn default_limit() -> i64 {
    50
}

async fn create(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Json(input): Json<NewBookmark>,
) -> AppResult<(StatusCode, Json<Bookmark>)> {
    // Reject obviously-bad page numbers up front (0-based or negative
    // page indexes were silently accepted before; not exploitable but
    // not what callers mean).
    if let Some(p) = input.page {
        if p < 1 {
            return Err(AppError::ValidationFailed {
                message: "page must be 1 or greater".into(),
                details: json!({ "page": "must be >= 1" }),
            });
        }
    }

    // Surface 404 on a non-existent manga / chapter rather than letting
    // the foreign-key violation collapse into a generic 500.
    repo::manga::get(&state.db, input.manga_id).await?;
    if let Some(chapter_id) = input.chapter_id {
        if !repo::chapter::belongs_to_manga(&state.db, chapter_id, input.manga_id).await? {
            return Err(AppError::NotFound);
        }
    }

    let bookmark = repo::bookmark::create(
        &state.db,
        user.id,
        input.manga_id,
        input.chapter_id,
        input.page,
    )
    .await?;

    // Fire-and-forget: kick off content syncs for any pending chapters of
    // the newly-bookmarked manga. The dedup index makes this idempotent
    // across repeated bookmarks of the same manga; failure here must not
    // surface to the user (the daily cron sweeps anything missed).
    let pool = state.db.clone();
    let manga_id = input.manga_id;
    tokio::spawn(async move {
        match pipeline::enqueue_pending_for_manga(&pool, manga_id).await {
            Ok(summary) => tracing::info!(
                %manga_id,
                inserted = summary.inserted,
                skipped = summary.skipped,
                failed = summary.failed,
                "bookmark hook: enqueued pending chapters"
            ),
            Err(e) => tracing::warn!(
                %manga_id, error = ?e,
                "bookmark hook: enqueue_pending_for_manga failed"
            ),
        }
    });

    Ok((StatusCode::CREATED, Json(bookmark)))
}

async fn delete_one(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(id): Path<Uuid>,
) -> AppResult<StatusCode> {
    match repo::bookmark::find_owner(&state.db, id).await? {
        None => Err(AppError::NotFound),
        Some(owner) if owner != user.id => Err(AppError::Forbidden),
        Some(_) => {
            repo::bookmark::delete(&state.db, id).await?;
            Ok(StatusCode::NO_CONTENT)
        }
    }
}

async fn list_me(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Query(params): Query<ListParams>,
) -> AppResult<Json<PagedResponse<BookmarkSummary>>> {
    let limit = params.limit.clamp(1, 200);
    let offset = params.offset.max(0);
    let (items, total) =
        repo::bookmark::list_for_user(&state.db, user.id, limit, offset).await?;
    Ok(Json(PagedResponse::with_total(items, limit, offset, total)))
}
