//! Per-agent key issuance endpoints (admin plane).
//!
//! Lets an administrator mint, list, and revoke per-agent `cak_` keys for a
//! managed machine. These keys are the v2 replacement for the shared
//! `AGENT_API_KEY`: each is per-machine, individually revocable, and stored
//! only as a SHA-256 hash.
//!
//! Auth: dashboard JWT + admin role (the [`AdminUser`] extractor). All routes
//! are mounted behind the JWT `auth_layer`, so the extractor has the JWT config
//! and blacklist available.
//!
//! SECURITY:
//! - The plaintext key is returned exactly once, in the create response. It is
//!   never persisted and never logged.
//! - List responses expose only metadata (id, timestamps) — never the hash and
//!   never the plaintext.
//! - Errors returned to clients use the standard envelope and never surface raw
//!   `sqlx`/`anyhow` strings (which can leak schema/host detail).

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;

use crate::auth::{agent_keys, AdminUser};
use crate::db;
use crate::AppState;

/// Standard error envelope (see `.claude/standards/api/response-format.md`).
#[derive(Debug, Serialize)]
pub struct ApiError {
    pub detail: String,
    pub error_code: String,
    pub status_code: u16,
}

impl ApiError {
    fn new(status: StatusCode, code: &str, detail: &str) -> (StatusCode, Json<ApiError>) {
        (
            status,
            Json(ApiError {
                detail: detail.to_string(),
                error_code: code.to_string(),
                status_code: status.as_u16(),
            }),
        )
    }
}

type ApiResult<T> = Result<T, (StatusCode, Json<ApiError>)>;

/// Response for a freshly minted key. `key` is present ONLY here, once.
#[derive(Debug, Serialize)]
pub struct CreatedKey {
    pub id: Uuid,
    pub machine_id: Uuid,
    /// The plaintext `cak_` key. Shown exactly once — store it now; the server
    /// keeps only its hash.
    pub key: String,
    pub created_at: DateTime<Utc>,
}

/// Public metadata for an existing key. Never includes the hash or plaintext.
#[derive(Debug, Serialize)]
pub struct KeyMetadata {
    pub id: Uuid,
    pub machine_id: Uuid,
    pub created_at: DateTime<Utc>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub revoked_at: Option<DateTime<Utc>>,
}

/// Resolve the database handle or a 503 envelope.
fn require_db(state: &AppState) -> ApiResult<&db::Database> {
    state.db.as_ref().ok_or_else(|| {
        ApiError::new(
            StatusCode::SERVICE_UNAVAILABLE,
            "DATABASE_UNAVAILABLE",
            "Database not available",
        )
    })
}

/// Resolve an `agent_id` path segment to the machine's UUID primary key (or a
/// 404 envelope). The route param is `agent_id` (string) to stay consistent
/// with the other `/api/machines/:agent_id` routes — matchit 0.7 panics if the
/// same path position uses two different param names — while the key tables key
/// on the machine UUID.
async fn resolve_machine_id(db: &db::Database, agent_id: &str) -> ApiResult<Uuid> {
    let machine = db::machines::get_machine_by_agent_id(db.pool(), agent_id)
        .await
        .map_err(|e| {
            tracing::error!("DB error resolving machine: {}", e);
            ApiError::new(
                StatusCode::INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR",
                "Internal server error",
            )
        })?
        .ok_or_else(|| {
            ApiError::new(
                StatusCode::NOT_FOUND,
                "MACHINE_NOT_FOUND",
                "Machine not found",
            )
        })?;
    Ok(machine.id)
}

