#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_TEMPLATE="${SCRIPT_DIR}/docker-compose.enterprise.yml"
NGINX_TEMPLATE="${SCRIPT_DIR}/nginx-security-center.enterprise.conf.template"
DOCS_DIR="${SCRIPT_DIR}/docs"
MYSQL_INIT_TEMPLATE="${SCRIPT_DIR}/mysql-init.sql"

readonly DEFAULT_OUTPUT_DIR="${SCRIPT_DIR}/rendered"
readonly DEFAULT_ENV_TEMPLATE="${SCRIPT_DIR}/enterprise.env.example"
readonly DEFAULT_PUBLIC_HTTPS_PORT=443
readonly DEFAULT_FRONTEND_UPSTREAM="frontend:3000"
readonly DEFAULT_BACKEND_UPSTREAM="backend:8080"

usage() {
  cat <<'EOF'
Security Center - Enterprise Deployment Helper

Purpose:
  Prepare, validate, and render the enterprise deployment path under
  scripts/enterprise/ without modifying other deployment directories.

Secret strategy:
  AWS SSM Parameter Store is the single source of truth for production secrets.

Public ingress model:
  Nginx is the only public ingress and TLS termination boundary.
  The public callback shape is:
    https://<public-hostname>/auth/callback

Commands:
  --help
      Show this help.

  --write-example-env
      Write an example enterprise env file. Defaults to:
      scripts/enterprise/enterprise.env.example

  --validate --env-file <path>
      Validate the enterprise env file, derived callback/origin values,
      break-glass policy, and Docker compose template inputs.

  --render --env-file <path> [--output-dir <dir>] [--resolve-ssm]
      Render:
        - .env.runtime
        - docker-compose.enterprise.yml
        - nginx-security-center.conf
        - external-integrations.md
      under the chosen output directory.

  --print-checklist
      Print the production operator checklist references.

Notes:
  - This helper does not modify standalone or archived deployment paths.
  - It does not create Auth0 / Google Workspace / Feishu resources for you.
  - It can resolve SSM parameters only when AWS CLI credentials are available.
EOF
}

die() {
  echo "ERROR: $*" >&2
  exit 1
}

info() {
  echo "INFO: $*"
}

is_truthy() {
  case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
    true|1|yes|y|on)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

sandbox_auth_mode_enabled() {
  if is_truthy "${SC_SANDBOX_AUTH_MODE:-false}"; then
    return 0
  fi
  [[ "${SC_AUTH0_DOMAIN:-}" == "placeholder.auth0.local" ]] && return 0
  [[ "${SC_AUTH0_CLIENT_ID:-}" == "placeholder-client-id" ]] && return 0
  return 1
}

test_tls_mode_enabled() {
  if sandbox_auth_mode_enabled; then
    return 0
  fi
  [[ "${SC_ENVIRONMENT_MODE:-}" == "test" ]]
}

require_file() {
  local path="$1"
  [[ -f "$path" ]] || die "required file not found: $path"
}

resolve_path() {
  local path="$1"
  if [[ "$path" = /* ]]; then
    printf '%s' "$path"
    return 0
  fi
  if [[ -f "$path" || -d "$path" ]]; then
    printf '%s' "$path"
    return 0
  fi
  if [[ -f "${ROOT_DIR}/${path}" || -d "${ROOT_DIR}/${path}" ]]; then
    printf '%s' "${ROOT_DIR}/${path}"
    return 0
  fi
  printf '%s' "$path"
}

trim() {
  local value="${1:-}"
  value="${value#"${value%%[![:space:]]*}"}"
  value="${value%"${value##*[![:space:]]}"}"
  printf '%s' "$value"
}

ensure_no_scheme() {
  local hostname="$1"
  [[ "$hostname" != http://* && "$hostname" != https://* ]] || die "SC_PUBLIC_HOSTNAME must not include a URL scheme"
  [[ "$hostname" != */* ]] || die "SC_PUBLIC_HOSTNAME must not include a path"
}

load_env_file() {
  local env_file="$1"
  env_file="$(resolve_path "$env_file")"
  require_file "$env_file"
  set -a
  # shellcheck disable=SC1090
  source "$env_file"
  set +a
}

