//! Cross-tenant admin endpoints (`/api/v1/admin/*`).
//!
//! Operator-only. Auth is a **static bearer token** (`ADMIN_API_TOKEN`
//! env on the agent) — explicitly NOT a Keycloak JWT, because the
//! whole point of these endpoints is to operate ACROSS tenants. A
//! customer JWT (which always carries a single tenant_id) has no
//! business mounting them.
//!
//! Routes are only registered when `ADMIN_API_TOKEN` is set. With no
//! token, the endpoints don't exist at all (404), which is a stronger
//! guarantee than "401 if you guess the path".
//!
//! Operations:
//! - `GET    /api/v1/admin/tenants`              — list tenant DBs
//! - `DELETE /api/v1/admin/tenants/{tenant_id}`  — GDPR delete
//!
//! Tenant ids in URLs are passed as-is to `DatabasePool::drop_tenant`,
//! which sanitises them the same way it does for creation. Listing
//! returns the raw DB names from `list_tenant_db_names` — operators
//! can reverse-derive the tenant_id from the prefix.

use axum::extract::{Extension, Path, Request};
use axum::http::{header, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum::Json;
use secrecy::ExposeSecret;
use serde::Serialize;

use super::dto::AgentExt;

#[derive(Serialize)]
pub struct ListTenantDbsResponse {
    pub tenant_db_names: Vec<String>,
}

#[tracing::instrument(skip_all)]
pub async fn list_tenant_dbs(
    Extension(agent): AgentExt,
) -> Result<Json<ListTenantDbsResponse>, StatusCode> {
    let names = agent.db_pool.list_tenant_db_names().await.map_err(|e| {
        tracing::error!("admin: list_tenant_db_names failed: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;
    Ok(Json(ListTenantDbsResponse {
        tenant_db_names: names,
    }))
}

#[tracing::instrument(skip_all, fields(tenant_id = %tenant_id))]
pub async fn drop_tenant_db(
    Extension(agent): AgentExt,
    Path(tenant_id): Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
    agent.db_pool.drop_tenant(&tenant_id).await.map_err(|e| {
        tracing::error!("admin: drop_tenant failed: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;
    Ok(Json(serde_json::json!({ "status": "dropped" })))
}

/// Constant-time-ish comparison of the configured admin token against
/// the incoming bearer. Uses `subtle`-style byte equality so timing
/// attacks can't probe the token character by character.
fn tokens_eq(a: &str, b: &str) -> bool {
    if a.len() != b.len() {
        return false;
    }
    let mut diff = 0u8;
    for (x, y) in a.bytes().zip(b.bytes()) {
        diff |= x ^ y;
    }
    diff == 0
}

/// Middleware enforcing the static `ADMIN_API_TOKEN`. Mounted only on
/// the admin sub-router, so this never runs on customer routes.
pub async fn require_admin_token(
    Extension(agent): AgentExt,
    request: Request,
    next: Next,
) -> Response {
    let Some(expected) = agent.config.admin_api_token.as_ref() else {
        // Belt-and-braces — if the routes were somehow mounted without
        // a token configured, refuse rather than no-op-pass.
        return (StatusCode::NOT_FOUND, "admin disabled").into_response();
    };
    let presented = request
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.strip_prefix("Bearer "))
        .map(|s| s.trim());
    let Some(presented) = presented.filter(|s| !s.is_empty()) else {
        return (StatusCode::UNAUTHORIZED, "Missing bearer token").into_response();
    };
    if !tokens_eq(presented, expected.expose_secret()) {
        return (StatusCode::UNAUTHORIZED, "Invalid admin token").into_response();
    }
    next.run(request).await
}

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

    #[test]
    fn tokens_eq_basic() {
        assert!(tokens_eq("abc", "abc"));
        assert!(!tokens_eq("abc", "abd"));
        assert!(!tokens_eq("abc", "abcd"));
        assert!(!tokens_eq("", "x"));
        assert!(tokens_eq("", ""));
    }
}
