//! Public, unauthenticated, read-only surface (`/api/public/**`).
//!
//! Serves only `public` records as a [`PublicView`] — a projection that carries
//! ONLY public-safe fields. The never-public set (location, owner, recorder, dates,
//! and any flexible fields) is excluded by construction: the type lacks those fields,
//! so leaking one here is impossible. Per-field publishability (to surface selected
//! flexible fields) is post-MVP.

use axum::{
    Json, Router,
    extract::{Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::get,
};
use domain::{CatalogueObject, ObjectId};
use serde::Serialize;
use utoipa::ToSchema;

use crate::{AppState, pagination::Pagination};

/// A catalogue object as exposed on the public surface (public-safe fields only).
#[derive(Serialize, ToSchema)]
pub(crate) struct PublicView {
    /// Stable object id (UUID).
    pub id: String,
    pub object_number: String,
    pub object_name: String,
    pub brief_description: Option<String>,
}

impl PublicView {
    fn from_object(object: &CatalogueObject) -> Self {
        PublicView {
            id: object.id.to_string(),
            object_number: object.object_number.clone(),
            object_name: object.object_name.clone(),
            brief_description: object.brief_description.clone(),
        }
    }
}

/// A page of public objects.
#[derive(Serialize, ToSchema)]
pub(crate) struct PublicObjectPage {
    pub items: Vec<PublicView>,
    /// Total number of public objects (independent of paging).
    pub total: i64,
    pub limit: i64,
    pub offset: i64,
}

/// List public objects (paginated).
#[utoipa::path(
    get,
    path = "/api/public/objects",
    params(
        ("limit" = Option<i64>, Query, description = "Max items (1..=200, default 50)"),
        ("offset" = Option<i64>, Query, description = "Items to skip (default 0)")
    ),
    responses((status = 200, body = PublicObjectPage))
)]
pub(crate) async fn list_objects(
    State(state): State<AppState>,
    Query(page): Query<Pagination>,
) -> Result<Json<PublicObjectPage>, StatusCode> {
    let (limit, offset) = (page.limit(), page.offset());

    // `items` and `total` come from two separate queries; under concurrent
    // publish/unpublish they can momentarily disagree by one — acceptable for a
    // public read surface.
    let objects = db::catalog::list_public_objects(state.db.pool(), limit, offset)
        .await
        .map_err(|err| {
            tracing::error!(?err, "listing public objects");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

    let total = db::catalog::count_public_objects(state.db.pool())
        .await
        .map_err(|err| {
            tracing::error!(?err, "counting public objects");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

    Ok(Json(PublicObjectPage {
        items: objects.iter().map(PublicView::from_object).collect(),
        total,
        limit,
        offset,
    }))
}

/// Get one public object by id. Returns 404 if missing OR not public.
#[utoipa::path(
    get,
    path = "/api/public/objects/{id}",
    params(("id" = String, Path, description = "Object id (UUID)")),
    responses(
        (status = 200, body = PublicView),
        (status = 404, description = "No public object with that id")
    )
)]
pub(crate) async fn get_object(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> impl IntoResponse {
    let Ok(object_id) = id.parse::<ObjectId>() else {
        return StatusCode::NOT_FOUND.into_response();
    };

    match db::catalog::public_object_by_id(state.db.pool(), object_id).await {
        Ok(Some(object)) => Json(PublicView::from_object(&object)).into_response(),
        Ok(None) => StatusCode::NOT_FOUND.into_response(),
        Err(err) => {
            tracing::error!(?err, "fetching public object");
            StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
}

/// Public routes, parameterized over [`AppState`].
pub(crate) fn routes() -> Router<AppState> {
    Router::new()
        .route("/api/public/objects", get(list_objects))
        .route("/api/public/objects/{id}", get(get_object))
}