derive_runtime_values() {
  : "${SC_PUBLIC_HOSTNAME:?SC_PUBLIC_HOSTNAME is required}"
  : "${SC_BACKEND_IMAGE:?SC_BACKEND_IMAGE is required}"
  : "${SC_FRONTEND_IMAGE:?SC_FRONTEND_IMAGE is required}"
  : "${SC_AUTH0_DOMAIN:?SC_AUTH0_DOMAIN is required}"
  : "${SC_AUTH0_CLIENT_ID:?SC_AUTH0_CLIENT_ID is required}"
  : "${SC_AUTH0_AUDIENCE:?SC_AUTH0_AUDIENCE is required}"
  : "${SC_AWS_REGION:?SC_AWS_REGION is required}"
  : "${SC_SSM_PATH_PREFIX:?SC_SSM_PATH_PREFIX is required}"
  : "${SC_SSM_PARAM_MYSQL_ROOT_PASSWORD:?SC_SSM_PARAM_MYSQL_ROOT_PASSWORD is required}"
  : "${SC_SSM_PARAM_MYSQL_APP_PASSWORD:?SC_SSM_PARAM_MYSQL_APP_PASSWORD is required}"
  : "${SC_SSM_PARAM_REDIS_PASSWORD:?SC_SSM_PARAM_REDIS_PASSWORD is required}"
  : "${SC_SSM_PARAM_JWT_ACCESS_SECRET:?SC_SSM_PARAM_JWT_ACCESS_SECRET is required}"
  : "${SC_SSM_PARAM_JWT_REFRESH_SECRET:?SC_SSM_PARAM_JWT_REFRESH_SECRET is required}"
  if ! sandbox_auth_mode_enabled; then
    : "${SC_SSM_PARAM_AUTH0_CLIENT_SECRET:?SC_SSM_PARAM_AUTH0_CLIENT_SECRET is required}"
  fi

  ensure_no_scheme "$SC_PUBLIC_HOSTNAME"

  export SC_PUBLIC_HTTPS_PORT="${SC_PUBLIC_HTTPS_PORT:-$DEFAULT_PUBLIC_HTTPS_PORT}"
  export SC_ALLOWED_ORIGIN="https://${SC_PUBLIC_HOSTNAME}"
  export SC_AUTH0_CALLBACK_URL="${SC_AUTH0_CALLBACK_URL:-https://${SC_PUBLIC_HOSTNAME}/auth/callback}"
  export SC_AUTH0_ALLOWED_WEB_ORIGIN="${SC_AUTH0_ALLOWED_WEB_ORIGIN:-https://${SC_PUBLIC_HOSTNAME}}"
  export SC_AUTH0_ALLOWED_LOGOUT_URL="${SC_AUTH0_ALLOWED_LOGOUT_URL:-https://${SC_PUBLIC_HOSTNAME}/auth/login}"
  export SC_NGINX_FRONTEND_UPSTREAM="${SC_NGINX_FRONTEND_UPSTREAM:-$DEFAULT_FRONTEND_UPSTREAM}"
  export SC_NGINX_BACKEND_UPSTREAM="${SC_NGINX_BACKEND_UPSTREAM:-$DEFAULT_BACKEND_UPSTREAM}"
  export SC_TLS_CERT_FULLCHAIN="${SC_TLS_CERT_FULLCHAIN:-/etc/letsencrypt/live/${SC_PUBLIC_HOSTNAME}/fullchain.pem}"
  export SC_TLS_CERT_PRIVKEY="${SC_TLS_CERT_PRIVKEY:-/etc/letsencrypt/live/${SC_PUBLIC_HOSTNAME}/privkey.pem}"
  export SC_ACME_WEBROOT="${SC_ACME_WEBROOT:-/var/www/certbot}"
  export SC_BREAK_GLASS_ENABLED="${SC_BREAK_GLASS_ENABLED:-false}"
  export SC_DEPLOYMENT_MODE="${SC_DEPLOYMENT_MODE:-production}"
  export SC_NEXT_PUBLIC_SSO_PROVIDER="${SC_NEXT_PUBLIC_SSO_PROVIDER:-auth0}"
  export SC_ALLOWED_ORIGINS="${SC_ALLOWED_ORIGINS:-${SC_ALLOWED_ORIGIN}}"
  if sandbox_auth_mode_enabled; then
    export SC_OAUTH_ENABLED="false"
    export SC_BYPASS_SSO="true"
    export SC_SANDBOX_AUTH_MODE="true"
  else
    export SC_OAUTH_ENABLED="true"
    export SC_BYPASS_SSO="false"
    export SC_SANDBOX_AUTH_MODE="${SC_SANDBOX_AUTH_MODE:-false}"
  fi
  export SC_COMPOSE_PROJECT_NAME="${SC_COMPOSE_PROJECT_NAME:-security-center-enterprise}"
  export SC_MYSQL_DATABASE="${SC_MYSQL_DATABASE:-security_center}"
  export SC_MYSQL_USER="${SC_MYSQL_USER:-sc_app}"
  export SC_FRONTEND_HEALTHCHECK_PATH="${SC_FRONTEND_HEALTHCHECK_PATH:-/api/health}"
  export SC_BACKEND_HEALTHCHECK_PATH="${SC_BACKEND_HEALTHCHECK_PATH:-/health}"
  export SC_ALERTS_LARK_BOUNDARY="${SC_ALERTS_LARK_BOUNDARY:-split}"
  export SC_UPLOADS_ENABLED="${SC_UPLOADS_ENABLED:-false}"
  export SC_UPLOADS_REGION="${SC_UPLOADS_REGION:-${SC_AWS_REGION}}"
  export SC_UPLOADS_PREFIX="${SC_UPLOADS_PREFIX:-vuln-uploads/}"
  export SC_UPLOADS_PRESIGN_TTL="${SC_UPLOADS_PRESIGN_TTL:-15m}"
  export SC_UPLOADS_USE_PATH_STYLE="${SC_UPLOADS_USE_PATH_STYLE:-false}"
  export SC_UPLOADS_CORS_ALLOWED_ORIGINS="${SC_UPLOADS_CORS_ALLOWED_ORIGINS:-${SC_ALLOWED_ORIGIN}}"
  export SC_DAST_ENABLED="${SC_DAST_ENABLED:-false}"
  export SC_DAST_BASE_URL="${SC_DAST_BASE_URL:-https://scanner-api.dast.internal}"
  export SC_DAST_API_TOKEN_PARAM="${SC_DAST_API_TOKEN_PARAM:-${SC_SSM_PATH_PREFIX%/}/dast/api-token}"
  export SC_DAST_CALLBACK_TOKEN_PARAM="${SC_DAST_CALLBACK_TOKEN_PARAM:-${SC_SSM_PATH_PREFIX%/}/dast/callback-token}"
  export SC_DAST_TOKEN_KMS_KEY_ARN="${SC_DAST_TOKEN_KMS_KEY_ARN:-}"
  export SC_DAST_CA_BUNDLE_PARAM="${SC_DAST_CA_BUNDLE_PARAM:-${SC_SSM_PATH_PREFIX%/}/dast/ca-bundle}"
  export SC_DAST_USE_PUBLIC_CA="${SC_DAST_USE_PUBLIC_CA:-false}"
  export SC_DAST_CA_BUNDLE_MOUNT_PATH="${SC_DAST_CA_BUNDLE_MOUNT_PATH:-/app/certs/dast-scanner-ca.crt}"
  export SC_DAST_MAX_TARGETS_PER_BATCH="${SC_DAST_MAX_TARGETS_PER_BATCH:-500}"
  export SC_DAST_MAX_CAMPAIGN_TARGETS="${SC_DAST_MAX_CAMPAIGN_TARGETS:-50000}"
  export SC_DAST_TIMEOUT="${SC_DAST_TIMEOUT:-10s}"
  export SC_DAST_USER_AGENT="${SC_DAST_USER_AGENT:-security-center-scan-client}"
  export SC_DAST_SCANNER_SECURITY_GROUP_ID="${SC_DAST_SCANNER_SECURITY_GROUP_ID:-}"
  export SC_DAST_SCANNER_SOURCE_IP="${SC_DAST_SCANNER_SOURCE_IP:-}"
  export SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR="${SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR:-}"
  export SC_DAST_CALLBACK_ORIGIN="${SC_DAST_CALLBACK_ORIGIN:-}"
  export SC_DAST_CA_BUNDLE_HOST_PATH="${SC_DAST_CA_BUNDLE_HOST_PATH:-./certs/dast-scanner-ca.disabled.crt}"
  export SC_DAST_SSL_CERT_FILE="${SC_DAST_SSL_CERT_FILE:-}"

  # Agent OpenAPI/MCP channel: API-key auth is on by default; quota mirrors the
  # local posture (100 rpm/key); permanent keys stay disabled in production.
  export SC_AGENT_ENABLED="${SC_AGENT_ENABLED:-true}"
  export SC_AGENT_KEY_PREFIX="${SC_AGENT_KEY_PREFIX:-sk-agent-}"
  export SC_AGENT_ALLOW_PERMANENT_KEY="${SC_AGENT_ALLOW_PERMANENT_KEY:-false}"
  export SC_AGENT_QUOTA_ENABLED="${SC_AGENT_QUOTA_ENABLED:-true}"
  export SC_AGENT_DEFAULT_QUOTA_RPM="${SC_AGENT_DEFAULT_QUOTA_RPM:-100}"
  export SC_AUDIT_RETENTION_DAYS="${SC_AUDIT_RETENTION_DAYS:-90}"
}

