//! `GET /api/v1/admin/apps/{id}/config/effective` — the resolved config an
//! app actually sees: every inherited var (with its value + provenance) and
//! every inherited secret (MASKED — name/owner/scope only, never the value).
//!
//! This is the read-only companion to the `vars`/`secrets` admin surfaces.
//! It runs the same §3 resolution the `vars::`/`secrets::` SDK calls run, so
//! a dev can see exactly what `vars::get`/`secrets::get` would return and
//! where each value comes from (`--explain` on the CLI surfaces the
//! provenance). Gated by `AppVarsRead` (config is app-readable); the secret
//! VALUES are deliberately absent — reading those needs `GroupSecretsRead`
//! at the owning group via the dedicated value endpoint.

use std::sync::Arc;

use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json, Response};
use axum::routing::get;
use axum::{Extension, Router};
use picloud_shared::{AppId, Principal};
use serde_json::json;
use sqlx::PgPool;

use crate::app_repo::AppRepository;
use crate::authz::{require, AuthzDenied, AuthzError, AuthzRepo, Capability};
use crate::config_resolver::{
    fetch_effective_secret_meta, fetch_var_candidates, resolve, OwnerKind,
};

#[derive(Clone)]
pub struct ConfigApiState {
    pub pool: PgPool,
    pub apps: Arc<dyn AppRepository>,
    pub authz: Arc<dyn AuthzRepo>,
}

pub fn config_router(state: ConfigApiState) -> Router {
    Router::new()
        .route("/apps/{id_or_slug}/config/effective", get(effective_config))
        .with_state(state)
}

fn owner_json(kind: OwnerKind, id: uuid::Uuid, depth: i32) -> serde_json::Value {
    json!({ "kind": kind.as_str(), "id": id, "depth": depth })
}

async fn effective_config(
    State(s): State<ConfigApiState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
) -> Result<Json<serde_json::Value>, ConfigApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppVarsRead(app_id),
    )
    .await?;

    // Vars: resolve to values + provenance (vars are app-readable config).
    let candidates = fetch_var_candidates(&s.pool, app_id)
        .await
        .map_err(|e| ConfigApiError::Backend(e.to_string()))?;
    let (values, provenance) = resolve(candidates);
    let mut vars = serde_json::Map::new();
    for (key, value) in values {
        let p = &provenance[&key];
        vars.insert(
            key,
            json!({
                "value": value,
                "owner": owner_json(p.owner_kind, p.owner_id, p.depth),
                "scope": p.scope,
                "merged_from": p.merged_from
                    .iter()
                    .map(|(d, sc)| json!({ "depth": d, "scope": sc }))
                    .collect::<Vec<_>>(),
            }),
        );
    }

    // Secrets: masked — name + owner/level/scope + status, never the value.
    let secret_meta = fetch_effective_secret_meta(&s.pool, app_id)
        .await
        .map_err(|e| ConfigApiError::Backend(e.to_string()))?;
    let mut secrets = serde_json::Map::new();
    for m in secret_meta {
        secrets.insert(
            m.name,
            json!({
                "status": "set",
                "owner": owner_json(m.owner_kind, m.owner_id, m.depth),
                "scope": m.scope,
            }),
        );
    }

    Ok(Json(json!({ "vars": vars, "secrets": secrets })))
}

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

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

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

impl From<AuthzError> for ConfigApiError {
    fn from(e: AuthzError) -> Self {
        Self::AuthzRepo(e.to_string())
    }
}

impl IntoResponse for ConfigApiError {
    fn into_response(self) -> Response {
        let (status, body) = match &self {
            Self::AppNotFound => (StatusCode::NOT_FOUND, json!({ "error": self.to_string() })),
            Self::Forbidden => (StatusCode::FORBIDDEN, json!({ "error": self.to_string() })),
            Self::AuthzRepo(e) => {
                tracing::error!(error = %e, "config effective authz repo error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
            Self::Backend(e) => {
                tracing::error!(error = %e, "config effective backend error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
        };
        (status, Json(body)).into_response()
    }
}
