//! `/api/v1/admin/{apps,groups}/{id}/secrets*` — secrets admin endpoints.
//!
//!   * `GET    /apps/{id}/secrets`          — list app secret names + updated_at
//!     (NEVER values).
//!   * `POST   /apps/{id}/secrets`          — set/overwrite an app secret.
//!   * `DELETE /apps/{id}/secrets/{name}`   — delete an app secret.
//!   * `GET    /groups/{id}/secrets`        — list group secret names + scope
//!     + updated_at (NEVER values).
//!   * `POST   /groups/{id}/secrets`        — set/overwrite a group secret
//!     (env-scoped).
//!   * `DELETE /groups/{id}/secrets/{name}` — delete a group secret.
//!   * `GET    /groups/{id}/secrets/{name}/value` — **human value read** —
//!     the ONE endpoint that returns plaintext, gated at the owning group.
//!
//! App set/delete are gated by `AppSecretsWrite`, list by `AppSecretsRead`.
//! Group set/list/delete are gated by `GroupSecretsWrite` (editor+); the
//! value-read is gated by `GroupSecretsRead` (group_admin only) — that is
//! the masked-secret boundary: a descendant app's dev can see that a group
//! secret EXISTS (and consume it at runtime via `secrets::get`) but only a
//! principal with read rights AT THE OWNING GROUP can read its value. The
//! owner is resolved FIRST (slug-or-uuid), THEN `authz::require` binds the
//! capability to the resolved owner id — never to a caller-controlled path
//! param. Values are encrypted with the process master key before they
//! touch the database (owner-bound AAD; see `secrets_service`).

use std::sync::Arc;

use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json, Response};
use axum::routing::get;
use axum::{Extension, Router};
use picloud_shared::{validate_secret_name, AppId, GroupId, MasterKey, Principal, SecretsError};
use serde::Deserialize;
use serde_json::json;

use crate::app_repo::AppRepository;
use crate::authz::{require, AuthzDenied, AuthzError, AuthzRepo, Capability};
use crate::group_repo::GroupRepository;
use crate::secrets_repo::{SecretOwner, SecretsRepo, SecretsRepoError};
use crate::secrets_service::{open, seal};

/// App secrets are env-agnostic; only group secrets carry a concrete scope.
const APP_SECRET_SCOPE: &str = "*";

#[derive(Clone)]
pub struct SecretsState {
    pub repo: Arc<dyn SecretsRepo>,
    pub apps: Arc<dyn AppRepository>,
    pub groups: Arc<dyn GroupRepository>,
    pub authz: Arc<dyn AuthzRepo>,
    pub master_key: MasterKey,
    pub max_value_bytes: usize,
}

pub fn secrets_router(state: SecretsState) -> Router {
    Router::new()
        .route(
            "/apps/{app_id}/secrets",
            get(list_app_secrets).post(set_app_secret),
        )
        .route(
            "/apps/{app_id}/secrets/{name}",
            axum::routing::delete(delete_app_secret),
        )
        .route(
            "/groups/{group_id}/secrets",
            get(list_group_secrets).post(set_group_secret),
        )
        .route(
            "/groups/{group_id}/secrets/{name}",
            axum::routing::delete(delete_group_secret),
        )
        .route(
            "/groups/{group_id}/secrets/{name}/value",
            get(read_group_secret_value),
        )
        .with_state(state)
}

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

#[derive(Debug, Deserialize)]
pub struct EnvQuery {
    #[serde(default)]
    pub env: Option<String>,
}

#[derive(Debug, serde::Serialize)]
struct SecretItem {
    name: String,
    /// Environment scope — `*` for app secrets, possibly a concrete env for
    /// group secrets.
    env: String,
    updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, serde::Serialize)]
struct ListSecretsResponse {
    secrets: Vec<SecretItem>,
    next_cursor: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct SetSecretRequest {
    pub name: String,
    /// Any JSON value — the dashboard sends a single-line string, but
    /// maps/arrays/numbers round-trip too (matching `secrets::set`).
    pub value: serde_json::Value,
    /// Environment scope (group secrets only). `*` (env-agnostic, default)
    /// or a concrete env matched against `apps.environment` at resolution.
    #[serde(default)]
    pub env: Option<String>,
}

// ----------------------------------------------------------------------------
// App handlers (env-agnostic, scope `*`)
// ----------------------------------------------------------------------------

async fn list_app_secrets(
    State(s): State<SecretsState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Query(q): Query<ListQuery>,
) -> Result<Json<ListSecretsResponse>, SecretsApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppSecretsRead(app_id),
    )
    .await?;
    list_meta(&*s.repo, SecretOwner::App(app_id), &q).await
}

