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

use crate::api::pagination::PagedResponse;
use crate::app::AppState;
use crate::auth::extractor::CurrentUser;
use crate::domain::collection::{
    Collection, CollectionPageItem, CollectionPatch, CollectionSummary, NewCollection,
};
use crate::domain::manga::Manga;
use crate::domain::patch::Patch;
use crate::error::{AppError, AppResult};
use crate::repo;

pub fn routes() -> Router<AppState> {
    Router::new()
        .route("/collections", post(create))
        .route("/me/collections", get(list_mine))
        .route("/collections/:id", get(get_one).patch(update).delete(delete_one))
        .route("/collections/:id/mangas", get(list_mangas).post(add_manga))
        .route(
            "/collections/:id/mangas/:manga_id",
            delete(remove_manga),
        )
        .route("/collections/:id/pages", get(list_pages).post(add_page))
        .route("/collections/:id/pages/:page_id", delete(remove_page))
        .route(
            "/mangas/:id/my-collections",
            get(list_my_collections_containing),
        )
        .route(
            "/pages/:id/my-collections",
            get(list_my_collections_containing_page),
        )
}

const MAX_NAME_LEN: usize = 64;
const MAX_DESCRIPTION_LEN: usize = 1024;
const DEFAULT_LIMIT: i64 = 50;

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

fn default_limit() -> i64 {
    DEFAULT_LIMIT
}

#[derive(Debug, Deserialize)]
pub struct AddMangaBody {
    pub manga_id: Uuid,
}

#[derive(Debug, Deserialize)]
pub struct AddPageBody {
    pub page_id: Uuid,
}

#[derive(Debug, Serialize)]
pub struct MangaCollectionIds {
    pub collection_ids: Vec<Uuid>,
}

#[derive(Debug, Serialize)]
pub struct PageCollectionIds {
    pub collection_ids: Vec<Uuid>,
}

fn validate_name(name: &str) -> AppResult<()> {
    let trimmed = name.trim();
    if trimmed.is_empty() {
        return Err(AppError::ValidationFailed {
            message: "name is required".into(),
            details: json!({ "name": "required" }),
        });
    }
    if trimmed.chars().count() > MAX_NAME_LEN {
        return Err(AppError::ValidationFailed {
            message: "name too long".into(),
            details: json!({ "name": format!("max {MAX_NAME_LEN} characters") }),
        });
    }
    Ok(())
}

fn validate_description(desc: Option<&str>) -> AppResult<()> {
    if let Some(d) = desc {
        if d.chars().count() > MAX_DESCRIPTION_LEN {
            return Err(AppError::ValidationFailed {
                message: "description too long".into(),
                details: json!({ "description": format!("max {MAX_DESCRIPTION_LEN} characters") }),
            });
        }
    }
    Ok(())
}

async fn create(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Json(input): Json<NewCollection>,
) -> AppResult<(StatusCode, Json<Collection>)> {
    validate_name(&input.name)?;
    validate_description(input.description.as_deref())?;
    let row = repo::collection::create(
        &state.db,
        user.id,
        &input.name,
        input.description.as_deref(),
    )
    .await?;
    Ok((StatusCode::CREATED, Json(row)))
}

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

async fn get_one(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(id): Path<Uuid>,
) -> AppResult<Json<Collection>> {
    let row = require_owner(&state, user.id, id).await?;
    Ok(Json(row))
}

async fn update(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(id): Path<Uuid>,
    Json(patch): Json<CollectionPatch>,
) -> AppResult<Json<Collection>> {
    require_owner_id(&state, user.id, id).await?;
    if let Some(ref n) = patch.name {
        validate_name(n)?;
    }
    if let Patch::Set(ref d) = patch.description {
        validate_description(Some(d.as_str()))?;
    }
    // Three-state semantics via `Patch<T>`: omitted → Unchanged
    // (column untouched), explicit `null` → Clear (NULL), value → Set.
    let description_provided = patch.description.is_provided();
    let description_value: Option<&str> = match &patch.description {
        Patch::Set(s) => Some(s.as_str()),
        Patch::Clear | Patch::Unchanged => None,
    };
    let updated = repo::collection::update(
        &state.db,
        id,
        patch.name.as_deref(),
        description_provided,
        description_value,
    )
    .await?;
    Ok(Json(updated))
}

