//! `/api/v1/admin/{apps,groups}/{id_or_slug}/vars*` — the Phase-3 config
//! `vars` admin surface (write/list side; resolution lives in
//! `config_resolver` + the `vars::` SDK).
//!
//!   * `GET    /apps/{id}/vars`          — list the app's OWN vars.
//!   * `PUT    /apps/{id}/vars`          — set/overwrite one app var.
//!   * `DELETE /apps/{id}/vars/{key}`    — delete one app var.
//!   * `GET/PUT/DELETE /groups/{id}/vars[...]` — same, group-owned.
//!
//! App routes gate on `App{Vars}Read/Write`; group routes on
//! `Group{Vars}Read/Write`. 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. Listing returns the owner's
//! OWN rows only (not the resolved/inherited view).

use std::sync::Arc;

use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json, Response};
use axum::routing::{get, put};
use axum::{Extension, Router};
use picloud_shared::{AppId, GroupId, Principal};
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::vars_repo::{VarOwner, VarsRepo, VarsRepoError};

#[derive(Clone)]
pub struct VarsApiState {
    pub vars: Arc<dyn VarsRepo>,
    pub apps: Arc<dyn AppRepository>,
    pub groups: Arc<dyn GroupRepository>,
    pub authz: Arc<dyn AuthzRepo>,
}

pub fn vars_router(state: VarsApiState) -> Router {
    Router::new()
        .route(
            "/apps/{id_or_slug}/vars",
            get(list_app_vars).put(set_app_var),
        )
        .route(
            "/apps/{id_or_slug}/vars/{key}",
            axum::routing::delete(delete_app_var),
        )
        .route(
            "/groups/{id_or_slug}/vars",
            put(set_group_var).get(list_group_vars),
        )
        .route(
            "/groups/{id_or_slug}/vars/{key}",
            axum::routing::delete(delete_group_var),
        )
        .with_state(state)
}

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

#[derive(Debug, Deserialize)]
pub struct SetVarRequest {
    pub key: String,
    pub value: serde_json::Value,
    /// Environment scope — `*` (env-agnostic, default) or a concrete env
    /// name matched against `apps.environment` at resolution time.
    #[serde(default)]
    pub env: Option<String>,
    /// Write a tombstone (suppresses an inherited key) instead of a real
    /// value. The body's `value` is ignored for a tombstone.
    #[serde(default)]
    pub tombstone: bool,
}

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

#[derive(Debug, serde::Serialize)]
struct VarItem {
    key: String,
    env: String,
    value: serde_json::Value,
    is_tombstone: bool,
    updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, serde::Serialize)]
struct ListVarsResponse {
    vars: Vec<VarItem>,
}

// ----------------------------------------------------------------------------
// App handlers
// ----------------------------------------------------------------------------

async fn list_app_vars(
    State(s): State<VarsApiState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
) -> Result<Json<ListVarsResponse>, VarsApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppVarsRead(app_id),
    )
    .await?;
    list(&*s.vars, VarOwner::App(app_id)).await
}

async fn set_app_var(
    State(s): State<VarsApiState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Json(input): Json<SetVarRequest>,
) -> Result<StatusCode, VarsApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppVarsWrite(app_id),
    )
    .await?;
    set(&*s.vars, VarOwner::App(app_id), input).await
}

async fn delete_app_var(
    State(s): State<VarsApiState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, key)): Path<(String, String)>,
    Query(q): Query<EnvQuery>,
) -> Result<StatusCode, VarsApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppVarsWrite(app_id),
    )
    .await?;
    delete(&*s.vars, VarOwner::App(app_id), &key, q.env.as_deref()).await
}

// ----------------------------------------------------------------------------
// Group handlers
// ----------------------------------------------------------------------------

async fn list_group_vars(
    State(s): State<VarsApiState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
) -> Result<Json<ListVarsResponse>, VarsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupVarsRead(group_id),
    )
    .await?;
    list(&*s.vars, VarOwner::Group(group_id)).await
}

