//! Machine-readable `OpenAPI` 3.1 specification for the tidalDB HTTP API.
//!
//! Two [`OpenApi`](utoipa::OpenApi) documents are derived from the
//! `#[utoipa::path(...)]` attributes on the handlers and the
//! `#[derive(ToSchema)]` on the DTOs:
//!
//! * [`StandaloneApiDoc`] — the data + health surface served by
//!   [`crate::router::build_router`].
//! * [`ClusterApiDoc`] — the same surface PLUS the cluster-management and
//!   sharded (scatter-gather) routes served by
//!   [`crate::cluster::build_cluster_router`].
//!
//! Both are served UNAUTHENTICATED at `GET /openapi.json` (sibling to the
//! `/health/*` probes): the document describes the API contract only — it
//! carries no entity data, signals, or secrets — so gating it behind the same
//! bearer token a client needs the spec to learn how to send would be
//! self-defeating. The `bearerAuth` security scheme is declared via the
//! [`SecurityAddon`] modifier so generated clients know the data routes require
//! a token; the spec endpoint itself is exempt.

use axum::Json;
use utoipa::{
    Modify, OpenApi,
    openapi::security::{Http, HttpAuthScheme, SecurityScheme},
};

/// `GET /openapi.json` handler for the **standalone** surface.
///
/// Mounted unauthenticated alongside the `/health/*` probes by
/// [`crate::router::build_router`]. Returns the derived `OpenAPI` 3.1 document
/// as JSON (`utoipa::openapi::OpenApi` is `Serialize`).
pub(crate) async fn serve_standalone() -> Json<utoipa::openapi::OpenApi> {
    Json(StandaloneApiDoc::openapi())
}

/// `GET /openapi.json` handler for the **cluster** surface — the superset
/// document that also describes the `/cluster/*` and `/sharded/*` routes.
/// Mounted by [`crate::cluster::build_cluster_router`].
pub(crate) async fn serve_cluster() -> Json<utoipa::openapi::OpenApi> {
    Json(ClusterApiDoc::openapi())
}

/// Injects the `bearerAuth` HTTP security scheme into the components.
///
/// The per-handler `security(("bearerAuth" = []))` attributes reference this
/// scheme by name; without registering it here the generated document would
/// name an undefined scheme. Health and `/openapi.json` carry no `security`
/// requirement, so they remain open.
struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        // `components` is always `Some` once any schema is registered, but guard
        // rather than unwrap so an empty-component build degrades gracefully
        // (the crate forbids `unwrap`/`expect` in non-test code).
        let components = openapi.components.get_or_insert_with(Default::default);
        components.add_security_scheme(
            "bearerAuth",
            SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
        );
    }
}

/// `OpenAPI` document for the standalone HTTP surface.
#[derive(OpenApi)]
#[openapi(
    info(
        title = "tidalDB HTTP API",
        version = env!("CARGO_PKG_VERSION"),
        description = "Embeddable, single-node-first database for the personalized \
                       content ranking problem. Write items, embeddings, and signals; \
                       retrieve ranked feeds and run hybrid search. Data routes require \
                       a Bearer token when TIDAL_API_KEY is configured; health probes \
                       and this spec do not.",
    ),
    paths(
        crate::router::health,
        crate::router::create_item,
        crate::router::write_embedding,
        crate::router::write_signal,
        crate::router::feed,
        crate::router::search,
    ),
    components(schemas(
        crate::dto::ItemRequest,
        crate::dto::EmbeddingRequest,
        crate::dto::SignalRequest,
        crate::dto::FeedResponse,
        crate::dto::FeedItem,
        crate::dto::SignalValue,
        crate::dto::SearchResponse,
        crate::dto::SearchItem,
    )),
    modifiers(&SecurityAddon),
    tags(
        (name = "health", description = "Liveness / readiness probes (unauthenticated)."),
        (name = "data", description = "Item, embedding, signal writes and ranked reads."),
    ),
)]
pub struct StandaloneApiDoc;