async fn set_app_secret(
    State(s): State<SecretsState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Json(input): Json<SetSecretRequest>,
) -> Result<StatusCode, SecretsApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppSecretsWrite(app_id),
    )
    .await?;
    // App secrets are always env-agnostic; reject a stray `env`.
    if input.env.as_deref().is_some_and(|e| e != APP_SECRET_SCOPE) {
        return Err(SecretsApiError::Invalid(
            "app secrets are env-agnostic; set an environment scope on a group secret instead"
                .into(),
        ));
    }
    seal_and_store(&s, SecretOwner::App(app_id), APP_SECRET_SCOPE, input).await
}

async fn delete_app_secret(
    State(s): State<SecretsState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, name)): Path<(String, String)>,
) -> Result<StatusCode, SecretsApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppSecretsWrite(app_id),
    )
    .await?;
    delete(&*s.repo, SecretOwner::App(app_id), APP_SECRET_SCOPE, &name).await
}

// ----------------------------------------------------------------------------
// Group handlers (env-scoped)
// ----------------------------------------------------------------------------

async fn list_group_secrets(
    State(s): State<SecretsState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Query(q): Query<ListQuery>,
) -> Result<Json<ListSecretsResponse>, SecretsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupSecretsWrite(group_id),
    )
    .await?;
    list_meta(&*s.repo, SecretOwner::Group(group_id), &q).await
}

async fn set_group_secret(
    State(s): State<SecretsState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Json(input): Json<SetSecretRequest>,
) -> Result<StatusCode, SecretsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupSecretsWrite(group_id),
    )
    .await?;
    let env = input.env.clone().unwrap_or_else(|| APP_SECRET_SCOPE.into());
    validate_env_scope(&env)?;
    seal_and_store(&s, SecretOwner::Group(group_id), &env, input).await
}

async fn delete_group_secret(
    State(s): State<SecretsState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, name)): Path<(String, String)>,
    Query(q): Query<EnvQuery>,
) -> Result<StatusCode, SecretsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupSecretsWrite(group_id),
    )
    .await?;
    let env = q.env.unwrap_or_else(|| APP_SECRET_SCOPE.into());
    validate_env_scope(&env)?;
    delete(&*s.repo, SecretOwner::Group(group_id), &env, &name).await
}

/// The ONE plaintext-returning endpoint. Gated by `GroupSecretsRead`
/// (group_admin) — the masked-secret boundary. A descendant app's dev can
/// see the secret exists and consume it at runtime, but only a reader at the
/// owning group gets the value here.
async fn read_group_secret_value(
    State(s): State<SecretsState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, name)): Path<(String, String)>,
    Query(q): Query<EnvQuery>,
) -> Result<Json<serde_json::Value>, SecretsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupSecretsRead(group_id),
    )
    .await?;
    validate_secret_name(&name)?;
    let env = q.env.unwrap_or_else(|| APP_SECRET_SCOPE.into());
    validate_env_scope(&env)?;
    let owner = SecretOwner::Group(group_id);
    let stored = s
        .repo
        .get(owner, &env, &name)
        .await?
        .ok_or(SecretsApiError::NotFound)?;
    let value = open(&s.master_key, owner, &name, &stored).map_err(|e| {
        tracing::error!(group_id = %group_id, secret = %name, "group secret could not be decrypted");
        SecretsApiError::from(e)
    })?;
    Ok(Json(json!({ "name": name, "env": env, "value": value })))
}

// ----------------------------------------------------------------------------
// Shared owner-generic bodies
// ----------------------------------------------------------------------------

async fn list_meta(
    repo: &dyn SecretsRepo,
    owner: SecretOwner,
    q: &ListQuery,
) -> Result<Json<ListSecretsResponse>, SecretsApiError> {
    let page = repo
        .list_meta(owner, q.cursor.as_deref(), q.limit.unwrap_or(0))
        .await?;
    Ok(Json(ListSecretsResponse {
        secrets: page
            .items
            .into_iter()
            .map(|m| SecretItem {
                name: m.name,
                env: m.environment_scope,
                updated_at: m.updated_at,
            })
            .collect(),
        next_cursor: page.next_cursor,
    }))
}