validate_break_glass_policy() {
  case "$(printf '%s' "$SC_BREAK_GLASS_ENABLED" | tr '[:upper:]' '[:lower:]')" in
    false|0|no)
      return 0
      ;;
    true|1|yes)
      : "${SC_BREAK_GLASS_JUSTIFICATION:?SC_BREAK_GLASS_JUSTIFICATION is required when SC_BREAK_GLASS_ENABLED=true}"
      : "${SC_BREAK_GLASS_ALLOWED_CIDRS:?SC_BREAK_GLASS_ALLOWED_CIDRS is required when SC_BREAK_GLASS_ENABLED=true}"
      ;;
    *)
      die "SC_BREAK_GLASS_ENABLED must be true or false"
      ;;
  esac
}

validate_callback_alignment() {
  [[ "$SC_AUTH0_CALLBACK_URL" == "https://${SC_PUBLIC_HOSTNAME}/auth/callback" ]] || \
    die "SC_AUTH0_CALLBACK_URL must match https://${SC_PUBLIC_HOSTNAME}/auth/callback"
  [[ "$SC_AUTH0_ALLOWED_WEB_ORIGIN" == "https://${SC_PUBLIC_HOSTNAME}" ]] || \
    die "SC_AUTH0_ALLOWED_WEB_ORIGIN must match https://${SC_PUBLIC_HOSTNAME}"
  [[ "$SC_AUTH0_ALLOWED_LOGOUT_URL" == "https://${SC_PUBLIC_HOSTNAME}/auth/login" ]] || \
    die "SC_AUTH0_ALLOWED_LOGOUT_URL must match https://${SC_PUBLIC_HOSTNAME}/auth/login"
  [[ "$SC_ALLOWED_ORIGIN" == "https://${SC_PUBLIC_HOSTNAME}" ]] || \
    die "SC_ALLOWED_ORIGIN must match https://${SC_PUBLIC_HOSTNAME}"
}