/// POST /api/machines/:agent_id/keys — mint a new per-agent key for a machine.
///
/// `:agent_id` is the machine's `agent_id`; it is resolved to the machine UUID
/// (`connect_machines.id`) that the `connect_agent_keys.machine_id` FK targets.
/// Returns the plaintext key once.
pub async fn create_key(
    AdminUser(admin): AdminUser,
    State(state): State<AppState>,
    Path(agent_id): Path<String>,
) -> ApiResult<(StatusCode, Json<CreatedKey>)> {
    let db = require_db(&state)?;
    let machine_id = resolve_machine_id(db, &agent_id).await?;

    // Mint plaintext + hash. Only the hash is persisted.
    let plaintext = agent_keys::generate_agent_key();
    let key_hash = agent_keys::hash_agent_key(&plaintext);
    let tenant_id = db::tenancy::current_tenant_id();

    let key_id =
        db::agent_keys::insert_agent_key(db.pool(), machine_id, &key_hash, Some(tenant_id))
            .await
            .map_err(|e| {
                tracing::error!("DB error inserting agent key: {}", e);
                ApiError::new(
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "INTERNAL_ERROR",
                    "Failed to create agent key",
                )
            })?;

    // Audit the issuance WITHOUT the key material.
    tracing::info!(
        "Admin {} issued a per-agent key {} for machine {}",
        admin.username,
        key_id,
        machine_id
    );

    Ok((
        StatusCode::CREATED,
        Json(CreatedKey {
            id: key_id,
            machine_id,
            key: plaintext,
            created_at: Utc::now(),
        }),
    ))
}

/// GET /api/machines/:agent_id/keys — list key metadata for a machine.
///
/// Returns only non-secret metadata. Useful for an admin to see which keys
/// exist, when they were last used, and which are revoked.
pub async fn list_keys(
    AdminUser(_admin): AdminUser,
    State(state): State<AppState>,
    Path(agent_id): Path<String>,
) -> ApiResult<Json<Vec<KeyMetadata>>> {
    let db = require_db(&state)?;
    let machine_id = resolve_machine_id(db, &agent_id).await?;

    let keys = db::agent_keys::list_for_machine(db.pool(), machine_id)
        .await
        .map_err(|e| {
            tracing::error!("DB error listing agent keys: {}", e);
            ApiError::new(
                StatusCode::INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR",
                "Failed to list agent keys",
            )
        })?;

    let out = keys
        .into_iter()
        .map(|k| KeyMetadata {
            id: k.id,
            machine_id: k.machine_id,
            created_at: k.created_at,
            last_used_at: k.last_used_at,
            revoked_at: k.revoked_at,
        })
        .collect();

    Ok(Json(out))
}

/// DELETE /api/machines/:agent_id/keys/:key_id — revoke a key.
///
/// Sets `revoked_at`; the key is immediately rejected by agent auth thereafter.
/// `:agent_id` (machine) scopes the revoke so a key id from another machine
/// cannot be revoked via a mismatched URL.
pub async fn revoke_key(
    AdminUser(admin): AdminUser,
    State(state): State<AppState>,
    Path((agent_id, key_id)): Path<(String, Uuid)>,
) -> ApiResult<StatusCode> {
    let db = require_db(&state)?;
    let machine_id = resolve_machine_id(db, &agent_id).await?;

    // Scope the revoke to the path machine so a key id from another machine
    // cannot be revoked via a mismatched URL.
    let owned = db::agent_keys::key_belongs_to_machine(db.pool(), key_id, machine_id)
        .await
        .map_err(|e| {
            tracing::error!("DB error verifying key ownership: {}", e);
            ApiError::new(
                StatusCode::INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR",
                "Internal server error",
            )
        })?;

    if !owned {
        return Err(ApiError::new(
            StatusCode::NOT_FOUND,
            "KEY_NOT_FOUND",
            "Key not found for this machine",
        ));
    }

    db::agent_keys::revoke_agent_key(db.pool(), key_id)
        .await
        .map_err(|e| {
            tracing::error!("DB error revoking agent key: {}", e);
            ApiError::new(
                StatusCode::INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR",
                "Failed to revoke agent key",
            )
        })?;

    tracing::info!(
        "Admin {} revoked per-agent key {} for machine {}",
        admin.username,
        key_id,
        machine_id
    );

    Ok(StatusCode::NO_CONTENT)
}