async fn seal_and_store(
    s: &SecretsState,
    owner: SecretOwner,
    env: &str,
    input: SetSecretRequest,
) -> Result<StatusCode, SecretsApiError> {
    validate_secret_name(&input.name)?;
    // Audit 2026-06-11 H-D1 — v1 envelope with AAD bound to the owner+name.
    let (ciphertext, nonce, version) = seal(
        &s.master_key,
        owner,
        &input.name,
        &input.value,
        s.max_value_bytes,
    )?;
    s.repo
        .set(owner, env, &input.name, &ciphertext, &nonce, version)
        .await?;
    Ok(StatusCode::NO_CONTENT)
}

async fn delete(
    repo: &dyn SecretsRepo,
    owner: SecretOwner,
    env: &str,
    name: &str,
) -> Result<StatusCode, SecretsApiError> {
    if !repo.delete(owner, env, name).await? {
        return Err(SecretsApiError::NotFound);
    }
    Ok(StatusCode::NO_CONTENT)
}

// ----------------------------------------------------------------------------
// Resolution + validation
// ----------------------------------------------------------------------------

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

async fn resolve_group(
    groups: &dyn GroupRepository,
    ident: &str,
) -> Result<GroupId, SecretsApiError> {
    let found = if let Ok(uuid) = ident.parse::<uuid::Uuid>() {
        groups
            .get_by_id(uuid.into())
            .await
            .map_err(|e| SecretsApiError::Backend(e.to_string()))?
    } else {
        groups
            .get_by_slug(ident)
            .await
            .map_err(|e| SecretsApiError::Backend(e.to_string()))?
    };
    found.map(|g| g.id).ok_or(SecretsApiError::GroupNotFound)
}

/// Env scope is `*` (env-agnostic) or a kebab env name. Mirrors the vars
/// admin validator so a secret and a var share the same env vocabulary.
fn validate_env_scope(env: &str) -> Result<(), SecretsApiError> {
    if env == "*" {
        return Ok(());
    }
    if env.is_empty() || env.len() > 63 {
        return Err(SecretsApiError::Invalid(
            "env must be '*' or 1–63 characters".into(),
        ));
    }
    let first = env.chars().next().unwrap();
    if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
        return Err(SecretsApiError::Invalid(
            "env must start with a lowercase letter or digit".into(),
        ));
    }
    if !env
        .chars()
        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
    {
        return Err(SecretsApiError::Invalid(
            "env may contain only lowercase letters, digits, and hyphens".into(),
        ));
    }
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum SecretsApiError {
    #[error("app not found")]
    AppNotFound,
    #[error("group not found")]
    GroupNotFound,
    #[error("secret not found")]
    NotFound,
    #[error("invalid request: {0}")]
    Invalid(String),
    #[error("forbidden")]
    Forbidden,
    #[error("authorization repo error: {0}")]
    AuthzRepo(String),
    #[error("secrets backend: {0}")]
    Backend(String),
}

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

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

impl From<SecretsRepoError> for SecretsApiError {
    fn from(e: SecretsRepoError) -> Self {
        match e {
            SecretsRepoError::InvalidCursor => Self::Invalid("invalid pagination cursor".into()),
            SecretsRepoError::Db(e) => Self::Backend(e.to_string()),
        }
    }
}

impl From<SecretsError> for SecretsApiError {
    fn from(e: SecretsError) -> Self {
        match e {
            SecretsError::InvalidName(m) => Self::Invalid(m),
            SecretsError::TooLarge { .. } => Self::Invalid(e.to_string()),
            SecretsError::Forbidden => Self::Forbidden,
            other => Self::Backend(other.to_string()),
        }
    }
}

impl IntoResponse for SecretsApiError {
    fn into_response(self) -> Response {
        let (status, body) = match &self {
            Self::AppNotFound | Self::GroupNotFound | Self::NotFound => {
                (StatusCode::NOT_FOUND, json!({ "error": self.to_string() }))
            }
            Self::Invalid(_) => (
                StatusCode::UNPROCESSABLE_ENTITY,
                json!({ "error": self.to_string() }),
            ),
            Self::Forbidden => (StatusCode::FORBIDDEN, json!({ "error": self.to_string() })),
            Self::AuthzRepo(e) => {
                tracing::error!(error = %e, "secrets admin authz repo error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
            Self::Backend(e) => {
                tracing::error!(error = %e, "secrets admin backend error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
        };
        (status, Json(body)).into_response()
    }
}