validate_secret_strategy() {
  [[ "$SC_SSM_PATH_PREFIX" == /* ]] || die "SC_SSM_PATH_PREFIX must be an absolute Parameter Store prefix"
  for key in \
    SC_SSM_PARAM_MYSQL_ROOT_PASSWORD \
    SC_SSM_PARAM_MYSQL_APP_PASSWORD \
    SC_SSM_PARAM_REDIS_PASSWORD \
    SC_SSM_PARAM_JWT_ACCESS_SECRET \
    SC_SSM_PARAM_JWT_REFRESH_SECRET
  do
    local value="${!key}"
    [[ "$value" == /* ]] || die "$key must be an absolute SSM parameter name"
  done

  if ! sandbox_auth_mode_enabled; then
    local auth0_secret_param="${SC_SSM_PARAM_AUTH0_CLIENT_SECRET:-}"
    [[ -n "${auth0_secret_param}" && "${auth0_secret_param}" == /* ]] || die "SC_SSM_PARAM_AUTH0_CLIENT_SECRET must be an absolute SSM parameter name when sandbox auth mode is disabled"
  fi

  for key in \
    SC_SSM_PARAM_FEISHU_ORG_APP_ID \
    SC_SSM_PARAM_FEISHU_ORG_APP_SECRET \
    SC_SSM_PARAM_FEISHU_ALERT_APP_ID \
    SC_SSM_PARAM_FEISHU_ALERT_APP_SECRET
  do
    local value="${!key:-}"
    [[ -z "${value}" || "${value}" == /* ]] || die "$key must be blank or an absolute SSM parameter name"
  done
}

validate_upload_settings() {
  local context="${1:-strict}"
  case "$(printf '%s' "$SC_UPLOADS_ENABLED" | tr '[:upper:]' '[:lower:]')" in
    false|0|no)
      export SC_UPLOADS_ENABLED="false"
      return 0
      ;;
    true|1|yes)
      export SC_UPLOADS_ENABLED="true"
      : "${SC_UPLOADS_REGION:?SC_UPLOADS_REGION is required when SC_UPLOADS_ENABLED=true}"
      if [[ -z "${SC_UPLOADS_BUCKET}" ]]; then
        if [[ "${context}" == "validate" ]]; then
          info "SC_UPLOADS_BUCKET is currently blank; provision is expected to generate or persist the uploads bucket before render/deploy"
          return 0
        fi
        : "${SC_UPLOADS_BUCKET:?SC_UPLOADS_BUCKET is required when SC_UPLOADS_ENABLED=true}"
      fi
      ;;
    *)
      die "SC_UPLOADS_ENABLED must be true or false"
      ;;
  esac
}

dast_enabled() {
  is_truthy "${SC_DAST_ENABLED:-false}"
}

validate_positive_integer() {
  local key="$1"
  local value="$2"
  [[ "${value}" =~ ^[0-9]+$ ]] || die "${key} must be a positive integer"
  (( value > 0 )) || die "${key} must be greater than 0"
}

validate_dast_base_url() {
  python3 - "${SC_DAST_BASE_URL}" <<'PY' || die "SC_DAST_BASE_URL must be an https:// origin and must not point to localhost or loopback"
from urllib.parse import urlparse
import ipaddress
import sys

raw = sys.argv[1].strip()
parsed = urlparse(raw)
if parsed.scheme != "https" or not parsed.netloc:
    raise SystemExit(1)
if parsed.path not in ("", "/") or parsed.params or parsed.query or parsed.fragment:
    raise SystemExit(1)
host = (parsed.hostname or "").strip().lower()
if host in {"localhost", "0.0.0.0"}:
    raise SystemExit(1)
try:
    ip = ipaddress.ip_address(host)
except ValueError:
    ip = None
if ip and (ip.is_loopback or ip.is_unspecified):
    raise SystemExit(1)
PY
}

validate_dast_settings() {
  if ! dast_enabled; then
    export SC_DAST_ENABLED="false"
    return 0
  fi

  export SC_DAST_ENABLED="true"
  validate_dast_base_url
  [[ "${SC_DAST_API_TOKEN_PARAM}" == /* ]] || die "SC_DAST_API_TOKEN_PARAM must be an absolute SSM parameter name"
  [[ "${SC_DAST_CALLBACK_TOKEN_PARAM}" == /* ]] || die "SC_DAST_CALLBACK_TOKEN_PARAM must be an absolute SSM parameter name"
  [[ "${SC_DAST_API_TOKEN_PARAM}" != "${SC_DAST_CALLBACK_TOKEN_PARAM}" ]] || die "SC_DAST_API_TOKEN_PARAM and SC_DAST_CALLBACK_TOKEN_PARAM must be different"
  validate_positive_integer "SC_DAST_MAX_TARGETS_PER_BATCH" "${SC_DAST_MAX_TARGETS_PER_BATCH}"
  validate_positive_integer "SC_DAST_MAX_CAMPAIGN_TARGETS" "${SC_DAST_MAX_CAMPAIGN_TARGETS}"
  [[ "${SC_DAST_CA_BUNDLE_MOUNT_PATH}" == /* ]] || die "SC_DAST_CA_BUNDLE_MOUNT_PATH must be an absolute backend container path"
  if [[ "${SC_ENVIRONMENT_MODE:-prod}" == "prod" ]] && ! is_truthy "${SC_DAST_USE_PUBLIC_CA}"; then
    [[ -n "${SC_DAST_CA_BUNDLE_PARAM:-}" && "${SC_DAST_CA_BUNDLE_PARAM}" == /* ]] || die "SC_DAST_CA_BUNDLE_PARAM must be an absolute SSM parameter name when SC_DAST_ENABLED=true in prod"
  elif [[ -n "${SC_DAST_CA_BUNDLE_PARAM:-}" ]]; then
    [[ "${SC_DAST_CA_BUNDLE_PARAM}" == /* ]] || die "SC_DAST_CA_BUNDLE_PARAM must be blank or an absolute SSM parameter name"
  fi
}

runtime_env_has_placeholders() {
  local path="$1"
  grep -q '__RESOLVE_FROM_SSM__' "$path"
}

print_secret_resolution_summary() {
  local path="$1"
  local resolve_ssm="$2"

  if runtime_env_has_placeholders "$path"; then
    if [[ "$resolve_ssm" == "true" ]]; then
      die "rendered runtime env still contains unresolved placeholders after --resolve-ssm"
    fi
    info "runtime secret state: deferred placeholders present in ${path}"
    info "deploy/build-deploy flows must not use this rendered bundle until secrets are resolved"
  else
    info "runtime secret state: resolved for compose/runtime secrets in ${path}"
  fi

  info "Feishu secret state: validated as SSM parameter names only; bootstrap into application settings remains a post-deploy step"
}

write_example_env() {
  local path="${1:-$DEFAULT_ENV_TEMPLATE}"
  cat >"$path" <<'EOF'
# Enterprise deployment source of truth
SC_PUBLIC_HOSTNAME=security-center.example.com
SC_BACKEND_IMAGE=ghcr.io/example/security-center-backend:latest
SC_FRONTEND_IMAGE=ghcr.io/example/security-center-frontend:latest
SC_AWS_REGION=eu-west-2
SC_AWS_ACCOUNT_ID=
SC_ENVIRONMENT_MODE=prod
SC_SSM_PATH_PREFIX=/security-center/prod
SC_REMOTE_TRANSPORT=ssm
SC_DEPLOY_WORKFLOW=build-deploy
SC_DEPLOY_AUTO_WRITE_SECRETS=false
SC_DEPLOY_AUTO_GENERATE_MISSING_SECRETS=false
SC_DEPLOY_AUTO_VERIFY=true

# Auth0 / Google Workspace public integration
SC_AUTH0_DOMAIN=example.eu.auth0.com
SC_AUTH0_CLIENT_ID=replace-me
SC_AUTH0_AUDIENCE=https://api.security-center.example.com
SC_AUTH0_CALLBACK_URL=https://security-center.example.com/auth/callback
SC_AUTH0_ALLOWED_WEB_ORIGIN=https://security-center.example.com
SC_AUTH0_ALLOWED_LOGOUT_URL=https://security-center.example.com/auth/login
SC_GOOGLE_WORKSPACE_DOMAIN=example.com
SC_TLS_ACME_EMAIL=ops@example.com
SC_SANDBOX_AUTH_MODE=false

# EC2 provisioning defaults
SC_EC2_INSTANCE_TYPE=t3.medium
SC_EC2_VOLUME_SIZE_GB=40
SC_EC2_NAME_PREFIX=security-center-prod
SC_EC2_SSH_USER=ec2-user
SC_EC2_HTTPS_INGRESS_CIDRS=
SC_EC2_HTTP_INGRESS_CIDRS=
SC_EC2_SSH_INGRESS_CIDRS=

# Secrets resolved from AWS SSM Parameter Store
SC_SSM_PARAM_MYSQL_ROOT_PASSWORD=/security-center/prod/mysql/root_password
SC_SSM_PARAM_MYSQL_APP_PASSWORD=/security-center/prod/mysql/app_password
SC_SSM_PARAM_REDIS_PASSWORD=/security-center/prod/redis/password
SC_SSM_PARAM_JWT_ACCESS_SECRET=/security-center/prod/jwt/access_secret
SC_SSM_PARAM_JWT_REFRESH_SECRET=/security-center/prod/jwt/refresh_secret
SC_SSM_PARAM_AUTH0_CLIENT_SECRET=/security-center/prod/auth0/client_secret

# Feishu credentials are still application settings. Keep the raw values in SSM and
# bootstrap them into system settings after deployment.
SC_SSM_PARAM_FEISHU_ORG_APP_ID=/security-center/prod/feishu/org_sync/app_id
SC_SSM_PARAM_FEISHU_ORG_APP_SECRET=/security-center/prod/feishu/org_sync/app_secret
SC_SSM_PARAM_FEISHU_ALERT_APP_ID=/security-center/prod/feishu/alerts/app_id
SC_SSM_PARAM_FEISHU_ALERT_APP_SECRET=/security-center/prod/feishu/alerts/app_secret

# Optional overrides
SC_BREAK_GLASS_ENABLED=false
SC_ALERTS_LARK_BOUNDARY=split

# Vulnerability image upload (private S3 bucket + presigned PUT/GET)
SC_UPLOADS_ENABLED=false
SC_UPLOADS_BUCKET=
SC_UPLOADS_REGION=eu-west-2
SC_UPLOADS_PREFIX=vuln-uploads/
SC_UPLOADS_BASE_URL=
SC_UPLOADS_PRESIGN_TTL=15m
SC_UPLOADS_ENDPOINT=
SC_UPLOADS_USE_PATH_STYLE=false
SC_UPLOADS_CORS_ALLOWED_ORIGINS=https://security-center.example.com

# Optional scanner integration. The scanner is deployed independently; this
# platform only consumes its private API contract.
SC_DAST_ENABLED=false
SC_DAST_BASE_URL=https://scanner-api.dast.internal
SC_DAST_API_TOKEN_PARAM=/security-center/prod/dast/api-token
SC_DAST_CALLBACK_TOKEN_PARAM=/security-center/prod/dast/callback-token
SC_DAST_TOKEN_KMS_KEY_ARN=
SC_DAST_CA_BUNDLE_PARAM=/security-center/prod/dast/ca-bundle
SC_DAST_USE_PUBLIC_CA=false
SC_DAST_CA_BUNDLE_MOUNT_PATH=/app/certs/dast-scanner-ca.crt
SC_DAST_MAX_TARGETS_PER_BATCH=500
SC_DAST_MAX_CAMPAIGN_TARGETS=50000
SC_DAST_TIMEOUT=10s
SC_DAST_USER_AGENT=security-center-scan-client
SC_DAST_SCANNER_SECURITY_GROUP_ID=
SC_DAST_SCANNER_SOURCE_IP=
SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR=
SC_DAST_CALLBACK_ORIGIN=
SC_DAST_ASSET_HANDOFF_FILE=
EOF
  info "wrote example enterprise env: $path"
}

resolve_ssm_param() {
  local name="$1"
  local value=""
  if ! value="$(aws ssm get-parameter \
    --name "$name" \
    --with-decryption \
    --region "$SC_AWS_REGION" \
    --query 'Parameter.Value' \
    --output text 2>/tmp/security-center-ssm-error.$$)"; then
    local error_text=""
    error_text="$(tr '\n' ' ' </tmp/security-center-ssm-error.$$ 2>/dev/null || true)"
    rm -f /tmp/security-center-ssm-error.$$
    die "failed to resolve SSM parameter ${name}: ${error_text:-unknown aws ssm error}"
  fi
  rm -f /tmp/security-center-ssm-error.$$
  printf '%s' "${value}"
}

write_runtime_env() {
  local path="$1"
  local resolve_ssm="${2:-false}"

  local mysql_root='__RESOLVE_FROM_SSM__'
  local mysql_app='__RESOLVE_FROM_SSM__'
  local redis_password='__RESOLVE_FROM_SSM__'
  local jwt_access='__RESOLVE_FROM_SSM__'
  local jwt_refresh='__RESOLVE_FROM_SSM__'
  local auth0_secret=''
  local dast_api_token=''
  local dast_callback_token=''

  if [[ "$resolve_ssm" == "true" ]]; then
    command -v aws >/dev/null 2>&1 || die "--resolve-ssm requested but aws CLI is not available"
    mysql_root="$(resolve_ssm_param "$SC_SSM_PARAM_MYSQL_ROOT_PASSWORD")"
    mysql_app="$(resolve_ssm_param "$SC_SSM_PARAM_MYSQL_APP_PASSWORD")"
    redis_password="$(resolve_ssm_param "$SC_SSM_PARAM_REDIS_PASSWORD")"
    jwt_access="$(resolve_ssm_param "$SC_SSM_PARAM_JWT_ACCESS_SECRET")"
    jwt_refresh="$(resolve_ssm_param "$SC_SSM_PARAM_JWT_REFRESH_SECRET")"
    if [[ "${SC_OAUTH_ENABLED}" == "true" ]]; then
      auth0_secret="$(resolve_ssm_param "$SC_SSM_PARAM_AUTH0_CLIENT_SECRET")"
    fi
    if dast_enabled; then
      dast_api_token="$(resolve_ssm_param "$SC_DAST_API_TOKEN_PARAM")"
      dast_callback_token="$(resolve_ssm_param "$SC_DAST_CALLBACK_TOKEN_PARAM")"
      [[ -n "${dast_api_token}" ]] || die "resolved SC_DAST_API_TOKEN_PARAM is empty"
      [[ -n "${dast_callback_token}" ]] || die "resolved SC_DAST_CALLBACK_TOKEN_PARAM is empty"
      [[ "${dast_api_token}" != "${dast_callback_token}" ]] || die "scanner API token and callback token must be different"
    fi
  elif [[ "${SC_OAUTH_ENABLED}" == "true" ]]; then
    auth0_secret='__RESOLVE_FROM_SSM__'
  fi
  if [[ "$resolve_ssm" != "true" && "$(dast_enabled && printf true || printf false)" == "true" ]]; then
    dast_api_token='__RESOLVE_FROM_SSM__'
    dast_callback_token='__RESOLVE_FROM_SSM__'
  fi

  cat >"$path" <<EOF
COMPOSE_PROJECT_NAME=${SC_COMPOSE_PROJECT_NAME}
SC_APP_ENVIRONMENT=${SC_DEPLOYMENT_MODE}
SC_ENVIRONMENT_MODE=${SC_ENVIRONMENT_MODE}
SC_PUBLIC_HOSTNAME=${SC_PUBLIC_HOSTNAME}
SC_ALLOWED_ORIGIN=${SC_ALLOWED_ORIGIN}
SC_ALLOWED_ORIGINS=${SC_ALLOWED_ORIGINS}
SC_BACKEND_IMAGE=${SC_BACKEND_IMAGE}
SC_FRONTEND_IMAGE=${SC_FRONTEND_IMAGE}
SC_OAUTH_ENABLED=${SC_OAUTH_ENABLED}
SC_BYPASS_SSO=${SC_BYPASS_SSO}
SC_AUTH0_DOMAIN=${SC_AUTH0_DOMAIN}
SC_AUTH0_CLIENT_ID=${SC_AUTH0_CLIENT_ID}
SC_AUTH0_CLIENT_SECRET=${auth0_secret}
SC_AUTH0_AUDIENCE=${SC_AUTH0_AUDIENCE}
SC_AUTH0_CALLBACK_URL=${SC_AUTH0_CALLBACK_URL}
SC_NEXT_PUBLIC_SSO_PROVIDER=${SC_NEXT_PUBLIC_SSO_PROVIDER}
SC_UPLOADS_ENABLED=${SC_UPLOADS_ENABLED}
SC_UPLOADS_BUCKET=${SC_UPLOADS_BUCKET:-}
SC_UPLOADS_REGION=${SC_UPLOADS_REGION:-}
SC_UPLOADS_PREFIX=${SC_UPLOADS_PREFIX}
SC_UPLOADS_BASE_URL=${SC_UPLOADS_BASE_URL:-}
SC_UPLOADS_PRESIGN_TTL=${SC_UPLOADS_PRESIGN_TTL}
SC_UPLOADS_ENDPOINT=${SC_UPLOADS_ENDPOINT:-}
SC_UPLOADS_PUBLIC_ENDPOINT=${SC_UPLOADS_PUBLIC_ENDPOINT:-}
SC_UPLOADS_USE_PATH_STYLE=${SC_UPLOADS_USE_PATH_STYLE}
SC_UPLOADS_CORS_ALLOWED_ORIGINS=${SC_UPLOADS_CORS_ALLOWED_ORIGINS}
SECURITY_CENTER_STORAGE_PROVIDER=s3
SECURITY_CENTER_STORAGE_S3_ENABLED=${SC_UPLOADS_ENABLED}
SECURITY_CENTER_STORAGE_S3_BUCKET=${SC_UPLOADS_BUCKET:-}
SECURITY_CENTER_STORAGE_S3_REGION=${SC_UPLOADS_REGION:-}
SECURITY_CENTER_STORAGE_S3_BASE_URL=${SC_UPLOADS_BASE_URL:-}
SECURITY_CENTER_STORAGE_S3_UPLOAD_PREFIX=${SC_UPLOADS_PREFIX}
SECURITY_CENTER_STORAGE_S3_PRESIGN_TTL=${SC_UPLOADS_PRESIGN_TTL}
SECURITY_CENTER_STORAGE_S3_ENDPOINT=${SC_UPLOADS_ENDPOINT:-}
SECURITY_CENTER_STORAGE_S3_PUBLIC_ENDPOINT=${SC_UPLOADS_PUBLIC_ENDPOINT:-}
SECURITY_CENTER_STORAGE_S3_USE_PATH_STYLE=${SC_UPLOADS_USE_PATH_STYLE}
SC_MYSQL_DATABASE=${SC_MYSQL_DATABASE}
SC_MYSQL_USER=${SC_MYSQL_USER}
SC_MYSQL_ROOT_PASSWORD=${mysql_root}
SC_MYSQL_APP_PASSWORD=${mysql_app}
SC_REDIS_PASSWORD=${redis_password}
SC_JWT_ACCESS_SECRET=${jwt_access}
SC_JWT_REFRESH_SECRET=${jwt_refresh}
SC_TLS_CERT_FULLCHAIN=${SC_TLS_CERT_FULLCHAIN}
SC_TLS_CERT_PRIVKEY=${SC_TLS_CERT_PRIVKEY}
SC_ACME_WEBROOT=${SC_ACME_WEBROOT}
SC_FRONTEND_HEALTHCHECK_PATH=${SC_FRONTEND_HEALTHCHECK_PATH}
SC_BACKEND_HEALTHCHECK_PATH=${SC_BACKEND_HEALTHCHECK_PATH}
SECURITY_CENTER_SCAN_SCANNER_ENABLED=${SC_DAST_ENABLED}
SECURITY_CENTER_SCAN_SCANNER_BASE_URL=${SC_DAST_BASE_URL}
SECURITY_CENTER_SCAN_SCANNER_API_TOKEN=${dast_api_token}
SECURITY_CENTER_SCAN_SCANNER_CALLBACK_TOKEN=${dast_callback_token}
SECURITY_CENTER_SCAN_SCANNER_MAX_TARGETS_PER_BATCH=${SC_DAST_MAX_TARGETS_PER_BATCH}
SECURITY_CENTER_SCAN_SCANNER_MAX_CAMPAIGN_TARGETS=${SC_DAST_MAX_CAMPAIGN_TARGETS}
SECURITY_CENTER_SCAN_SCANNER_TIMEOUT=${SC_DAST_TIMEOUT}
SECURITY_CENTER_SCAN_SCANNER_USER_AGENT=${SC_DAST_USER_AGENT}
SECURITY_CENTER_AGENT_ENABLED=${SC_AGENT_ENABLED}
SECURITY_CENTER_AGENT_KEY_PREFIX=${SC_AGENT_KEY_PREFIX}
SECURITY_CENTER_AGENT_ALLOW_PERMANENT_KEY=${SC_AGENT_ALLOW_PERMANENT_KEY}
SECURITY_CENTER_AGENT_QUOTA_ENABLED=${SC_AGENT_QUOTA_ENABLED}
SECURITY_CENTER_AGENT_DEFAULT_QUOTA_RPM=${SC_AGENT_DEFAULT_QUOTA_RPM}
SECURITY_CENTER_AUDIT_RETENTION_DAYS=${SC_AUDIT_RETENTION_DAYS}
SC_DAST_CA_BUNDLE_HOST_PATH=${SC_DAST_CA_BUNDLE_HOST_PATH}
SC_DAST_CA_BUNDLE_MOUNT_PATH=${SC_DAST_CA_BUNDLE_MOUNT_PATH}
SC_DAST_SSL_CERT_FILE=${SC_DAST_SSL_CERT_FILE}
SC_DAST_USE_PUBLIC_CA=${SC_DAST_USE_PUBLIC_CA}
SC_DAST_SCANNER_SECURITY_GROUP_ID=${SC_DAST_SCANNER_SECURITY_GROUP_ID}
SC_DAST_SCANNER_SOURCE_IP=${SC_DAST_SCANNER_SOURCE_IP}
SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR=${SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR}
EOF
}

prepare_dast_ca_bundle() {
  local output_dir="$1"
  local resolve_ssm="${2:-false}"
  local certs_dir="${output_dir}/certs"
  local enabled_ca_path="${certs_dir}/dast-scanner-ca.crt"
  local disabled_ca_path="${certs_dir}/dast-scanner-ca.disabled.crt"

  mkdir -p "${certs_dir}"
  : >"${disabled_ca_path}"
  export SC_DAST_CA_BUNDLE_HOST_PATH="./certs/dast-scanner-ca.disabled.crt"
  export SC_DAST_SSL_CERT_FILE=""

  if ! dast_enabled; then
    return 0
  fi

  if is_truthy "${SC_DAST_USE_PUBLIC_CA:-false}"; then
    return 0
  fi

  if [[ -z "${SC_DAST_CA_BUNDLE_PARAM:-}" ]]; then
    return 0
  fi

  export SC_DAST_CA_BUNDLE_HOST_PATH="./certs/dast-scanner-ca.crt"
  export SC_DAST_SSL_CERT_FILE="${SC_DAST_CA_BUNDLE_MOUNT_PATH}"
  if [[ "${resolve_ssm}" == "true" ]]; then
    resolve_ssm_param "${SC_DAST_CA_BUNDLE_PARAM}" >"${enabled_ca_path}"
    [[ -s "${enabled_ca_path}" ]] || die "resolved SC_DAST_CA_BUNDLE_PARAM is empty"
  else
    printf '%s\n' '__RESOLVE_FROM_SSM__' >"${enabled_ca_path}"
    export SC_DAST_CA_BUNDLE_HOST_PATH="__RESOLVE_FROM_SSM__"
  fi
}

render_template() {
  local template="$1"
  local output="$2"
  python3 - "$template" "$output" <<'PY'
from pathlib import Path
import os
import sys

template_path = Path(sys.argv[1])
output_path = Path(sys.argv[2])
text = template_path.read_text()
replacements = {
    "{{DOMAIN_NAME}}": os.environ["SC_PUBLIC_HOSTNAME"],
    "{{TLS_CERT_FULLCHAIN}}": os.environ["SC_TLS_CERT_FULLCHAIN"],
    "{{TLS_CERT_PRIVKEY}}": os.environ["SC_TLS_CERT_PRIVKEY"],
    "{{WEBROOT_PATH}}": os.environ["SC_ACME_WEBROOT"],
    "{{FRONTEND_UPSTREAM}}": os.environ["SC_NGINX_FRONTEND_UPSTREAM"],
    "{{BACKEND_UPSTREAM}}": os.environ["SC_NGINX_BACKEND_UPSTREAM"],
}
for needle, repl in replacements.items():
    text = text.replace(needle, repl)
output_path.write_text(text)
PY
}

ensure_test_tls_bundle() {
  local output_dir="$1"
  local tls_dir="${output_dir}/tls"
  local fullchain_path="${tls_dir}/fullchain.pem"
  local privkey_path="${tls_dir}/privkey.pem"

  mkdir -p "${tls_dir}"
  if [[ -s "${fullchain_path}" && -s "${privkey_path}" ]]; then
    export SC_TLS_CERT_FULLCHAIN="${fullchain_path}"
    export SC_TLS_CERT_PRIVKEY="${privkey_path}"
    return 0
  fi

  command -v openssl >/dev/null 2>&1 || die "openssl is required to generate disposable test TLS assets"
  local openssl_cfg
  openssl_cfg="$(mktemp "${tls_dir}/openssl.XXXXXX.cnf")"
  cat >"${openssl_cfg}" <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = req_distinguished_name
x509_extensions = req_ext

[req_distinguished_name]
CN = ${SC_PUBLIC_HOSTNAME}

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = ${SC_PUBLIC_HOSTNAME}
EOF

  openssl req -x509 -nodes -newkey rsa:2048 -days 7 \
    -keyout "${privkey_path}" \
    -out "${fullchain_path}" \
    -config "${openssl_cfg}" >/dev/null 2>&1
  rm -f "${openssl_cfg}"

  export SC_TLS_CERT_FULLCHAIN="${fullchain_path}"
  export SC_TLS_CERT_PRIVKEY="${privkey_path}"
}

write_external_integrations_doc() {
  local path="$1"
  cat >"$path" <<EOF
# Enterprise Production Integration Checklist

Public hostname:
- https://${SC_PUBLIC_HOSTNAME}

Public Auth0 callback:
- ${SC_AUTH0_CALLBACK_URL}

Allowed web origin:
- ${SC_AUTH0_ALLOWED_WEB_ORIGIN}

Allowed logout URL:
- ${SC_AUTH0_ALLOWED_LOGOUT_URL}

Auth0 application checklist:
- Application type supports a regular web app callback to ${SC_AUTH0_CALLBACK_URL}
- Allowed Callback URLs contains ${SC_AUTH0_CALLBACK_URL}
- Allowed Web Origins contains ${SC_AUTH0_ALLOWED_WEB_ORIGIN}
- Allowed Logout URLs contains ${SC_AUTH0_ALLOWED_LOGOUT_URL}
- Auth0 domain is ${SC_AUTH0_DOMAIN}
- Auth0 client ID is ${SC_AUTH0_CLIENT_ID}
- Auth0 audience is ${SC_AUTH0_AUDIENCE}

Google Workspace enterprise connection checklist:
- Configure the enterprise connection inside Auth0, not directly in this app
- Workspace domain matches ${SC_GOOGLE_WORKSPACE_DOMAIN:-"<set SC_GOOGLE_WORKSPACE_DOMAIN>"}
- Redirect chain remains:
  1. Browser -> https://${SC_PUBLIC_HOSTNAME}/auth/login
  2. Auth entry -> Auth0
  3. Auth0 -> Google Workspace enterprise connection
  4. Auth0 -> ${SC_AUTH0_CALLBACK_URL}
  5. Frontend callback page exchanges code against /api/v1/auth/auth0/callback
  6. Callback -> dashboard over https://${SC_PUBLIC_HOSTNAME}

Feishu production checklist:
- Org Sync and alert delivery boundary is ${SC_ALERTS_LARK_BOUNDARY}
- Keep Feishu secrets in AWS SSM Parameter Store under ${SC_SSM_PATH_PREFIX}
- Bootstrap Feishu org-sync and alerts settings from SSM-managed values after deployment
- Do not commit Feishu app credentials into compose files or git-tracked env files

Vulnerability image upload checklist:
- Uploads enabled flag: ${SC_UPLOADS_ENABLED}
- Upload bucket: ${SC_UPLOADS_BUCKET:-<disabled>}
- Upload region: ${SC_UPLOADS_REGION:-<disabled>}
- Upload prefix: ${SC_UPLOADS_PREFIX}
- Upload bucket remains private; browser upload uses presigned PUT and viewing uses presigned GET unless base_url is set
- If uploads are enabled, ensure bucket CORS allows PUT/GET/HEAD from ${SC_UPLOADS_CORS_ALLOWED_ORIGINS}

Scanner integration checklist:
- Scanner enabled flag: ${SC_DAST_ENABLED}
- Scanner base URL: ${SC_DAST_BASE_URL}
- Scanner API token parameter: ${SC_DAST_API_TOKEN_PARAM}
- Scanner callback token parameter: ${SC_DAST_CALLBACK_TOKEN_PARAM}
- Scanner CA bundle parameter: ${SC_DAST_CA_BUNDLE_PARAM:-<public CA or disabled>}
- Scanner public CA mode: ${SC_DAST_USE_PUBLIC_CA}
- Scanner max targets per batch: ${SC_DAST_MAX_TARGETS_PER_BATCH}
- Scanner is deployed independently; this enterprise bundle only configures the asset backend scanner client

Emergency access:
- Normal username/password login remains disabled
- Break-glass enabled flag: ${SC_BREAK_GLASS_ENABLED}
- If break-glass is enabled, activation and CIDR restrictions must be documented separately before rollout
EOF
}

validate_env() {
  local env_file="$1"
  local context="${2:-validate}"
  require_file "$COMPOSE_TEMPLATE"
  require_file "$NGINX_TEMPLATE"
  require_file "$MYSQL_INIT_TEMPLATE"
  load_env_file "$env_file"
  derive_runtime_values
  validate_break_glass_policy
  validate_callback_alignment
  validate_secret_strategy
  validate_upload_settings "${context}"
  validate_dast_settings
  info "enterprise env validation passed"
}

render_bundle() {
  local env_file="$1"
  local output_dir="$2"
  local resolve_ssm="$3"

  validate_env "$env_file" "render"
  mkdir -p "$output_dir"
  if test_tls_mode_enabled; then
    ensure_test_tls_bundle "$output_dir"
  fi
  prepare_dast_ca_bundle "$output_dir" "$resolve_ssm"
  write_runtime_env "${output_dir}/.env.runtime" "$resolve_ssm"
  cp "$COMPOSE_TEMPLATE" "${output_dir}/docker-compose.enterprise.yml"
  cp "$MYSQL_INIT_TEMPLATE" "${output_dir}/mysql-init.sql"
  render_template "$NGINX_TEMPLATE" "${output_dir}/nginx-security-center.conf"
  write_external_integrations_doc "${output_dir}/external-integrations.md"
  print_secret_resolution_summary "${output_dir}/.env.runtime" "${resolve_ssm}"
  info "rendered enterprise bundle into ${output_dir}"
}

print_checklist() {
  cat <<EOF
Enterprise deployment references:
- ${SCRIPT_DIR}/README.md
- ${DOCS_DIR}/PRODUCTION-INTEGRATION-CHECKLIST.md
- ${DOCS_DIR}/ROLLBACK-AND-HEALTHCHECKS.md
- ${SCRIPT_DIR}/remote-deploy-enterprise.sh
- ${SCRIPT_DIR}/diagnose-enterprise.sh

Recommended validation sequence:
1. Write an example env file:
   ${BASH_SOURCE[0]} --write-example-env
2. Fill hostname, Auth0 values, TLS email, and SSM prefix:
   ${SCRIPT_DIR}/enterprise.sh init --env-file /path/to/enterprise.env
3. Provision or reuse the EC2 target:
   ${SCRIPT_DIR}/enterprise.sh provision --env-file /path/to/enterprise.env
4. Point DNS A record to the printed EC2 public IP, then issue the certificate:
   ${SCRIPT_DIR}/enterprise.sh cert --env-file /path/to/enterprise.env
5. Deploy with resolved runtime secrets:
   ${SCRIPT_DIR}/enterprise.sh deploy --env-file /path/to/enterprise.env --resolve-ssm
6. Verify the public endpoint and remote health:
   ${SCRIPT_DIR}/enterprise.sh verify --env-file /path/to/enterprise.env
EOF
}

main() {
  local command="${1:---help}"
  local env_file=""
  local output_dir="$DEFAULT_OUTPUT_DIR"
  local resolve_ssm="false"

  shift || true
  while (($# > 0)); do
    case "$1" in
      --env-file)
        env_file="$2"
        shift 2
        ;;
      --output-dir)
        output_dir="$2"
        shift 2
        ;;
      --resolve-ssm)
        resolve_ssm="true"
        shift
        ;;
      *)
        die "unknown argument: $1"
        ;;
    esac
  done

  case "$command" in
    --help|-h|help)
      usage
      ;;
    --write-example-env)
      write_example_env "$DEFAULT_ENV_TEMPLATE"
      ;;
    --validate)
      [[ -n "$env_file" ]] || die "--validate requires --env-file"
      validate_env "$env_file"
      ;;
    --render)
      [[ -n "$env_file" ]] || die "--render requires --env-file"
      render_bundle "$env_file" "$output_dir" "$resolve_ssm"
      ;;
    --print-checklist)
      print_checklist
      ;;
    *)
      die "unknown command: $command"
      ;;
  esac
}

main "$@"
