//! `/api/v1/admin/api-keys/*` — bearer API key CRUD (blueprint §11.6).
//!
//! All endpoints are guarded by `require_authenticated`. Capability
//! checks: none — every authenticated user manages **their own** keys.
//! The repo enforces caller ownership on `delete`, and `list` is
//! scoped to the caller's user_id. No instance-level authority is
//! exposed (no listing other users' keys, no admin-issued keys for
//! another user — those flows belong with the invite system).
//!
//! Mint semantics:
//!   * raw token is returned **exactly once** in the POST response and
//!     never logged. Lose it = mint a new key.
//!   * `app_id` (optional) binds the key to one app; capability checks
//!     deny every `App*(other_app)`.
//!   * scopes containing `instance:*` are rejected when `app_id` is
//!     set — the combination is irreconcilable.

use std::sync::Arc;

use axum::extract::{Path, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Json, Response};
use axum::routing::{delete, get};
use axum::{Extension, Router};
use chrono::{DateTime, Utc};
use picloud_shared::{ApiKeyId, AppId, Principal, Scope};
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::api_key_repo::{ApiKeyRepository, ApiKeyRepositoryError, ApiKeyRow, NewApiKey};
use crate::auth::generate_api_key;

/// Validation bounds for the user-supplied `name` field — keeps the
/// dashboard's list view tidy and rejects accidental whole-token
/// pastes.
const NAME_MIN: usize = 1;
const NAME_MAX: usize = 64;

#[derive(Clone)]
pub struct ApiKeysState {
    pub keys: Arc<dyn ApiKeyRepository>,
    /// Audit 2026-06-11 (PrincipalCache revocation-lag) — evicted when
    /// a user deletes one of their own keys so it stops authenticating
    /// from cache immediately rather than after the cache TTL.
    pub principal_cache: Arc<crate::auth_middleware::PrincipalCache>,
}

pub fn api_keys_router(state: ApiKeysState) -> Router {
    Router::new()
        .route("/api-keys", get(list_keys).post(mint_key))
        .route("/api-keys/{id}", delete(delete_key))
        .with_state(state)
}

// ----------------------------------------------------------------------------
// DTOs
// ----------------------------------------------------------------------------

#[derive(Debug, Deserialize)]
pub struct MintApiKeyRequest {
    pub name: String,
    pub scopes: Vec<Scope>,
    /// When set, the key is bound to this app — every `App*(other)`
    /// capability is denied regardless of role.
    #[serde(default)]
    pub app_id: Option<AppId>,
    /// When set, lookup rejects the key after this instant. Absent =
    /// never expires (until explicit DELETE).
    #[serde(default)]
    pub expires_at: Option<DateTime<Utc>>,
}

/// Response body for a freshly-minted key. `raw_token` only appears
/// here — `GET /api-keys` returns `ApiKeyDto` without it.
#[derive(Debug, Serialize)]
pub struct MintApiKeyResponse {
    #[serde(flatten)]
    pub key: ApiKeyDto,
    /// The full wire-format token (`pic_<base32>`). Shown exactly once;
    /// store it client-side immediately.
    pub raw_token: String,
}

#[derive(Debug, Serialize)]
pub struct ApiKeyDto {
    pub id: ApiKeyId,
    pub prefix: String,
    pub name: String,
    pub scopes: Vec<Scope>,
    pub app_id: Option<AppId>,
    pub expires_at: Option<DateTime<Utc>>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
}

impl From<ApiKeyRow> for ApiKeyDto {
    fn from(r: ApiKeyRow) -> Self {
        Self {
            id: r.id,
            prefix: r.prefix,
            name: r.name,
            scopes: r.scopes,
            app_id: r.app_id,
            expires_at: r.expires_at,
            last_used_at: r.last_used_at,
            created_at: r.created_at,
        }
    }
}

// ----------------------------------------------------------------------------
// Handlers
// ----------------------------------------------------------------------------