async fn set_group_var(
    State(s): State<VarsApiState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Json(input): Json<SetVarRequest>,
) -> Result<StatusCode, VarsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupVarsWrite(group_id),
    )
    .await?;
    set(&*s.vars, VarOwner::Group(group_id), input).await
}

async fn delete_group_var(
    State(s): State<VarsApiState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, key)): Path<(String, String)>,
    Query(q): Query<EnvQuery>,
) -> Result<StatusCode, VarsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupVarsWrite(group_id),
    )
    .await?;
    delete(&*s.vars, VarOwner::Group(group_id), &key, q.env.as_deref()).await
}

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

async fn list(
    vars: &dyn VarsRepo,
    owner: VarOwner,
) -> Result<Json<ListVarsResponse>, VarsApiError> {
    let rows = vars.list_for_owner(owner).await?;
    Ok(Json(ListVarsResponse {
        vars: rows
            .into_iter()
            .map(|r| VarItem {
                key: r.key,
                env: r.environment_scope,
                value: r.value,
                is_tombstone: r.is_tombstone,
                updated_at: r.updated_at,
            })
            .collect(),
    }))
}

async fn set(
    vars: &dyn VarsRepo,
    owner: VarOwner,
    input: SetVarRequest,
) -> Result<StatusCode, VarsApiError> {
    validate_key(&input.key)?;
    let env = input.env.as_deref().unwrap_or("*");
    validate_env_scope(env)?;
    // A tombstone carries no meaningful value (the resolver suppresses the
    // key regardless); store JSON null so the NOT NULL column is satisfied.
    let value = if input.tombstone {
        serde_json::Value::Null
    } else {
        input.value
    };
    vars.set(owner, env, &input.key, &value, input.tombstone)
        .await?;
    Ok(StatusCode::NO_CONTENT)
}

async fn delete(
    vars: &dyn VarsRepo,
    owner: VarOwner,
    key: &str,
    env: Option<&str>,
) -> Result<StatusCode, VarsApiError> {
    let env = env.unwrap_or("*");
    validate_env_scope(env)?;
    if !vars.delete(owner, env, key).await? {
        return Err(VarsApiError::NotFound);
    }
    Ok(StatusCode::NO_CONTENT)
}

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

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

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

/// Keys are kebab identifiers (`^[a-z0-9][a-z0-9-]*$`) — same shape as the
/// manifest's var names (docs/design §4.3).
fn validate_key(key: &str) -> Result<(), VarsApiError> {
    if key.is_empty() || key.len() > 128 {
        return Err(VarsApiError::Invalid("key must be 1–128 characters".into()));
    }
    let mut chars = key.chars();
    let first = chars.next().unwrap();
    if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
        return Err(VarsApiError::Invalid(
            "key must start with a lowercase letter or digit".into(),
        ));
    }
    if !key
        .chars()
        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
    {
        return Err(VarsApiError::Invalid(
            "key may contain only lowercase letters, digits, and hyphens".into(),
        ));
    }
    Ok(())
}

/// Env scope is `*` (env-agnostic) or a kebab env name.
fn validate_env_scope(env: &str) -> Result<(), VarsApiError> {
    if env == "*" {
        return Ok(());
    }
    if env.is_empty() || env.len() > 63 {
        return Err(VarsApiError::Invalid(
            "env must be '*' or 1–63 characters".into(),
        ));
    }
    let mut chars = env.chars();
    let first = chars.next().unwrap();
    if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
        return Err(VarsApiError::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(VarsApiError::Invalid(
            "env may contain only lowercase letters, digits, and hyphens".into(),
        ));
    }
    Ok(())
}

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

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

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

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

impl From<VarsRepoError> for VarsApiError {
    fn from(e: VarsRepoError) -> Self {
        match e {
            VarsRepoError::Db(e) => Self::Backend(e.to_string()),
        }
    }
}

impl IntoResponse for VarsApiError {
    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, "vars admin authz repo error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
            Self::Backend(e) => {
                tracing::error!(error = %e, "vars admin backend error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
        };
        (status, Json(body)).into_response()
    }
}
