//! `/api/v1/admin/groups/{id}/scripts*` — group-owned script admin (Phase 4).
//!
//!   * `GET  /groups/{id}/scripts` — list the group's own scripts (not
//!     inherited; just rows owned directly by this group). Gated by
//!     `GroupScriptsRead` (viewer+ on the group).
//!   * `POST /groups/{id}/scripts` — create a group-owned script. Gated by
//!     `GroupScriptsWrite` (editor+ on the group).
//!
//! A group script is a **template** inherited by every descendant app,
//! resolved by name with nearest-owner-wins (CoW). Get / update / delete of an
//! existing group script go through the by-id `/scripts/{id}` endpoints, which
//! are owner-polymorphic — a group script there gates on `GroupScripts*`.
//!
//! **Phase 4-lite scope:** group ENDPOINT scripts only, and they must be
//! self-contained — `kind=module` and any `import` are rejected here, because
//! the origin-aware (lexical) module resolver is Phase 4b. The owner is
//! resolved FIRST (slug-or-uuid) and `authz::require` binds the capability to
//! the resolved group id, never to a caller-controlled path param.

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::{
    GroupId, Principal, Script, ScriptKind, ScriptSandbox, ScriptValidator, ValidationError,
};
use serde::Deserialize;
use serde_json::json;

use crate::authz::{require, AuthzDenied, AuthzError, AuthzRepo, Capability};
use crate::group_repo::GroupRepository;
use crate::repo::{NewScript, ScriptRepository, ScriptRepositoryError};
use crate::sandbox::SandboxCeiling;

#[derive(Clone)]
pub struct GroupScriptsState {
    pub scripts: Arc<dyn ScriptRepository>,
    pub groups: Arc<dyn GroupRepository>,
    pub authz: Arc<dyn AuthzRepo>,
    pub validator: Arc<dyn ScriptValidator>,
    pub sandbox_ceiling: SandboxCeiling,
}

pub fn group_scripts_router(state: GroupScriptsState) -> Router {
    Router::new()
        .route(
            "/groups/{group_id}/scripts",
            get(list_group_scripts).post(create_group_script),
        )
        .with_state(state)
}

#[derive(Debug, Deserialize)]
pub struct CreateGroupScriptRequest {
    pub name: String,
    pub description: Option<String>,
    pub source: String,
    /// Phase 4-lite accepts only `endpoint`. A `module` is rejected (group
    /// modules + the lexical import resolver are Phase 4b).
    #[serde(default)]
    pub kind: ScriptKind,
    pub timeout_seconds: Option<i32>,
    pub memory_limit_mb: Option<i32>,
    #[serde(default)]
    pub sandbox: ScriptSandbox,
}

async fn list_group_scripts(
    State(s): State<GroupScriptsState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
) -> Result<Json<Vec<Script>>, GroupScriptsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupScriptsRead(group_id),
    )
    .await?;
    Ok(Json(s.scripts.list_for_group(group_id).await?))
}

async fn create_group_script(
    State(s): State<GroupScriptsState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Json(input): Json<CreateGroupScriptRequest>,
) -> Result<(StatusCode, Json<Script>), GroupScriptsApiError> {
    let group_id = resolve_group(&*s.groups, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::GroupScriptsWrite(group_id),
    )
    .await?;

    // Phase 4b: group modules + imports are allowed. Validate per kind,
    // mirroring the app-script create path (`api.rs`): a module gets the
    // stricter shape check + the reserved-name guard; an endpoint the
    // parse-only path. Without the kind branch a malformed-shape group
    // module would be accepted here and only fail later at import time,
    // and a reserved name (`kv`, `log`, …) would slip through. Imports
    // resolve lexically from this group's chain at runtime (§5.5); the
    // recorded edges feed the declarative dangling-import plan check.
    let validated = if input.kind == ScriptKind::Module {
        if crate::api::RESERVED_MODULE_NAMES.contains(&input.name.as_str()) {
            return Err(GroupScriptsApiError::Invalid(format!(
                "{:?} is a reserved module name (shadows a built-in SDK namespace)",
                input.name
            )));
        }
        s.validator.validate_module(&input.source)?
    } else {
        s.validator.validate(&input.source)?
    };
    s.sandbox_ceiling
        .check(&input.sandbox)
        .map_err(|e| GroupScriptsApiError::Invalid(e.to_string()))?;

    let created = s
        .scripts
        .create(NewScript {
            app_id: None,
            group_id: Some(group_id),
            name: input.name,
            description: input.description,
            source: input.source,
            kind: input.kind,
            timeout_seconds: input.timeout_seconds,
            memory_limit_mb: input.memory_limit_mb,
            sandbox: if input.sandbox.is_empty() {
                None
            } else {
                Some(input.sandbox)
            },
            enabled: true,
            imports: validated.imports,
        })
        .await?;
    Ok((StatusCode::CREATED, Json(created)))
}

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

#[derive(Debug, thiserror::Error)]
pub enum GroupScriptsApiError {
    #[error("group not found")]
    GroupNotFound,
    #[error("invalid request: {0}")]
    Invalid(String),
    #[error("conflict: {0}")]
    Conflict(String),
    #[error("forbidden")]
    Forbidden,
    #[error("authorization repo error: {0}")]
    AuthzRepo(String),
    #[error("scripts backend: {0}")]
    Backend(String),
}

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

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

impl From<ValidationError> for GroupScriptsApiError {
    fn from(e: ValidationError) -> Self {
        Self::Invalid(e.to_string())
    }
}

impl From<ScriptRepositoryError> for GroupScriptsApiError {
    fn from(e: ScriptRepositoryError) -> Self {
        match e {
            ScriptRepositoryError::Conflict(m) => Self::Conflict(m),
            ScriptRepositoryError::NotFound(id) => Self::Invalid(format!("script {id} not found")),
            ScriptRepositoryError::Db(e) => Self::Backend(e.to_string()),
        }
    }
}

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