/// `OpenAPI` document for the cluster HTTP surface.
///
/// Superset of [`StandaloneApiDoc`]: the same data + health routes plus the
/// cluster-management (`/cluster/*`) and sharded scatter-gather
/// (`/sharded/*`) routes. The cluster handlers register the SAME `/items`,
/// `/embeddings`, `/signals`, `/feed`, `/search` paths as the standalone ones;
/// because this is a distinct document the shared paths are described once here
/// from the cluster handlers' attributes (region-aware response text), so there
/// is no path-key collision.
#[derive(OpenApi)]
#[openapi(
    info(
        title = "tidalDB HTTP API",
        version = env!("CARGO_PKG_VERSION"),
        description = "Cluster-mode tidalDB HTTP surface: the standalone data + health \
                       routes plus multi-region cluster management and sharded \
                       scatter-gather routes. EXPERIMENTAL — all regions run in one \
                       process (no host/process isolation).",
    ),
    paths(
        crate::cluster::cluster_status,
        crate::cluster::cluster_promote,
        crate::cluster::cluster_partition,
        crate::cluster::cluster_heal,
        crate::cluster::create_item,
        crate::cluster::write_embedding,
        crate::cluster::write_signal,
        crate::cluster::feed,
        crate::cluster::search,
        crate::cluster::sharded_create_item,
        crate::cluster::sharded_write_embedding,
        crate::cluster::sharded_write_signal,
        crate::cluster::sharded_feed,
        crate::cluster::sharded_search,
    ),
    components(schemas(
        crate::dto::ItemRequest,
        crate::dto::EmbeddingRequest,
        crate::dto::SignalRequest,
        crate::dto::FeedResponse,
        crate::dto::FeedItem,
        crate::dto::SignalValue,
        crate::dto::SearchResponse,
        crate::dto::SearchItem,
        crate::cluster::ClusterStatusResponse,
        crate::cluster::RegionStatus,
        crate::cluster::RegionRequest,
        crate::cluster::ScatterGatherInfo,
        crate::cluster::ShardedFeedResponse,
        crate::cluster::ShardedSearchResponse,
    )),
    modifiers(&SecurityAddon),
    tags(
        (name = "health", description = "Liveness / readiness probes (unauthenticated)."),
        (name = "data", description = "Item, embedding, signal writes and ranked reads."),
        (name = "cluster", description = "Multi-region cluster management."),
        (name = "sharded", description = "Scatter-gather routes fanning across all shards."),
    ),
)]
pub struct ClusterApiDoc;

#[cfg(test)]
mod tests {
    use super::*;

    /// The standalone document must enumerate the data + health paths and carry
    /// the bearerAuth scheme so generated clients know how to authenticate.
    #[test]
    fn standalone_doc_lists_core_paths() {
        let doc = StandaloneApiDoc::openapi();
        let paths = &doc.paths.paths;
        for p in [
            "/health",
            "/items",
            "/embeddings",
            "/signals",
            "/feed",
            "/search",
        ] {
            assert!(paths.contains_key(p), "standalone doc missing path {p}");
        }
        // The cluster-only routes must NOT leak into the standalone document.
        assert!(
            !paths.contains_key("/cluster/status"),
            "standalone doc must not advertise cluster routes"
        );
        let schemes = &doc
            .components
            .as_ref()
            .expect("components present after schema registration")
            .security_schemes;
        assert!(
            schemes.contains_key("bearerAuth"),
            "SecurityAddon must register the bearerAuth scheme"
        );
    }

    /// The cluster document is a superset: data + health + cluster + sharded.
    #[test]
    fn cluster_doc_lists_cluster_and_sharded_paths() {
        let doc = ClusterApiDoc::openapi();
        let paths = &doc.paths.paths;
        for p in [
            "/feed",
            "/cluster/status",
            "/cluster/promote",
            "/cluster/partition",
            "/cluster/heal",
            "/sharded/items",
            "/sharded/feed",
            "/sharded/search",
        ] {
            assert!(paths.contains_key(p), "cluster doc missing path {p}");
        }
    }

    /// The served version string is the crate version, exposed for the route's
    /// contract test and the verification report.
    #[test]
    fn doc_version_is_crate_version() {
        let doc = StandaloneApiDoc::openapi();
        assert_eq!(doc.info.version, env!("CARGO_PKG_VERSION"));
    }
}
