//! HTTP surface for the vault module — Phase 1 ships KV + transit.
//!
//! Plan 17 §S1 (KV v2) and §S2 (transit). Mounted by the engine under
//! `/api/v1/vault/*`. Auth gating in Phase 1 is admin-key-only — every
//! route requires `Authorization: Bearer <admin-key>`. Phase 3+ adds
//! biscuit-share for delegated access and Phase 7 adds the BW-protocol
//! shim's per-user session auth.

use axum::Router;
use axum::extract::FromRef;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};

use assay_auth::state::AdminApiKeys;

use crate::ctx::VaultCtx;

#[cfg(feature = "vault-collections")]
mod collections;
#[cfg(any(
    feature = "vault-dynamic-postgres",
    feature = "vault-dynamic-aws",
    feature = "vault-dynamic-gcp",
    feature = "vault-dynamic-kubernetes",
))]
mod dynamic;
#[cfg(feature = "vault-kv")]
mod kv;
#[cfg(feature = "vault-share")]
mod share;
mod sys;
#[cfg(feature = "vault-transit")]
mod transit;

/// Compose the vault HTTP router. Generic over a parent state from which
/// both [`VaultCtx`] and [`AdminApiKeys`] are extractable via `FromRef`.
/// The engine binary's `EngineState<S>` satisfies both; tests can use a
/// thin parent state — the auth crate's pattern.
pub fn vault_router<S>() -> Router<S>
where
    S: Clone + Send + Sync + 'static,
    VaultCtx: FromRef<S>,
    AdminApiKeys: FromRef<S>,
{
    let mut r = Router::new().merge(sys::router::<S>());
    // BW-compat shim mounts at /identity/* + /api/* (unprefixed); the
    // engine nests THIS router under /api/v1/vault/*. The BW-compat
    // routes therefore appear under /api/v1/vault/identity/* and
    // /api/v1/vault/api/*. The engine's lib.rs additionally mounts
    // the compat router at the top level so stock BW clients (which
    // hardcode /identity and /api) can talk directly.
    #[cfg(feature = "vault-kv")]
    {
        r = r.merge(kv::router::<S>());
    }
    #[cfg(feature = "vault-transit")]
    {
        r = r.merge(transit::router::<S>());
    }
    #[cfg(feature = "vault-collections")]
    {
        r = r.merge(collections::router::<S>());
    }
    #[cfg(feature = "vault-share")]
    {
        r = r.merge(share::router::<S>());
    }
    #[cfg(any(
        feature = "vault-dynamic-postgres",
        feature = "vault-dynamic-aws",
        feature = "vault-dynamic-gcp",
        feature = "vault-dynamic-kubernetes",
    ))]
    {
        r = r.merge(dynamic::router::<S>());
    }
    r
}

/// Top-of-handler admin-key check. Constant-time bytewise compare via
/// [`AdminApiKeys::check`]. Returns `Err(Response)` (axum 401) if the
/// token is absent or invalid; every handler propagates the response
/// verbatim, so the size of the Err variant is intentional — boxing
/// it would force every call site to dereference. Suppress the lint
/// here at the API boundary.
#[allow(clippy::result_large_err)]
pub(crate) fn check_admin(
    headers: &HeaderMap,
    keys: &AdminApiKeys,
) -> std::result::Result<(), Response> {
    let token = headers
        .get(axum::http::header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.strip_prefix("Bearer "));
    if matches!(token, Some(t) if keys.check(t)) {
        return Ok(());
    }
    Err((
        StatusCode::UNAUTHORIZED,
        axum::Json(serde_json::json!({
            "error": "unauthorized",
            "error_description": "missing or invalid Bearer token",
        })),
    )
        .into_response())
}

/// Map a [`crate::error::VaultError`] to an HTTP response. Centralised
/// so KV and transit handlers stay terse.
pub(crate) fn vault_err_to_response(e: crate::error::VaultError) -> Response {
    use crate::error::VaultError as E;
    let (status, code) = match &e {
        E::NotFound => (StatusCode::NOT_FOUND, "not_found"),
        E::Forbidden => (StatusCode::FORBIDDEN, "forbidden"),
        E::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
        E::Invalid(_) => (StatusCode::BAD_REQUEST, "invalid"),
        E::Crypto(_) => (StatusCode::INTERNAL_SERVER_ERROR, "crypto_error"),
        E::Sealed => (StatusCode::SERVICE_UNAVAILABLE, "sealed"),
        E::Backend(_) => (StatusCode::INTERNAL_SERVER_ERROR, "backend_error"),
    };
    (
        status,
        axum::Json(serde_json::json!({
            "error": code,
            "error_description": e.to_string(),
        })),
    )
        .into_response()
}