async fn delete_one(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(id): Path<Uuid>,
) -> AppResult<StatusCode> {
    require_owner_id(&state, user.id, id).await?;
    repo::collection::delete(&state.db, id).await?;
    Ok(StatusCode::NO_CONTENT)
}

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

async fn add_manga(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(id): Path<Uuid>,
    Json(body): Json<AddMangaBody>,
) -> AppResult<StatusCode> {
    require_owner_id(&state, user.id, id).await?;
    if !repo::manga::exists(&state.db, body.manga_id).await? {
        return Err(AppError::NotFound);
    }
    let created = repo::collection::add_manga(&state.db, id, body.manga_id).await?;
    Ok(if created { StatusCode::CREATED } else { StatusCode::OK })
}

async fn remove_manga(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path((collection_id, manga_id)): Path<(Uuid, Uuid)>,
) -> AppResult<StatusCode> {
    require_owner_id(&state, user.id, collection_id).await?;
    repo::collection::remove_manga(&state.db, collection_id, manga_id).await?;
    Ok(StatusCode::NO_CONTENT)
}

async fn list_my_collections_containing(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(manga_id): Path<Uuid>,
) -> AppResult<Json<MangaCollectionIds>> {
    // No 404 if the manga doesn't exist — the empty list is the
    // correct answer ("you have it in zero of your collections") and
    // keeps the request side-effect-free.
    let ids =
        repo::collection::list_collections_containing(&state.db, user.id, manga_id).await?;
    Ok(Json(MangaCollectionIds { collection_ids: ids }))
}

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

async fn add_page(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(id): Path<Uuid>,
    Json(body): Json<AddPageBody>,
) -> AppResult<StatusCode> {
    require_owner_id(&state, user.id, id).await?;
    // FK violation in `repo::collection::add_page` maps to NotFound, so
    // no separate `repo::page::exists` check is needed — the insert is
    // the existence check.
    let created = repo::collection::add_page(&state.db, id, body.page_id).await?;
    Ok(if created { StatusCode::CREATED } else { StatusCode::OK })
}

async fn remove_page(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path((collection_id, page_id)): Path<(Uuid, Uuid)>,
) -> AppResult<StatusCode> {
    require_owner_id(&state, user.id, collection_id).await?;
    repo::collection::remove_page(&state.db, collection_id, page_id).await?;
    Ok(StatusCode::NO_CONTENT)
}

async fn list_my_collections_containing_page(
    State(state): State<AppState>,
    CurrentUser(user): CurrentUser,
    Path(page_id): Path<Uuid>,
) -> AppResult<Json<PageCollectionIds>> {
    // Mirrors `list_my_collections_containing` for pages: unknown page
    // returns an empty list, not 404 — keeps the endpoint side-effect-
    // free and skips a distinguishing-status oracle.
    let ids = repo::collection::list_collections_containing_page(&state.db, user.id, page_id)
        .await?;
    Ok(Json(PageCollectionIds { collection_ids: ids }))
}

/// Returns the row iff the caller owns it. Both "doesn't exist" and
/// "exists but belongs to someone else" surface as `NotFound` so the
/// API doesn't disclose collection existence to non-owners — the
/// frontend already does this funnelling for URLs, and consistency at
/// the API matters because the same identifiers travel through bots
/// and shared links.
async fn require_owner(
    state: &AppState,
    user_id: Uuid,
    id: Uuid,
) -> AppResult<Collection> {
    match repo::collection::get(&state.db, id).await {
        Ok(row) if row.user_id == user_id => Ok(row),
        // Either the row doesn't exist (NotFound from `get`) or it
        // belongs to someone else — both collapse to NotFound.
        Ok(_) | Err(AppError::NotFound) => Err(AppError::NotFound),
        Err(other) => Err(other),
    }
}

async fn require_owner_id(state: &AppState, user_id: Uuid, id: Uuid) -> AppResult<()> {
    match repo::collection::find_owner(&state.db, id).await? {
        Some(owner) if owner == user_id => Ok(()),
        // Same non-leakage rationale as `require_owner` above.
        _ => Err(AppError::NotFound),
    }
}
