//! `/api/v1/admin/apps/{id}/kv*` — read-only KV inspection (G2).
//!
//! Mirrors the minimal `files_api` / `queues_api` admin surface so the
//! `pic kv` CLI (and a future dashboard tab) can browse stored keys
//! without a script. **Read-only by design** — KV writes go through
//! `kv::set` in scripts, which emit change events the trigger framework
//! depends on; an admin write would bypass that and could break app
//! invariants, so it is deliberately out of scope here (matching the
//! read-only queues precedent).
//!
//! Two operations:
//!   * `GET /apps/{id}/kv?collection=<c>&cursor=&limit=` — list keys in a
//!     collection (cursor-paginated).
//!   * `GET /apps/{id}/kv/{collection}/{key}` — fetch one value.
//!
//! Capability: `AppKvRead`, resolved against the app loaded from the
//! path (same tier the SDK read path uses).

use std::sync::Arc;

use axum::extract::{Path, Query, State};
use axum::response::{IntoResponse, Json, Response};
use axum::routing::get;
use axum::{Extension, Router};
use picloud_shared::{AppId, Principal};
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::app_repo::AppRepository;
use crate::authz::{require, AuthzDenied, AuthzRepo, Capability};
use crate::kv_repo::KvRepo;

#[derive(Clone)]
pub struct KvAdminState {
    pub kv: Arc<dyn KvRepo>,
    pub apps: Arc<dyn AppRepository>,
    pub authz: Arc<dyn AuthzRepo>,
}

pub fn kv_admin_router(state: KvAdminState) -> Router {
    Router::new()
        .route("/apps/{app_id}/kv", get(list_keys))
        .route("/apps/{app_id}/kv/{collection}/{key}", get(get_value))
        .with_state(state)
}

#[derive(Debug, Deserialize)]
pub struct ListKvQuery {
    pub collection: String,
    #[serde(default)]
    pub cursor: Option<String>,
    #[serde(default)]
    pub limit: Option<u32>,
}

#[derive(Debug, Serialize)]
struct GetValueResponse {
    value: serde_json::Value,
}

/// Serialize mirror of `shared::KvListPage` (which is not `Serialize`).
#[derive(Debug, Serialize)]
struct ListKeysResponse {
    keys: Vec<String>,
    next_cursor: Option<String>,
}

async fn list_keys(
    State(s): State<KvAdminState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Query(q): Query<ListKvQuery>,
) -> Result<Json<ListKeysResponse>, KvApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(s.authz.as_ref(), &principal, Capability::AppKvRead(app_id)).await?;
    let page =
        s.kv.list(
            app_id,
            &q.collection,
            q.cursor.as_deref(),
            q.limit.unwrap_or(0),
        )
        .await
        .map_err(|e| KvApiError::Backend(e.to_string()))?;
    Ok(Json(ListKeysResponse {
        keys: page.keys,
        next_cursor: page.next_cursor,
    }))
}

async fn get_value(
    State(s): State<KvAdminState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, collection, key)): Path<(String, String, String)>,
) -> Result<Json<GetValueResponse>, KvApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(s.authz.as_ref(), &principal, Capability::AppKvRead(app_id)).await?;
    let value =
        s.kv.get(app_id, &collection, &key)
            .await
            .map_err(|e| KvApiError::Backend(e.to_string()))?
            .ok_or(KvApiError::NotFound)?;
    Ok(Json(GetValueResponse { value }))
}

async fn resolve_app(apps: &dyn AppRepository, ident: &str) -> Result<AppId, KvApiError> {
    crate::app_repo::resolve_app(apps, ident)
        .await
        .map_err(|e| KvApiError::Backend(e.to_string()))?
        .map(|l| l.app.id)
        .ok_or(KvApiError::AppNotFound)
}

#[derive(Debug, thiserror::Error)]
pub enum KvApiError {
    #[error("app not found")]
    AppNotFound,
    #[error("key not found")]
    NotFound,
    #[error("forbidden")]
    Forbidden,
    #[error("authorization repo error: {0}")]
    AuthzRepo(String),
    #[error("kv backend: {0}")]
    Backend(String),
}

impl From<AuthzDenied> for KvApiError {
    fn from(d: AuthzDenied) -> Self {
        match d {
            AuthzDenied::Denied => Self::Forbidden,
            AuthzDenied::Repo(e) => Self::AuthzRepo(e.to_string()),
        }
    }
}

impl IntoResponse for KvApiError {
    fn into_response(self) -> Response {
        use axum::http::StatusCode;
        let (status, body) = match &self {
            Self::AppNotFound | Self::NotFound => {
                (StatusCode::NOT_FOUND, json!({ "error": self.to_string() }))
            }
            Self::Forbidden => (StatusCode::FORBIDDEN, json!({ "error": self.to_string() })),
            Self::AuthzRepo(e) => {
                tracing::error!(error = %e, "kv admin authz error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
            Self::Backend(e) => {
                tracing::error!(error = %e, "kv admin backend error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
        };
        (status, Json(body)).into_response()
    }
}