async fn mint_key(
    State(state): State<ApiKeysState>,
    Extension(principal): Extension<Principal>,
    Json(input): Json<MintApiKeyRequest>,
) -> Result<(StatusCode, HeaderMap, Json<MintApiKeyResponse>), ApiKeysError> {
    validate_name(&input.name)?;
    validate_scopes(&input.scopes, input.app_id)?;

    let minted = generate_api_key().map_err(|e| ApiKeysError::Hash(e.to_string()))?;
    let row = state
        .keys
        .create(NewApiKey {
            user_id: principal.user_id,
            hash: minted.hash,
            prefix: minted.prefix,
            name: input.name,
            scopes: input.scopes,
            app_id: input.app_id,
            expires_at: input.expires_at,
        })
        .await?;
    Ok((
        StatusCode::CREATED,
        // C2: the response carries the raw key exactly once — never cache it.
        crate::auth_api::no_store_headers(),
        Json(MintApiKeyResponse {
            key: row.into(),
            raw_token: minted.raw,
        }),
    ))
}

async fn list_keys(
    State(state): State<ApiKeysState>,
    Extension(principal): Extension<Principal>,
) -> Result<Json<Vec<ApiKeyDto>>, ApiKeysError> {
    let rows = state.keys.list_for_user(principal.user_id).await?;
    Ok(Json(rows.into_iter().map(Into::into).collect()))
}

async fn delete_key(
    State(state): State<ApiKeysState>,
    Extension(principal): Extension<Principal>,
    Path(id): Path<ApiKeyId>,
) -> Result<StatusCode, ApiKeysError> {
    let deleted = state
        .keys
        .delete_by_id_and_user(id, principal.user_id)
        .await?;
    if !deleted {
        // 404 covers both "doesn't exist" and "exists but not yours" —
        // we deliberately don't leak the distinction.
        return Err(ApiKeysError::NotFound(id));
    }
    // Audit 2026-06-11 (PrincipalCache revocation-lag) — the cache is
    // keyed by token hash, which we can't derive from the key id, so
    // evict every cached principal for this user. That also drops their
    // session entries; re-auth on the next request is the right cost for
    // a deliberate key revocation.
    state.principal_cache.evict_user(principal.user_id);
    Ok(StatusCode::NO_CONTENT)
}

// ----------------------------------------------------------------------------
// Validation
// ----------------------------------------------------------------------------

fn validate_name(s: &str) -> Result<(), ApiKeysError> {
    let trimmed = s.trim();
    if trimmed.len() < NAME_MIN || trimmed.len() > NAME_MAX {
        return Err(ApiKeysError::InvalidName(format!(
            "name must be {NAME_MIN}-{NAME_MAX} characters after trimming"
        )));
    }
    Ok(())
}

fn validate_scopes(scopes: &[Scope], app_id: Option<AppId>) -> Result<(), ApiKeysError> {
    if scopes.is_empty() {
        return Err(ApiKeysError::InvalidScopes(
            "scopes must be non-empty".into(),
        ));
    }
    // Bound key + any instance:* scope → irreconcilable.
    if app_id.is_some() && scopes.iter().any(|s| s.is_instance()) {
        return Err(ApiKeysError::InvalidScopes(
            "bound keys (app_id set) cannot carry instance:* scopes".into(),
        ));
    }
    Ok(())
}

// ----------------------------------------------------------------------------
// Errors
// ----------------------------------------------------------------------------

#[derive(Debug, thiserror::Error)]
pub enum ApiKeysError {
    #[error("api key not found: {0}")]
    NotFound(ApiKeyId),

    #[error("{0}")]
    InvalidName(String),

    #[error("{0}")]
    InvalidScopes(String),

    #[error("failed to hash key: {0}")]
    Hash(String),

    #[error("repository error: {0}")]
    Repo(#[from] ApiKeyRepositoryError),
}

impl IntoResponse for ApiKeysError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            Self::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            Self::InvalidName(_) | Self::InvalidScopes(_) => {
                (StatusCode::UNPROCESSABLE_ENTITY, self.to_string())
            }
            Self::Hash(_) => {
                tracing::error!(error = %self, "api key hash failure");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "internal error".to_string(),
                )
            }
            Self::Repo(ApiKeyRepositoryError::NotFound(_)) => {
                (StatusCode::NOT_FOUND, self.to_string())
            }
            Self::Repo(ApiKeyRepositoryError::InvalidScope(_)) => {
                tracing::error!(error = %self, "api key row carries an unknown scope");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "internal error".to_string(),
                )
            }
            Self::Repo(ApiKeyRepositoryError::Db(e)) => {
                tracing::error!(error = %e, "api_keys db error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "internal error".to_string(),
                )
            }
        };
        (status, Json(json!({ "error": message }))).into_response()
    }
}
