#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
DEPLOY_HELPER="${SCRIPT_DIR}/deploy-enterprise.sh"
REMOTE_DEPLOY_HELPER="${SCRIPT_DIR}/remote-deploy-enterprise.sh"
DB_BACKUP_HELPER="${SCRIPT_DIR}/backup-enterprise-db.sh"
DB_MIGRATION_HELPER="${SCRIPT_DIR}/run-db-migrations.sh"
DIAGNOSE_HELPER="${SCRIPT_DIR}/diagnose-enterprise.sh"
PROVISION_HELPER="${SCRIPT_DIR}/provision-enterprise-ec2.sh"
CERT_HELPER="${SCRIPT_DIR}/cert-enterprise-letsencrypt.sh"
CLEANUP_HELPER="${SCRIPT_DIR}/cleanup-enterprise.sh"
SECRETS_HELPER="${SCRIPT_DIR}/write-enterprise-ssm-secrets.sh"
PROMOTE_ADMIN_HELPER="${SCRIPT_DIR}/promote-enterprise-admin.sh"
SCANNER_HANDOFF_HELPER="${SCRIPT_DIR}/scanner-handoff-enterprise.sh"
TRANSPORT_LIB="${SCRIPT_DIR}/lib/transport.sh"
DEFAULT_ENV_FILE="${SCRIPT_DIR}/enterprise.env"
DEFAULT_REMOTE_DIR="/opt/security-center/enterprise"
DEFAULT_SOURCE_DIR="${ROOT_DIR}"
DEFAULT_IDENTITY_FILE="${HOME}/.ssh/id_rsa"
WIZARD_SELECTED_MODE=""

# shellcheck disable=SC1090
source "${TRANSPORT_LIB}"

usage() {
  cat <<'EOF'
Security Center enterprise entrypoint

Recommended:
  ./scripts/enterprise/enterprise.sh

Subcommands:
  wizard                 Interactive guided flow (default)
  init                   Create or update an enterprise env file interactively
  provision              Create or reuse the AWS EC2 target and supporting AWS resources
  secrets                Interactively write MySQL/Redis/JWT/Auth0/Feishu values into AWS SSM Parameter Store
  scanner-export         Export asset-platform handoff values for the independently deployed scanner
  scanner-config         Import scanner handoff values and update the enterprise env file
  scanner-ingress        Authorize scanner source IP to call the asset platform callback endpoint
  rollout                Config-driven deploy flow: validate -> optional secrets -> provision -> deploy -> verify
  scanner-rollout        Apply scanner handoff -> authorize callback ingress -> redeploy -> verify
  promote-admin          Promote a specific user to the admin role by email
  validate               Validate an env file
  render                 Render deployment assets
  cert                   Issue or renew a Let's Encrypt certificate on the EC2 host
  deploy                 Remote deploy using prebuilt image tags and run DB migrations before backend startup
  build-deploy           Remote deploy and build backend/frontend images on EC2, then run DB migrations before backend startup
  db-backup              Run a remote MySQL logical backup on the EC2 host and upload it to a dedicated S3 bucket
  bootstrap              Guided first-time rollout: provision -> cert -> deploy -> verify
  inventory              Print deployment resource inventory/cleanup plan without deleting resources
  cleanup                Destroy the enterprise sandbox resources and remote certs
  verify                 Verify the deployed public endpoint and remote health
  preflight              Check remote deploy readiness without starting compose
  diagnose               Run remote diagnostics
  checklist              Print helper/checklist references
  profiles               List local AWS CLI profiles
  help                   Show this help

Global options:
  --env-file <path>      Enterprise env file. Default: scripts/enterprise/enterprise.env
  --profile <name>       AWS CLI profile to use
  --region <name>        AWS region override
  --transport <mode>     Remote transport: ssm (default) or ssh
  --instance-id <id>     EC2 instance ID for ssm transport
  --host <user@host>     SSH target for ssh transport
  --identity-file <path> SSH private key for ssh transport
  --remote-dir <path>    Remote deployment root. Default: /opt/security-center/enterprise
  --source-dir <path>    Repo source directory for remote builds. Default: repository root
  --email <address>      User email for commands that operate on a specific user, such as promote-admin
  --output-file <path>   scanner-export: write handoff output to this file instead of stdout
  --handoff-file <path>  scanner-config: scanner-to-asset handoff env file
  --apply                scanner-config: apply env updates. Without this, scanner-config is dry-run
  --allow-token-param-update
                         scanner-config: allow scanner handoff to update token parameter names
  --generate-missing-secrets
                         secrets/rollout: generate missing deploy secrets that have safe generators
  --s3-bucket <name>     S3 bucket used for artifact staging in ssm mode
  --s3-prefix <prefix>   S3 object prefix used for artifact staging in ssm mode
  --backup-s3-bucket <name>
                         S3 bucket used for enterprise DB backups
  --backup-s3-prefix <prefix>
                         S3 object prefix used for enterprise DB backups
  --backup-s3-region <name>
                         AWS region for the enterprise DB backup bucket
  --resolve-ssm          Resolve SSM parameters during render/deploy
  --dns-ready            Skip the interactive DNS confirmation pause during bootstrap
  --non-interactive      Do not prompt; fail on missing inputs
  --dry-run              Dry-run for deploy/build-deploy
  --skip-db-migrations    Skip the database migration phase during deploy/build-deploy
  --skip-db-backup       Skip the automatic pre-deploy database backup (not recommended)
  --allow-schema-drift    Override the conservative schema-sensitive release gate
  --skip-upload          Reuse uploaded rendered bundle for deploy/build-deploy
  --skip-build           Reuse remote source/images for build-deploy
  --skip-up              Upload/prepare only, do not compose up
  --force                Execute destructive cleanup operations
  --yes                  Skip the interactive cleanup confirmation
  --keep-certs           Cleanup: keep Let's Encrypt certificate files
  --revoke-certs         Cleanup: revoke the Let's Encrypt certificate before deleting local files
  --keep-buckets         Cleanup: keep artifact/uploads buckets
  --keep-ssm             Cleanup: keep SSM parameters
  --help                 Show this help
EOF
}

info() { echo "INFO: $*"; }
warn() { echo "WARN: $*" >&2; }
die() { echo "ERROR: $*" >&2; exit 1; }

format_elapsed_seconds() {
  local seconds="${1:-0}"
  local minutes=$((seconds / 60))
  local remainder=$((seconds % 60))
  if (( minutes > 0 )); then
    printf '%sm%ss' "${minutes}" "${remainder}"
  else
    printf '%ss' "${remainder}"
  fi
}

run_logged_stage() {
  local label="$1"
  shift
  local started_at finished_at elapsed
  started_at="$(date +%s)"
  info "starting: ${label}"
  "$@"
  finished_at="$(date +%s)"
  elapsed=$((finished_at - started_at))
  info "completed: ${label} ($(format_elapsed_seconds "${elapsed}"))"
}

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

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

env_truthy() {
  local key="$1"
  local default="${2:-false}"
  local value
  value="$(trim "$(get_env_value "${ENV_FILE}" "${key}")")"
  [[ -z "${value}" ]] && value="${default}"
  is_truthy_value "${value}"
}

image_reference_is_mutable() {
  local image_ref
  image_ref="$(trim "${1:-}")"
  [[ -n "${image_ref}" ]] || return 0
  [[ "${image_ref}" == *@* ]] && return 1

  local remainder="${image_ref##*/}"
  if [[ "${remainder}" != *:* ]]; then
    return 0
  fi

  local tag="${remainder##*:}"
  [[ "${tag}" == "latest" ]]
}

warn_on_mutable_deploy_images() {
  local backend_image frontend_image
  backend_image="$(trim "$(get_env_value "${ENV_FILE}" "SC_BACKEND_IMAGE")")"
  frontend_image="$(trim "$(get_env_value "${ENV_FILE}" "SC_FRONTEND_IMAGE")")"

  if image_reference_is_mutable "${backend_image}"; then
    warn "SC_BACKEND_IMAGE uses a mutable reference (${backend_image:-<empty>}). deploy will pull before compose up, but production should prefer an immutable tag or digest for traceable rollouts."
  fi
  if image_reference_is_mutable "${frontend_image}"; then
    warn "SC_FRONTEND_IMAGE uses a mutable reference (${frontend_image:-<empty>}). deploy will pull before compose up, but production should prefer an immutable tag or digest for traceable rollouts."
  fi
}

enforce_production_deploy_image_policy() {
  local environment_mode backend_image frontend_image
  environment_mode="$(infer_environment_mode)"
  [[ "${environment_mode}" == "prod" ]] || return 0

  backend_image="$(trim "$(get_env_value "${ENV_FILE}" "SC_BACKEND_IMAGE")")"
  frontend_image="$(trim "$(get_env_value "${ENV_FILE}" "SC_FRONTEND_IMAGE")")"

  if image_reference_is_mutable "${backend_image}" || image_reference_is_mutable "${frontend_image}"; then
    die "production deploy with prebuilt images requires immutable backend/frontend references. Use an explicit version tag or digest, or switch to ./scripts/enterprise/enterprise.sh build-deploy --resolve-ssm ..."
  fi
}

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

hostname_resolves_to_ip() {
  local hostname="$1"
  local expected_ip="$2"
  python3 - "$hostname" "$expected_ip" <<'PY'
import socket
import sys

hostname = sys.argv[1]
expected = sys.argv[2]

try:
    _, _, addresses = socket.gethostbyname_ex(hostname)
except OSError:
    raise SystemExit(1)

if expected in addresses:
    raise SystemExit(0)
raise SystemExit(1)
PY
}

wait_for_dns_propagation() {
  local hostname="$1"
  local expected_ip="$2"
  local timeout_seconds="${3:-600}"
  local poll_interval="${4:-10}"

  [[ -n "${hostname}" ]] || die "hostname is required for DNS propagation checks"
  [[ -n "${expected_ip}" ]] || die "expected IP is required for DNS propagation checks"

  local elapsed=0
  while true; do
    if hostname_resolves_to_ip "${hostname}" "${expected_ip}"; then
      info "DNS is live: ${hostname} resolves to ${expected_ip}"
      return 0
    fi

    if (( elapsed >= timeout_seconds )); then
      die "DNS for ${hostname} still does not point to ${expected_ip} after $(format_elapsed_seconds "${timeout_seconds}"); update the A record and rerun bootstrap or cert"
    fi

    info "waiting for DNS propagation: ${hostname} -> ${expected_ip} (elapsed $(format_elapsed_seconds "${elapsed}"))"
    sleep "${poll_interval}"
    elapsed=$((elapsed + poll_interval))
  done
}

print_secret_state_summary() {
  local mode="$1"
  local state=""
  case "${mode}" in
    render)
      if [[ "${RESOLVE_SSM}" == "true" ]]; then
        state="resolved during render"
      else
        state="deferred placeholders allowed for render-only output"
      fi
      ;;
    deploy|build-deploy)
      if [[ "${RESOLVE_SSM}" == "true" ]]; then
        state="resolved during deploy/build-deploy"
      else
        state="blocked: deploy/build-deploy require resolved runtime secrets"
      fi
      ;;
    *)
      state="unknown"
      ;;
  esac

  info "secret state summary:"
  info "  - runtime compose secrets: ${state}"
  info "  - Feishu app secrets: deferred to post-deploy application settings bootstrap from SSM"
}

detect_tls_mode() {
  local env_file="$1"
  local hostname cert_fullchain
  hostname="$(get_env_value "${env_file}" "SC_PUBLIC_HOSTNAME")"
  cert_fullchain="$(get_env_value "${env_file}" "SC_TLS_CERT_FULLCHAIN")"
  if [[ -n "${hostname}" && ( -z "${cert_fullchain}" || "${cert_fullchain}" == "/etc/letsencrypt/live/${hostname}/fullchain.pem" ) ]]; then
    printf '%s' "Let's Encrypt / host-mounted cert files"
  else
    printf '%s' "custom host-mounted cert files"
  fi
}

print_deployment_plan_summary() {
  local mode="$1"
  local next_action="$2"
  local tls_mode
  tls_mode="$(detect_tls_mode "${ENV_FILE}")"
  info "deployment summary:"
  info "  - aws profile: ${AWS_PROFILE_NAME:-<not set>}"
  info "  - aws region: ${AWS_REGION_NAME:-<not set>}"
  info "  - transport: ${TRANSPORT}"
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    info "  - instance id: ${INSTANCE_ID:-<not resolved>}"
    info "  - artifact staging: s3://${S3_BUCKET:-<missing>}/${S3_PREFIX:-<missing>}"
  else
    info "  - remote host: ${REMOTE_HOST:-<not resolved>}"
  fi
  info "  - deploy mode: ${mode}"
  info "  - TLS mode assumption: ${tls_mode}"
  if [[ "${RESOLVE_SSM}" == "true" ]]; then
    info "  - runtime secret state: resolved during ${mode}"
  else
    info "  - runtime secret state: blocked until resolved"
  fi
  if [[ "${SKIP_DB_MIGRATIONS}" == "true" ]]; then
    info "  - database migration phase: skipped by operator request"
  else
    info "  - database migration phase: run before backend startup"
  fi
  info "  - next action: ${next_action}"
}

is_schema_sensitive_path() {
  case "$1" in
    *_test.go)
      return 1
      ;;
    security-center-backend/internal/model/*)
      return 0
      ;;
    security-center-backend/internal/modules/*/model/*)
      return 0
      ;;
    security-center-backend/internal/infra/mysql/*)
      return 0
      ;;
    security-center-backend/migrations/*)
      return 0
      ;;
    scripts/enterprise/mysql-init.sql)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

is_new_migration_file() {
  case "$1" in
    security-center-backend/migrations/*.sql)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

enforce_schema_migration_gate() {
  if [[ "${ALLOW_SCHEMA_DRIFT}" == "true" ]]; then
    warn "schema-sensitive release gate overridden by --allow-schema-drift"
    return 0
  fi

  require_cmd git

  local -a sensitive_paths=()
  local -a migration_files=()
  local line status path

  while IFS= read -r line; do
    [[ -z "${line}" ]] && continue
    status="${line:0:2}"
    path="${line:3}"
    path="${path%% -> *}"
    if is_schema_sensitive_path "${path}"; then
      sensitive_paths+=("${path}")
    fi
    case "${status}" in
      A*|\?\?)
        if is_new_migration_file "${path}"; then
          migration_files+=("${path}")
        fi
        ;;
    esac
  done < <(git -C "${ROOT_DIR}" status --porcelain=v1 --untracked-files=normal)

  if ((${#sensitive_paths[@]} == 0)); then
    info "schema-sensitive release gate: no backend schema-sensitive changes detected"
    return 0
  fi

  if ((${#migration_files[@]} == 0)); then
    printf 'INFO: schema-sensitive release gate saw these paths:\n' >&2
    printf 'INFO:   - %s\n' "${sensitive_paths[@]}" >&2
    die "schema-sensitive changes detected without a new migration file; add a new SQL migration under security-center-backend/migrations or rerun with --allow-schema-drift for an explicit emergency override"
  fi

  info "schema-sensitive release gate: sensitive paths detected and new migration files present"
  info "  - sensitive paths: ${sensitive_paths[*]}"
  info "  - migration files: ${migration_files[*]}"
}

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

prompt_value() {
  local __var="$1"
  local message="$2"
  local default="${3:-}"
  local value=""

  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    if [[ -z "${default}" ]]; then
      die "missing required value in non-interactive mode: ${message}"
    fi
    printf -v "$__var" '%s' "$default"
    return 0
  fi

  if [[ -n "${default}" ]]; then
    printf "%s [%s]: " "$message" "$default"
  else
    printf "%s: " "$message"
  fi
  IFS= read -r value || true
  value="$(trim "$value")"
  if [[ -z "${value}" ]]; then
    value="${default}"
  fi
  printf -v "$__var" '%s' "$value"
}

prompt_optional_value() {
  local __var="$1"
  local message="$2"
  local default="${3:-}"
  local value=""

  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    printf -v "$__var" '%s' "$default"
    return 0
  fi

  if [[ -n "${default}" ]]; then
    printf "%s [%s]: " "$message" "$default"
  else
    printf "%s: " "$message"
  fi
  IFS= read -r value || true
  value="$(trim "$value")"
  if [[ -z "${value}" ]]; then
    value="${default}"
  fi
  printf -v "$__var" '%s' "$value"
}

ensure_region_selected() {
  local default_region="${AWS_REGION_NAME:-$(get_env_value "${ENV_FILE}" "SC_AWS_REGION")}"
  if [[ -n "${default_region}" ]]; then
    AWS_REGION_NAME="${default_region}"
    return 0
  fi
  prompt_value AWS_REGION_NAME "AWS region" "eu-west-2"
}

prompt_yes_no() {
  local __var="$1"
  local message="$2"
  local default="${3:-y}"
  local answer=""

  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    case "${default}" in
      y|Y|yes|YES|true) printf -v "$__var" '%s' "true" ;;
      *) printf -v "$__var" '%s' "false" ;;
    esac
    return 0
  fi

  while true; do
    printf "%s [%s]: " "$message" "$default"
    IFS= read -r answer || true
    answer="$(trim "$answer")"
    [[ -z "${answer}" ]] && answer="${default}"
    case "${answer}" in
      y|Y|yes|YES|true|TRUE) printf -v "$__var" '%s' "true"; return 0 ;;
      n|N|no|NO|false|FALSE) printf -v "$__var" '%s' "false"; return 0 ;;
      *) warn "Please answer y or n." ;;
    esac
  done
}

list_profiles() {
  if ! command -v aws >/dev/null 2>&1; then
    warn "aws CLI not found; cannot list profiles"
    return 0
  fi
  aws configure list-profiles 2>/dev/null || true
}

choose_profile() {
  local current="${AWS_PROFILE_NAME:-}"
  local profiles=""
  profiles="$(list_profiles)"

  if [[ -n "${current}" ]]; then
    return 0
  fi

  if [[ -z "${profiles}" ]]; then
    if [[ "${NON_INTERACTIVE}" == "true" ]]; then
      return 0
    fi
    prompt_value AWS_PROFILE_NAME "AWS CLI profile (optional)" ""
    return 0
  fi

  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    return 0
  fi

  echo "Available AWS CLI profiles:"
  local idx=1
  local profile=""
  while IFS= read -r profile; do
    [[ -z "${profile}" ]] && continue
    printf "  %d. %s\n" "${idx}" "${profile}"
    idx=$((idx + 1))
  done <<< "${profiles}"
  prompt_value profile_choice "Select profile by name (or enter a new name)" "$(printf '%s\n' "${profiles}" | head -n1)"
  AWS_PROFILE_NAME="${profile_choice}"
}

ensure_aws_session() {
  [[ -n "${AWS_PROFILE_NAME}" ]] || return 0
  require_cmd aws
  export AWS_PROFILE="${AWS_PROFILE_NAME}"
  export AWS_SDK_LOAD_CONFIG=1

  if [[ -n "${AWS_REGION_NAME}" ]]; then
    export AWS_REGION="${AWS_REGION_NAME}"
    export AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  fi

  if aws sts get-caller-identity --profile "${AWS_PROFILE_NAME}" >/dev/null 2>&1; then
    return 0
  fi

  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    die "AWS profile '${AWS_PROFILE_NAME}' is not logged in; run 'aws sso login --profile ${AWS_PROFILE_NAME}' first"
  fi

  info "AWS session is not active for profile '${AWS_PROFILE_NAME}'. Running aws sso login..."
  aws sso login --profile "${AWS_PROFILE_NAME}"
  aws sts get-caller-identity --profile "${AWS_PROFILE_NAME}" >/dev/null 2>&1 || die "AWS login failed for profile '${AWS_PROFILE_NAME}'"
}

ensure_ssm_target_ready() {
  [[ "${TRANSPORT}" == "ssm" ]] || return 0
  [[ -n "${INSTANCE_ID}" ]] || die "remote instance must be resolved before readiness check"
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "dry-run: skipping EC2/SSM readiness check for ${INSTANCE_ID}"
    return 0
  fi
  require_cmd aws

  local state
  state="$(aws ec2 describe-instances --instance-ids "${INSTANCE_ID}" \
    --query 'Reservations[0].Instances[0].State.Name' --output text 2>/dev/null || true)"
  if [[ -z "${state}" || "${state}" == "None" ]]; then
    die "cannot describe instance ${INSTANCE_ID}; check --profile/--region and IAM permissions"
  fi
  [[ "${state}" == "running" ]] || die "instance ${INSTANCE_ID} is '${state}', expected 'running'"

  local ping
  ping="$(aws ssm describe-instance-information \
    --filters "Key=InstanceIds,Values=${INSTANCE_ID}" \
    --query 'InstanceInformationList[0].PingStatus' --output text 2>/dev/null || true)"
  [[ "${ping}" == "Online" ]] || die "instance ${INSTANCE_ID} SSM agent is not Online (got '${ping:-unknown}'); start the SSM agent and attach AmazonSSMManagedInstanceCore to the instance role, or retry with --transport ssh"
  info "instance ${INSTANCE_ID} is running and SSM agent is Online"
}

get_env_value() {
  local env_file="$1"
  local key="$2"
  [[ -f "${env_file}" ]] || return 0
  python3 - "$env_file" "$key" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
key = sys.argv[2]
for raw in path.read_text().splitlines():
    line = raw.strip()
    if not line or line.startswith("#") or "=" not in line:
        continue
    lhs, rhs = line.split("=", 1)
    if lhs.strip() == key:
        print(rhs.strip())
        break
PY
}

sync_ssm_parameter_names() {
  [[ -f "${ENV_FILE}" ]] || return 0

  local prefix current_prefix_trimmed updated="false"
  prefix="$(trim "$(get_env_value "${ENV_FILE}" "SC_SSM_PATH_PREFIX")")"
  [[ -n "${prefix}" ]] || return 0
  current_prefix_trimmed="${prefix%/}"

  if [[ "$(trim "$(get_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE")")" != "" ]]; then
    :
  elif [[ "$(trim "$(get_env_value "${ENV_FILE}" "SC_AUTH0_DOMAIN")")" == "placeholder.auth0.local" ]]; then
    set_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE" "true"
    updated="true"
  fi

  local -a keys=(
    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
    SC_SSM_PARAM_AUTH0_CLIENT_SECRET
    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
    SC_DAST_API_TOKEN_PARAM
    SC_DAST_CALLBACK_TOKEN_PARAM
  )
  local -a suffixes=(
    /mysql/root_password
    /mysql/app_password
    /redis/password
    /jwt/access_secret
    /jwt/refresh_secret
    /auth0/client_secret
    /feishu/org_sync/app_id
    /feishu/org_sync/app_secret
    /feishu/alerts/app_id
    /feishu/alerts/app_secret
    /dast/api-token
    /dast/callback-token
  )
  local idx=0 expected existing
  for idx in "${!keys[@]}"; do
    expected="${current_prefix_trimmed}${suffixes[$idx]}"
    existing="$(trim "$(get_env_value "${ENV_FILE}" "${keys[$idx]}")")"
    if [[ "${existing}" != "${expected}" ]]; then
      set_env_value "${ENV_FILE}" "${keys[$idx]}" "${expected}"
      updated="true"
    fi
  done

  if [[ "${updated}" == "true" ]]; then
    info "synced SSM parameter names in ${ENV_FILE} to prefix ${current_prefix_trimmed}"
  fi
}

load_env_runtime_defaults() {
  [[ -f "${ENV_FILE}" ]] || return 0

  sync_ssm_parameter_names

  if [[ -z "${TRANSPORT}" || "${TRANSPORT}" == "ssm" ]]; then
    local env_transport
    env_transport="$(trim "$(get_env_value "${ENV_FILE}" "SC_REMOTE_TRANSPORT")")"
    [[ -n "${env_transport}" ]] && TRANSPORT="${env_transport}"
  fi

  if [[ -z "${IDENTITY_FILE}" ]]; then
    local env_identity
    env_identity="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_IDENTITY_FILE")")"
    if [[ -n "${env_identity}" ]]; then
      env_identity="$(resolve_path "${env_identity}")"
      if [[ -f "${env_identity}" ]]; then
        IDENTITY_FILE="${env_identity}"
      fi
    fi
  fi

  if [[ "${SSH_USER}" == "ec2-user" ]]; then
    local env_ssh_user
    env_ssh_user="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_SSH_USER")")"
    [[ -n "${env_ssh_user}" ]] && SSH_USER="${env_ssh_user}"
  fi

  if [[ -z "${INSTANCE_ID}" ]]; then
    local env_instance_id
    env_instance_id="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_ID")")"
    [[ -n "${env_instance_id}" ]] && INSTANCE_ID="${env_instance_id}"
  fi

  if [[ -z "${S3_BUCKET}" ]]; then
    local env_s3_bucket
    env_s3_bucket="$(trim "$(get_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_BUCKET")")"
    [[ -n "${env_s3_bucket}" ]] && S3_BUCKET="${env_s3_bucket}"
  fi

  if [[ -z "${S3_PREFIX}" ]]; then
    local env_s3_prefix
    env_s3_prefix="$(trim "$(get_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_PREFIX")")"
    [[ -n "${env_s3_prefix}" ]] && S3_PREFIX="${env_s3_prefix}"
  fi
  if [[ -z "${S3_PREFIX}" ]]; then
    S3_PREFIX="security-center-enterprise"
  fi

  if [[ -z "${DB_BACKUP_S3_BUCKET}" ]]; then
    local env_backup_bucket
    env_backup_bucket="$(trim "$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_BUCKET")")"
    [[ -n "${env_backup_bucket}" ]] && DB_BACKUP_S3_BUCKET="${env_backup_bucket}"
  fi

  if [[ -z "${DB_BACKUP_S3_PREFIX}" ]]; then
    local env_backup_prefix
    env_backup_prefix="$(trim "$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_PREFIX")")"
    [[ -n "${env_backup_prefix}" ]] && DB_BACKUP_S3_PREFIX="${env_backup_prefix}"
  fi
  if [[ -z "${DB_BACKUP_S3_PREFIX}" ]]; then
    DB_BACKUP_S3_PREFIX="security-center-enterprise"
  fi

  if [[ -z "${DB_BACKUP_S3_REGION}" ]]; then
    local env_backup_region
    env_backup_region="$(trim "$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_REGION")")"
    [[ -n "${env_backup_region}" ]] && DB_BACKUP_S3_REGION="${env_backup_region}"
  fi
  if [[ -z "${DB_BACKUP_S3_REGION}" ]]; then
    DB_BACKUP_S3_REGION="${AWS_REGION_NAME:-$(trim "$(get_env_value "${ENV_FILE}" "SC_AWS_REGION")")}"
  fi

  if [[ -z "${REMOTE_HOST}" && "${TRANSPORT}" == "ssh" ]]; then
    local env_public_ip
    env_public_ip="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_PUBLIC_IP")")"
    if [[ -n "${env_public_ip}" ]]; then
      REMOTE_HOST="${SSH_USER}@${env_public_ip}"
    fi
  fi
}

set_env_value() {
  local env_file="$1"
  local key="$2"
  local value="$3"
  python3 - "$env_file" "$key" "$value" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
key = sys.argv[2]
value = sys.argv[3]
lines = []
found = False
if path.exists():
    lines = path.read_text().splitlines()

new_lines = []
for raw in lines:
    if raw.strip().startswith(f"{key}="):
        new_lines.append(f"{key}={value}")
        found = True
    else:
        new_lines.append(raw)
if not found:
    if new_lines and new_lines[-1].strip():
      new_lines.append("")
    new_lines.append(f"{key}={value}")
path.write_text("\n".join(new_lines) + "\n")
PY
}

fetch_instance_catalog() {
  local region="$1"
  require_cmd aws
  local raw_json=""
  raw_json="$(aws ec2 describe-instances \
    --region "${region}" \
    --filters "Name=instance-state-name,Values=running" \
    --query "Reservations[].Instances[].{instance_id:InstanceId,public_ip:PublicIpAddress,public_dns:PublicDnsName,name:Tags[?Key=='Name']|[0].Value}" \
    --output json 2>/dev/null || true)"
  [[ -n "${raw_json}" ]] || return 0
  python3 - "${raw_json}" <<'PY'
import json
import sys

try:
    data = json.loads(sys.argv[1])
except json.JSONDecodeError:
    sys.exit(0)

def norm(value):
    if value in (None, "None"):
        return ""
    return str(value)

for item in data:
    instance_id = norm(item.get("instance_id"))
    name = norm(item.get("name"))
    public_ip = norm(item.get("public_ip"))
    public_dns = norm(item.get("public_dns"))
    preferred = public_ip or public_dns
    print("\t".join([instance_id, name, public_ip, public_dns, preferred]))
PY
}

choose_remote_target() {
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    [[ -n "${INSTANCE_ID}" ]] && return 0
  else
    [[ -n "${REMOTE_HOST}" ]] && return 0
  fi
  [[ -n "${AWS_PROFILE_NAME}" || -n "${INSTANCE_ID}" ]] || return 0

  local region="${AWS_REGION_NAME:-${AWS_REGION:-}}"
  [[ -n "${region}" ]] || region="eu-west-2"

  local instances=""
  instances="$(fetch_instance_catalog "${region}" || true)"

  if [[ -z "${instances}" ]]; then
    if [[ "${NON_INTERACTIVE}" == "true" ]]; then
      if [[ "${TRANSPORT}" == "ssm" ]]; then
        die "no running EC2 instance candidates found; pass --instance-id explicitly"
      fi
      die "no running EC2 instance candidates found; pass --host explicitly"
    fi
    if [[ "${TRANSPORT}" == "ssm" ]]; then
      prompt_value instance_id_prompt "EC2 instance ID" ""
      [[ -n "${instance_id_prompt}" ]] || die "EC2 instance ID is required"
      INSTANCE_ID="${instance_id_prompt}"
      return 0
    fi
    prompt_value ssh_user "SSH user" "${SSH_USER}"
    prompt_value ssh_host "SSH host or public IP" ""
    [[ -n "${ssh_host}" ]] || die "SSH host is required"
    REMOTE_HOST="${ssh_user}@${ssh_host}"
    return 0
  fi

  if [[ -n "${INSTANCE_ID}" ]]; then
    local selected_by_id=""
    selected_by_id="$(printf '%s\n' "${instances}" | awk -F '\t' -v wanted="${INSTANCE_ID}" '$1 == wanted {print; exit}')"
    [[ -n "${selected_by_id}" ]] || die "EC2 instance not found or not running: ${INSTANCE_ID}"
    if [[ "${TRANSPORT}" == "ssm" ]]; then
      return 0
    fi
    local preferred_host
    preferred_host="$(printf '%s\n' "${selected_by_id}" | awk -F '\t' '{print $5}')"
    [[ -n "${preferred_host}" ]] || die "selected instance does not have a public IP/DNS; pass --host explicitly"
    REMOTE_HOST="${SSH_USER}@${preferred_host}"
    return 0
  fi

  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    if [[ "${TRANSPORT}" == "ssm" ]]; then
      die "non-interactive mode requires --instance-id for ssm transport"
    fi
    die "non-interactive mode requires --host for ssh transport"
  fi

  echo "Running EC2 instances in ${region}:"
  local i=1
  local line=""
  while IFS= read -r line; do
    [[ -z "${line}" ]] && continue
    local instance_id name public_ip public_dns preferred_host
    instance_id="$(printf '%s\n' "${line}" | awk -F '\t' '{print $1}')"
    name="$(printf '%s\n' "${line}" | awk -F '\t' '{print $2}')"
    public_ip="$(printf '%s\n' "${line}" | awk -F '\t' '{print $3}')"
    public_dns="$(printf '%s\n' "${line}" | awk -F '\t' '{print $4}')"
    preferred_host="$(printf '%s\n' "${line}" | awk -F '\t' '{print $5}')"
    printf "  %d. %s | name=%s | public_ip=%s | public_dns=%s | preferred=%s\n" \
      "${i}" "${instance_id}" "${name:-<none>}" "${public_ip:-<none>}" "${public_dns:-<none>}" "${preferred_host:-<none>}"
    i=$((i + 1))
  done <<< "${instances}"

  if [[ "${TRANSPORT}" == "ssm" ]]; then
    prompt_value instance_choice "Choose a row number, or press Enter to type an instance ID manually" ""
  else
    prompt_value instance_choice "Choose a row number, or press Enter to type a host manually" ""
  fi
  if [[ -z "${instance_choice}" ]]; then
    if [[ "${TRANSPORT}" == "ssm" ]]; then
      prompt_value instance_id_manual "EC2 instance ID" ""
      [[ -n "${instance_id_manual}" ]] || die "EC2 instance ID is required"
      INSTANCE_ID="${instance_id_manual}"
      return 0
    fi
    prompt_value ssh_user "SSH user" "${SSH_USER}"
    prompt_value ssh_host "SSH host or public IP" ""
    [[ -n "${ssh_host}" ]] || die "SSH host is required"
    REMOTE_HOST="${ssh_user}@${ssh_host}"
    return 0
  fi

  local selected=""
  selected="$(printf '%s\n' "${instances}" | sed -n "${instance_choice}p")"
  [[ -n "${selected}" ]] || die "invalid instance selection: ${instance_choice}"
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    INSTANCE_ID="$(printf '%s\n' "${selected}" | awk -F '\t' '{print $1}')"
    [[ -n "${INSTANCE_ID}" ]] || die "selected instance is missing an instance ID"
    return 0
  fi
  local ssh_target
  ssh_target="$(printf '%s\n' "${selected}" | awk -F '\t' '{print $5}')"
  [[ -n "${ssh_target}" ]] || die "selected instance does not have a public IP/DNS; pass --host explicitly"
  REMOTE_HOST="${SSH_USER}@${ssh_target}"
}

ensure_env_file() {
  ENV_FILE="$(resolve_path "${ENV_FILE}")"
  if [[ -f "${ENV_FILE}" ]]; then
    return 0
  fi
  info "creating enterprise env file at ${ENV_FILE}"
  mkdir -p "$(dirname "${ENV_FILE}")"
  "${DEPLOY_HELPER}" --write-example-env
  cp "${SCRIPT_DIR}/enterprise.env.example" "${ENV_FILE}"
}

infer_environment_mode() {
  local explicit_mode sandbox_mode ssm_prefix
  explicit_mode="$(trim "$(get_env_value "${ENV_FILE}" "SC_ENVIRONMENT_MODE")")"
  case "${explicit_mode}" in
    test|prod)
      printf '%s' "${explicit_mode}"
      return 0
      ;;
  esac

  sandbox_mode="$(trim "$(get_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE")")"
  if [[ "${sandbox_mode}" == "true" ]]; then
    printf '%s' "test"
    return 0
  fi

  ssm_prefix="$(trim "$(get_env_value "${ENV_FILE}" "SC_SSM_PATH_PREFIX")")"
  if [[ "${ssm_prefix}" == *"/test"* || "${ssm_prefix}" == *"/sandbox"* ]]; then
    printf '%s' "test"
    return 0
  fi

  printf '%s' "prod"
}

interactive_init() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  elif [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" == "true" ]]; then
    warn "skipping aws sso login because --dry-run was requested"
  fi

  local environment_mode environment_mode_default ssm_prefix_default use_env_defaults="false"
  environment_mode_default="$(infer_environment_mode)"
  if [[ -n "${WIZARD_SELECTED_MODE}" ]]; then
    environment_mode="${WIZARD_SELECTED_MODE}"
  else
    prompt_value environment_mode "Environment mode (test or prod)" "${environment_mode_default}"
  fi
  case "${environment_mode}" in
    test|prod)
      ;;
    *)
      die "environment mode must be test or prod"
      ;;
  esac

  if [[ "${NON_INTERACTIVE}" != "true" && -f "${ENV_FILE}" ]]; then
    prompt_yes_no use_env_defaults "Use current enterprise.env defaults and only change values later if needed" "y"
  fi

  local public_hostname tls_acme_email ssm_prefix transport_mode ec2_instance_type ec2_volume_size
  local s3_artifact_bucket s3_artifact_prefix uploads_enabled uploads_bucket uploads_region uploads_prefix uploads_cors_origins
  local db_backup_bucket db_backup_prefix db_backup_region
  local enable_ssh_fallback deploy_workflow_default remote_build_default remote_build backend_image frontend_image
  local auth0_domain auth0_client_id auth0_audience workspace_domain sandbox_auth_mode

  public_hostname="$(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")"
  tls_acme_email="$(get_env_value "${ENV_FILE}" "SC_TLS_ACME_EMAIL")"
  ssm_prefix_default="$(get_env_value "${ENV_FILE}" "SC_SSM_PATH_PREFIX")"
  transport_mode="$(get_env_value "${ENV_FILE}" "SC_REMOTE_TRANSPORT")"
  ec2_instance_type="$(get_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_TYPE")"
  ec2_volume_size="$(get_env_value "${ENV_FILE}" "SC_EC2_VOLUME_SIZE_GB")"
  s3_artifact_bucket="$(get_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_BUCKET")"
  s3_artifact_prefix="$(get_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_PREFIX")"
  db_backup_bucket="$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_BUCKET")"
  db_backup_prefix="$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_PREFIX")"
  db_backup_region="$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_REGION")"
  uploads_enabled="$(get_env_value "${ENV_FILE}" "SC_UPLOADS_ENABLED")"
  uploads_bucket="$(get_env_value "${ENV_FILE}" "SC_UPLOADS_BUCKET")"
  uploads_region="$(get_env_value "${ENV_FILE}" "SC_UPLOADS_REGION")"
  uploads_prefix="$(get_env_value "${ENV_FILE}" "SC_UPLOADS_PREFIX")"
  uploads_cors_origins="$(get_env_value "${ENV_FILE}" "SC_UPLOADS_CORS_ALLOWED_ORIGINS")"
  enable_ssh_fallback="$(get_env_value "${ENV_FILE}" "SC_ENABLE_SSH_FALLBACK")"
  backend_image="$(get_env_value "${ENV_FILE}" "SC_BACKEND_IMAGE")"
  frontend_image="$(get_env_value "${ENV_FILE}" "SC_FRONTEND_IMAGE")"

  if [[ "${use_env_defaults}" != "true" ]]; then
    prompt_value public_hostname "Public hostname" "${public_hostname}"
  else
    info "using current enterprise.env defaults for hostname, transport, EC2 sizing, S3 settings, and deploy workflow"
  fi

  if [[ "${environment_mode}" == "test" ]]; then
    auth0_domain="placeholder.auth0.local"
    auth0_client_id="placeholder-client-id"
    auth0_audience="https://api.${public_hostname}"
    workspace_domain="placeholder.local"
    sandbox_auth_mode="true"
    info "test mode selected: Auth0/Google values will be written as placeholders and can be replaced later"
  else
    auth0_domain="$(get_env_value "${ENV_FILE}" "SC_AUTH0_DOMAIN")"
    auth0_client_id="$(get_env_value "${ENV_FILE}" "SC_AUTH0_CLIENT_ID")"
    auth0_audience="$(get_env_value "${ENV_FILE}" "SC_AUTH0_AUDIENCE")"
    workspace_domain="$(get_env_value "${ENV_FILE}" "SC_GOOGLE_WORKSPACE_DOMAIN")"
    if [[ "${use_env_defaults}" != "true" ]]; then
      prompt_value auth0_domain "Auth0 domain" "${auth0_domain}"
      prompt_value auth0_client_id "Auth0 client ID" "${auth0_client_id}"
      prompt_value auth0_audience "Auth0 audience" "${auth0_audience}"
      prompt_value workspace_domain "Google Workspace domain" "${workspace_domain}"
    fi
    sandbox_auth_mode="false"
  fi
  if [[ -z "${ssm_prefix_default}" ]]; then
    if [[ "${environment_mode}" == "test" ]]; then
      ssm_prefix_default="/security-center/test"
    else
      ssm_prefix_default="/security-center/prod"
    fi
  fi
  ssm_prefix="${ssm_prefix_default}"
  [[ -n "${transport_mode}" ]] || transport_mode="ssm"
  [[ -n "${uploads_enabled}" ]] || uploads_enabled="false"
  if [[ "${use_env_defaults}" != "true" ]]; then
    prompt_value tls_acme_email "Let's Encrypt email" "${tls_acme_email}"
    prompt_value ssm_prefix "SSM path prefix" "${ssm_prefix_default}"
    prompt_value transport_mode "Remote transport (ssm or ssh)" "${transport_mode}"
    prompt_value ec2_instance_type "EC2 instance type" "${ec2_instance_type}"
    prompt_value ec2_volume_size "EC2 root volume size (GiB)" "${ec2_volume_size}"
    prompt_optional_value s3_artifact_bucket "S3 artifact bucket (for ssm deploys)" "${s3_artifact_bucket}"
    prompt_optional_value s3_artifact_prefix "S3 artifact prefix" "${s3_artifact_prefix}"
    prompt_optional_value db_backup_bucket "S3 database backup bucket" "${db_backup_bucket}"
    prompt_value db_backup_region "S3 database backup bucket region" "${db_backup_region:-${AWS_REGION_NAME}}"
    prompt_value db_backup_prefix "S3 database backup prefix" "${db_backup_prefix:-security-center-enterprise}"
    prompt_yes_no uploads_enabled "Enable vulnerability image uploads" "$( [[ "${uploads_enabled}" == "true" ]] && echo y || echo n )"
  fi
  if [[ "${uploads_enabled}" == "true" ]]; then
    if [[ "${use_env_defaults}" != "true" ]]; then
      prompt_optional_value uploads_bucket "S3 uploads bucket" "${uploads_bucket}"
      prompt_value uploads_region "S3 uploads bucket region" "${uploads_region}"
      prompt_value uploads_prefix "S3 uploads prefix" "${uploads_prefix}"
      prompt_value uploads_cors_origins "S3 uploads CORS allowed origins (comma-separated)" "${uploads_cors_origins}"
    fi
  else
    uploads_bucket=""
  fi
  if [[ "${use_env_defaults}" != "true" ]]; then
    prompt_yes_no enable_ssh_fallback "Create SSH fallback access" "$( [[ "${enable_ssh_fallback}" == "true" ]] && echo y || echo n )"
  fi

  deploy_workflow_default="$(trim "$(get_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW")")"
  if [[ "${deploy_workflow_default}" == "build-deploy" ]]; then
    remote_build_default="y"
  else
    remote_build_default="n"
  fi
  if [[ "${use_env_defaults}" != "true" ]]; then
    prompt_yes_no remote_build "Build backend/frontend images on EC2" "${remote_build_default}"
  else
    if [[ "${remote_build_default}" == "y" ]]; then
      remote_build="true"
    else
      remote_build="false"
    fi
  fi
  if [[ "${remote_build}" == "true" ]]; then
    if [[ "${use_env_defaults}" != "true" ]]; then
      prompt_value backend_image "Backend image tag (used for remote build tag)" "${backend_image}"
      prompt_value frontend_image "Frontend image tag (used for remote build tag)" "${frontend_image}"
    fi
  else
    if [[ "${use_env_defaults}" != "true" ]]; then
      prompt_value backend_image "Backend image tag (prebuilt image)" "${backend_image}"
      prompt_value frontend_image "Frontend image tag (prebuilt image)" "${frontend_image}"
    fi
  fi

  set_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME" "${public_hostname}"
  set_env_value "${ENV_FILE}" "SC_BACKEND_IMAGE" "${backend_image}"
  set_env_value "${ENV_FILE}" "SC_FRONTEND_IMAGE" "${frontend_image}"
  set_env_value "${ENV_FILE}" "SC_AWS_REGION" "${AWS_REGION_NAME}"
  set_env_value "${ENV_FILE}" "SC_ENVIRONMENT_MODE" "${environment_mode}"
  set_env_value "${ENV_FILE}" "SC_SSM_PATH_PREFIX" "${ssm_prefix}"
  set_env_value "${ENV_FILE}" "SC_AUTH0_DOMAIN" "${auth0_domain}"
  set_env_value "${ENV_FILE}" "SC_AUTH0_CLIENT_ID" "${auth0_client_id}"
  set_env_value "${ENV_FILE}" "SC_AUTH0_AUDIENCE" "${auth0_audience}"
  set_env_value "${ENV_FILE}" "SC_GOOGLE_WORKSPACE_DOMAIN" "${workspace_domain}"
  set_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE" "${sandbox_auth_mode}"
  set_env_value "${ENV_FILE}" "SC_TLS_ACME_EMAIL" "${tls_acme_email}"
  set_env_value "${ENV_FILE}" "SC_REMOTE_TRANSPORT" "${transport_mode}"
  set_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_TYPE" "${ec2_instance_type}"
  set_env_value "${ENV_FILE}" "SC_EC2_VOLUME_SIZE_GB" "${ec2_volume_size}"
  set_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_BUCKET" "${s3_artifact_bucket}"
  set_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_PREFIX" "${s3_artifact_prefix}"
  set_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_BUCKET" "${db_backup_bucket}"
  set_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_REGION" "${db_backup_region:-${AWS_REGION_NAME}}"
  set_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_PREFIX" "${db_backup_prefix:-security-center-enterprise}"
  set_env_value "${ENV_FILE}" "SC_UPLOADS_ENABLED" "${uploads_enabled}"
  set_env_value "${ENV_FILE}" "SC_UPLOADS_BUCKET" "${uploads_bucket}"
  set_env_value "${ENV_FILE}" "SC_UPLOADS_REGION" "${uploads_region:-${AWS_REGION_NAME}}"
  set_env_value "${ENV_FILE}" "SC_UPLOADS_PREFIX" "${uploads_prefix:-vuln-uploads/}"
  set_env_value "${ENV_FILE}" "SC_UPLOADS_CORS_ALLOWED_ORIGINS" "${uploads_cors_origins:-https://${public_hostname}}"
  set_env_value "${ENV_FILE}" "SC_ENABLE_SSH_FALLBACK" "${enable_ssh_fallback}"

  local prefix_trimmed="${ssm_prefix%/}"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_MYSQL_ROOT_PASSWORD" "${prefix_trimmed}/mysql/root_password"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_MYSQL_APP_PASSWORD" "${prefix_trimmed}/mysql/app_password"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_REDIS_PASSWORD" "${prefix_trimmed}/redis/password"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_JWT_ACCESS_SECRET" "${prefix_trimmed}/jwt/access_secret"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_JWT_REFRESH_SECRET" "${prefix_trimmed}/jwt/refresh_secret"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_AUTH0_CLIENT_SECRET" "${prefix_trimmed}/auth0/client_secret"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_FEISHU_ORG_APP_ID" "${prefix_trimmed}/feishu/org_sync/app_id"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_FEISHU_ORG_APP_SECRET" "${prefix_trimmed}/feishu/org_sync/app_secret"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_FEISHU_ALERT_APP_ID" "${prefix_trimmed}/feishu/alerts/app_id"
  set_env_value "${ENV_FILE}" "SC_SSM_PARAM_FEISHU_ALERT_APP_SECRET" "${prefix_trimmed}/feishu/alerts/app_secret"
  if [[ "${remote_build}" == "true" ]]; then
    set_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW" "build-deploy"
  else
    set_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW" "deploy"
  fi

  info "enterprise env updated: ${ENV_FILE}"
  info "environment branch selected: ${environment_mode}"
  info "note: run ./scripts/enterprise/enterprise.sh secrets if you want the script to write MySQL/Redis/JWT/Auth0/Feishu values into SSM for you"
  local existing_instance_id existing_public_ip recommended_workflow
  existing_instance_id="$(get_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_ID")"
  existing_public_ip="$(get_env_value "${ENV_FILE}" "SC_EC2_PUBLIC_IP")"
  recommended_workflow="$(get_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW")"
  if [[ -z "${existing_instance_id}" || -z "${existing_public_ip}" ]]; then
    info "recommended next command: ./scripts/enterprise/enterprise.sh provision"
    info "after provision: configure DNS -> ./scripts/enterprise/enterprise.sh cert -> ./scripts/enterprise/enterprise.sh ${recommended_workflow} -> ./scripts/enterprise/enterprise.sh verify"
  else
    info "existing EC2 target detected in env; recommended next command: ./scripts/enterprise/enterprise.sh preflight"
    info "then continue with: ./scripts/enterprise/enterprise.sh ${recommended_workflow} -> ./scripts/enterprise/enterprise.sh verify"
  fi
}

run_provision() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  elif [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" == "true" ]]; then
    warn "skipping aws sso login because --dry-run was requested"
  fi

  local -a cmd=("${PROVISION_HELPER}" --env-file "${ENV_FILE}" --region "${AWS_REGION_NAME}")
  [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  cmd+=(--transport "${TRANSPORT}")
  [[ "${DRY_RUN}" == "true" ]] && cmd+=(--dry-run)
  run_logged_stage "provision AWS resources" "${cmd[@]}"

  load_env_runtime_defaults
}

run_validate() {
  ensure_env_file
  choose_profile
  load_env_runtime_defaults
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  run_logged_stage "validate enterprise env" "${DEPLOY_HELPER}" --validate --env-file "${ENV_FILE}"
}

run_render() {
  ensure_env_file
  choose_profile
  load_env_runtime_defaults
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  print_secret_state_summary "render"
  if [[ "${RESOLVE_SSM}" == "true" ]]; then
    ensure_aws_session
    run_logged_stage "render deployment bundle" "${DEPLOY_HELPER}" --render --env-file "${ENV_FILE}" --output-dir "${OUTPUT_DIR}" --resolve-ssm
  else
    run_logged_stage "render deployment bundle" "${DEPLOY_HELPER}" --render --env-file "${ENV_FILE}" --output-dir "${OUTPUT_DIR}"
  fi
}

run_secrets() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  ensure_aws_session

  if [[ "$(trim "$(get_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE")")" == "true" ]]; then
    warn "sandbox auth mode is still enabled in ${ENV_FILE}; writing real secrets is allowed, but production auth still requires SC_SANDBOX_AUTH_MODE=false and SC_BYPASS_SSO=false at deploy time"
  fi

  local -a cmd=("${SECRETS_HELPER}" --env-file "${ENV_FILE}" --region "${AWS_REGION_NAME}")
  [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  [[ "${DRY_RUN}" == "true" ]] && cmd+=(--dry-run)
  [[ "${NON_INTERACTIVE}" == "true" ]] && cmd+=(--non-interactive)
  [[ "${GENERATE_MISSING_SECRETS}" == "true" ]] && cmd+=(--generate-missing)
  if ((${#SECRET_EXTRA_ARGS[@]} > 0)); then
    cmd+=("${SECRET_EXTRA_ARGS[@]}")
  fi
  run_logged_stage "write deployment secrets to SSM" "${cmd[@]}"
}

run_scanner_export() {
  ensure_env_file
  load_env_runtime_defaults
  local -a cmd=("${SCANNER_HANDOFF_HELPER}" export --env-file "${ENV_FILE}")
  [[ -n "${SCANNER_OUTPUT_FILE}" ]] && cmd+=(--output-file "${SCANNER_OUTPUT_FILE}")
  run_logged_stage "export scanner handoff" "${cmd[@]}"
}

run_scanner_config() {
  ensure_env_file
  [[ -n "${SCANNER_HANDOFF_FILE}" ]] || die "scanner-config requires --handoff-file <path>"
  local -a cmd=("${SCANNER_HANDOFF_HELPER}" import --env-file "${ENV_FILE}" --handoff-file "${SCANNER_HANDOFF_FILE}")
  [[ "${SCANNER_APPLY}" == "true" ]] && cmd+=(--apply)
  [[ "${SCANNER_ALLOW_TOKEN_PARAM_UPDATE}" == "true" ]] && cmd+=(--allow-token-param-update)
  run_logged_stage "import scanner handoff" "${cmd[@]}"
  if [[ "${SCANNER_APPLY}" == "true" ]]; then
    info "next: run enterprise.sh scanner-ingress to authorize scanner callback ingress on the asset platform security group"
  fi
}

first_env_file_value() {
  local file="$1"
  shift
  local key value
  for key in "$@"; do
    value="$(trim "$(get_env_value "${file}" "${key}")")"
    if [[ -n "${value}" ]]; then
      printf '%s' "${value}"
      return 0
    fi
  done
}

preflight_scanner_handoff_dependencies() {
  [[ -n "${SCANNER_HANDOFF_FILE}" ]] || die "scanner handoff file is required"
  [[ -f "${SCANNER_HANDOFF_FILE}" ]] || die "scanner handoff file not found: ${SCANNER_HANDOFF_FILE}"

  local use_public_ca ca_param
  use_public_ca="$(first_env_file_value "${SCANNER_HANDOFF_FILE}" SC_DAST_USE_PUBLIC_CA DAST_USE_PUBLIC_CA)"
  ca_param="$(first_env_file_value "${SCANNER_HANDOFF_FILE}" SC_DAST_CA_BUNDLE_PARAM DAST_SSM_PARAM_CA_BUNDLE)"
  if [[ -z "${ca_param}" ]]; then
    ca_param="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_CA_BUNDLE_PARAM")")"
  fi

  if is_truthy_value "${use_public_ca}"; then
    info "scanner handoff preflight: public CA requested; skipping scanner CA bundle SSM check"
    return 0
  fi

  [[ -n "${ca_param}" ]] || die "scanner handoff preflight requires SC_DAST_CA_BUNDLE_PARAM or SC_DAST_USE_PUBLIC_CA=true"

  require_cmd aws
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if ! aws ssm get-parameter --region "${AWS_REGION_NAME}" --name "${ca_param}" --with-decryption --query 'Parameter.Name' --output text >/dev/null 2>&1; then
    die "scanner handoff preflight failed: CA bundle SSM parameter is not readable in ${AWS_REGION_NAME}: ${ca_param}"
  fi
  info "scanner handoff preflight passed: CA bundle parameter is readable (${ca_param})"
}

normalize_scanner_source_cidr() {
  python3 - "$1" <<'PY'
import ipaddress
import sys

raw = sys.argv[1].strip()
if not raw:
    raise SystemExit("scanner source IP is empty")
try:
    network = ipaddress.ip_network(raw if "/" in raw else f"{raw}/32", strict=False)
except ValueError as exc:
    raise SystemExit(f"invalid scanner source IP/CIDR {raw!r}: {exc}")
if network.version != 4:
    raise SystemExit("scanner source ingress currently supports IPv4 only")
print(network.with_prefixlen)
PY
}

asset_sg_allows_scanner_cidr() {
  local sg_json="$1"
  local scanner_cidr="$2"
  python3 - "${sg_json}" "${scanner_cidr}" <<'PY'
import ipaddress
import json
import sys

doc = json.loads(sys.argv[1])
scanner = ipaddress.ip_network(sys.argv[2], strict=False)
groups = doc.get("SecurityGroups", [])
if not groups:
    raise SystemExit("asset platform security group not found")
for rule in groups[0].get("IpPermissions", []):
    if rule.get("IpProtocol") not in ("tcp", "-1"):
        continue
    from_port = rule.get("FromPort")
    to_port = rule.get("ToPort")
    covers_443 = rule.get("IpProtocol") == "-1" or (from_port is not None and to_port is not None and from_port <= 443 <= to_port)
    if not covers_443:
        continue
    for item in rule.get("IpRanges", []):
        cidr = item.get("CidrIp", "")
        if not cidr:
            continue
        try:
            network = ipaddress.ip_network(cidr, strict=False)
        except ValueError:
            continue
        if network.version == 4 and scanner.subnet_of(network):
            sys.exit(0)
raise SystemExit(f"asset platform security group does not allow scanner source {scanner} on 443")
PY
}

run_scanner_ingress() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  load_env_runtime_defaults

  local security_group_id scanner_source scanner_cidr
  security_group_id="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_SECURITY_GROUP_ID")")"
  scanner_source="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR")")"
  [[ -n "${scanner_source}" ]] || scanner_source="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_SCANNER_SOURCE_IP")")"
  [[ -n "${security_group_id}" ]] || die "scanner-ingress requires SC_EC2_SECURITY_GROUP_ID in ${ENV_FILE}"
  [[ -n "${scanner_source}" ]] || die "scanner-ingress requires SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR or SC_DAST_SCANNER_SOURCE_IP in ${ENV_FILE}"
  if ! scanner_cidr="$(normalize_scanner_source_cidr "${scanner_source}")"; then
    die "invalid scanner callback ingress source: ${scanner_source}"
  fi

  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would authorize ${scanner_cidr} to reach ${security_group_id} on 443/tcp for scanner callbacks"
    info "would keep existing inbound rules, including enterprise VPN HTTPS, unchanged"
    info "would keep outbound rules unchanged"
    return 0
  fi

  require_cmd aws
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" ]]; then
    ensure_aws_session
  fi

  local permissions_json aws_output
  permissions_json="$(python3 - "${scanner_cidr}" <<'PY'
import json
import sys

scanner_cidr = sys.argv[1]
print(json.dumps([{
    "IpProtocol": "tcp",
    "FromPort": 443,
    "ToPort": 443,
    "IpRanges": [{
        "CidrIp": scanner_cidr,
        "Description": "DAST scanner callback"
    }],
}]))
PY
)"
  if aws_output="$(aws ec2 authorize-security-group-ingress \
    --region "${AWS_REGION_NAME}" \
    --group-id "${security_group_id}" \
    --ip-permissions "${permissions_json}" 2>&1)"; then
    info "authorized scanner callback ingress: ${scanner_cidr} -> ${security_group_id}:443/tcp"
  elif printf '%s' "${aws_output}" | grep -q "InvalidPermission.Duplicate"; then
    info "scanner callback ingress already exists: ${scanner_cidr} -> ${security_group_id}:443/tcp"
  else
    printf '%s\n' "${aws_output}" >&2
    die "failed to authorize scanner callback ingress"
  fi
  info "existing inbound rules were not removed or rewritten; outbound rules remain unchanged"
}

run_promote_admin() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  load_env_runtime_defaults
  [[ -n "${PROMOTE_EMAIL}" ]] || die "promote-admin requires --email <user@example.com>"
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  fi
  choose_remote_target

  local promote_remote_dir="${REMOTE_DIR}"
  if [[ "${REMOTE_DIR_EXPLICIT}" != "true" ]]; then
    promote_remote_dir="${REMOTE_DIR}/rendered"
  fi

  local -a cmd=("${PROMOTE_ADMIN_HELPER}" --env-file "${ENV_FILE}" --email "${PROMOTE_EMAIL}" --transport "${TRANSPORT}" --remote-dir "${promote_remote_dir}")
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    cmd+=(--instance-id "${INSTANCE_ID}" --region "${AWS_REGION_NAME}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  else
    cmd+=(--host "${REMOTE_HOST}" --region "${AWS_REGION_NAME}")
    [[ -n "${IDENTITY_FILE}" ]] && cmd+=(--identity-file "${IDENTITY_FILE}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  fi
  [[ "${DRY_RUN}" == "true" ]] && cmd+=(--dry-run)
  run_logged_stage "promote user to admin" "${cmd[@]}"
}

run_db_backup() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  load_env_runtime_defaults
  if [[ "${TRANSPORT}" == "ssm" && "${NON_INTERACTIVE}" == "true" && -z "${INSTANCE_ID}" ]]; then
    die "non-interactive db-backup requires --instance-id for ssm transport"
  fi
  if [[ "${TRANSPORT}" == "ssh" && "${NON_INTERACTIVE}" == "true" && -z "${REMOTE_HOST}" ]]; then
    die "non-interactive db-backup requires --host for ssh transport"
  fi
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  elif [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" == "true" ]]; then
    warn "skipping aws sso login because --dry-run was requested"
  fi
  choose_remote_target

  local -a cmd=(bash "${DB_BACKUP_HELPER}" --env-file "${ENV_FILE}" --transport "${TRANSPORT}" --remote-dir "${REMOTE_DIR}" --region "${AWS_REGION_NAME}")
  [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    cmd+=(--instance-id "${INSTANCE_ID}")
  else
    cmd+=(--host "${REMOTE_HOST}")
    [[ -n "${IDENTITY_FILE}" ]] && cmd+=(--identity-file "${IDENTITY_FILE}")
  fi
  [[ -n "${DB_BACKUP_S3_BUCKET}" ]] && cmd+=(--backup-s3-bucket "${DB_BACKUP_S3_BUCKET}")
  [[ -n "${DB_BACKUP_S3_PREFIX}" ]] && cmd+=(--backup-s3-prefix "${DB_BACKUP_S3_PREFIX}")
  [[ -n "${DB_BACKUP_S3_REGION}" ]] && cmd+=(--backup-s3-region "${DB_BACKUP_S3_REGION}")
  [[ "${NON_INTERACTIVE}" == "true" ]] && cmd+=(--non-interactive)
  [[ "${DRY_RUN}" == "true" ]] && cmd+=(--dry-run)
  run_logged_stage "run enterprise database backup" "${cmd[@]}"
}

run_deploy_common() {
  ensure_env_file
  choose_profile
  load_env_runtime_defaults
  ensure_region_selected
  if [[ "${TRANSPORT}" == "ssm" && "${NON_INTERACTIVE}" == "true" && -z "${INSTANCE_ID}" ]]; then
    die "non-interactive deploy/build-deploy requires --instance-id for ssm transport"
  fi
  if [[ "${TRANSPORT}" == "ssm" && -z "${S3_BUCKET}" ]]; then
    die "ssm deploy/build-deploy requires SC_S3_ARTIFACT_BUCKET or --s3-bucket"
  fi
  if [[ "${TRANSPORT}" == "ssh" && "${NON_INTERACTIVE}" == "true" && -z "${REMOTE_HOST}" ]]; then
    die "non-interactive deploy/build-deploy requires --host for ssh transport"
  fi
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  local mode="deploy"
  [[ "${BUILD_ON_REMOTE}" == "true" ]] && mode="build-deploy"
  print_secret_state_summary "${mode}"
  if [[ "${mode}" == "deploy" ]]; then
    run_logged_stage "check deploy image references" warn_on_mutable_deploy_images
    run_logged_stage "enforce production image policy" enforce_production_deploy_image_policy
  fi
  run_logged_stage "schema-sensitive release gate" enforce_schema_migration_gate
  export SC_SCHEMA_GATE_CHECKED="true"
  if [[ "${RESOLVE_SSM}" != "true" ]]; then
    die "deploy/build-deploy require --resolve-ssm because runtime secrets must be resolved before remote startup"
  fi
  if [[ "${DRY_RUN}" == "true" ]]; then
    if ! command -v aws >/dev/null 2>&1 || ! aws sts get-caller-identity >/dev/null 2>&1; then
      warn "AWS session is not active; dry-run will continue without resolved runtime secrets"
      RESOLVE_SSM="false"
    fi
  fi
  if [[ "${RESOLVE_SSM}" == "true" || ( -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ) ]]; then
    ensure_aws_session
  fi
  choose_remote_target
  run_logged_stage "check ssm target readiness" ensure_ssm_target_ready
  print_deployment_plan_summary "${mode}" "run remote preflight, then apply migrations and deploy the rendered bundle"
  run_logged_stage "preflight remote target" run_preflight
  if [[ "${SKIP_DB_BACKUP}" == "true" ]]; then
    warn "skipping automatic pre-deploy database backup (--skip-db-backup)"
  else
    run_logged_stage "pre-deploy database backup" run_db_backup
  fi
  local -a cmd=("${REMOTE_DEPLOY_HELPER}" --env-file "${ENV_FILE}" --transport "${TRANSPORT}" --remote-dir "${REMOTE_DIR}")
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    cmd+=(--instance-id "${INSTANCE_ID}" --region "${AWS_REGION_NAME}" --s3-bucket "${S3_BUCKET}" --s3-prefix "${S3_PREFIX}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  else
    cmd+=(--host "${REMOTE_HOST}")
    [[ -n "${IDENTITY_FILE}" ]] && cmd+=(--identity-file "${IDENTITY_FILE}")
  fi
  [[ "${RESOLVE_SSM}" == "true" ]] && cmd+=(--resolve-ssm)
  [[ "${DRY_RUN}" == "true" ]] && cmd+=(--dry-run)
  [[ "${SKIP_UPLOAD}" == "true" ]] && cmd+=(--skip-upload)
  [[ "${SKIP_UP}" == "true" ]] && cmd+=(--skip-up)
  [[ "${SKIP_DB_MIGRATIONS}" == "true" ]] && cmd+=(--skip-db-migrations)
  [[ "${ALLOW_SCHEMA_DRIFT}" == "true" ]] && cmd+=(--allow-schema-drift)
  if [[ "${BUILD_ON_REMOTE}" == "true" ]]; then
    cmd+=(--build-on-remote --source-dir "${SOURCE_DIR}")
    [[ "${SKIP_BUILD}" == "true" ]] && cmd+=(--skip-build)
  fi
  run_logged_stage "remote ${mode}" "${cmd[@]}"
}

run_cert() {
  ensure_env_file
  choose_profile
  load_env_runtime_defaults
  if [[ "${TRANSPORT}" == "ssm" && "${NON_INTERACTIVE}" == "true" && -z "${INSTANCE_ID}" ]]; then
    die "non-interactive cert requires --instance-id for ssm transport"
  fi
  if [[ "${TRANSPORT}" == "ssh" && "${NON_INTERACTIVE}" == "true" && -z "${REMOTE_HOST}" ]]; then
    die "non-interactive cert requires --host for ssh transport"
  fi
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" && "${TRANSPORT}" == "ssm" && -z "${INSTANCE_ID}" ]]; then
    ensure_aws_session
  fi
  choose_remote_target

  local -a cmd=("${CERT_HELPER}" --env-file "${ENV_FILE}" --transport "${TRANSPORT}" --remote-dir "${REMOTE_DIR}")
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    cmd+=(--instance-id "${INSTANCE_ID}" --region "${AWS_REGION_NAME}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  else
    cmd+=(--host "${REMOTE_HOST}")
    [[ -n "${IDENTITY_FILE}" ]] && cmd+=(--identity-file "${IDENTITY_FILE}")
  fi
  [[ "${DRY_RUN}" == "true" ]] && cmd+=(--dry-run)
  run_logged_stage "issue or renew Let's Encrypt certificate" "${cmd[@]}"
}

run_verify() {
  ensure_env_file
  choose_profile
  load_env_runtime_defaults
  if [[ "${TRANSPORT}" == "ssm" && "${NON_INTERACTIVE}" == "true" && -z "${INSTANCE_ID}" ]]; then
    die "non-interactive verify requires --instance-id for ssm transport"
  fi
  if [[ "${TRANSPORT}" == "ssh" && "${NON_INTERACTIVE}" == "true" && -z "${REMOTE_HOST}" ]]; then
    die "non-interactive verify requires --host for ssh transport"
  fi
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" && "${TRANSPORT}" == "ssm" && -z "${INSTANCE_ID}" ]]; then
    ensure_aws_session
  fi
  choose_remote_target

  local hostname
  hostname="$(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")"
  [[ -n "${hostname}" ]] || die "SC_PUBLIC_HOSTNAME is required for verify"

  info "verification summary:"
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    info "  - target instance: ${INSTANCE_ID}"
  else
    info "  - target host: ${REMOTE_HOST}"
  fi
  info "  - public hostname: ${hostname}"

  if [[ "${DRY_RUN}" == "true" ]]; then
    echo "DRY-RUN local verify: curl -k -fsS https://${hostname}/auth/login"
    echo "DRY-RUN remote verify: docker compose ps under ${REMOTE_DIR}/rendered"
    echo "DRY-RUN remote verify: curl -fsS http://localhost/health via nginx/backend paths"
    if [[ "$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_ENABLED")")" == "true" ]]; then
      echo "DRY-RUN scanner verify: resolve scanner DNS from asset EC2/backend container"
      echo "DRY-RUN scanner verify: HTTPS /healthz and bearer-token probe from backend container"
      echo "DRY-RUN scanner verify: callback URL reachability and scanner security-group boundary"
    fi
    return 0
  fi

  local public_origin="https://${hostname}"
  if ! curl -k -fsS --connect-timeout 10 --max-time 20 "${public_origin}/auth/login" >/dev/null; then
    local environment_mode public_ip
    environment_mode="$(trim "$(get_env_value "${ENV_FILE}" "SC_ENVIRONMENT_MODE")")"
    public_ip="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_PUBLIC_IP")")"
    if [[ "${environment_mode}" == "test" && -n "${public_ip}" ]]; then
      warn "public hostname verify failed for ${public_origin}; falling back to EC2 public IP because SC_ENVIRONMENT_MODE=test"
      public_origin="https://${public_ip}"
      run_logged_stage "verify public login endpoint via EC2 public IP" curl -k -fsS --connect-timeout 10 --max-time 20 "${public_origin}/auth/login" >/dev/null
    else
      die "failed to verify public login endpoint: ${public_origin}/auth/login"
    fi
  else
    info "verified public login endpoint: ${public_origin}/auth/login"
  fi
  local -a diagnose_cmd=("${DIAGNOSE_HELPER}" --transport "${TRANSPORT}" --remote-dir "${REMOTE_DIR}/rendered")
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    diagnose_cmd+=(--instance-id "${INSTANCE_ID}" --region "${AWS_REGION_NAME}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && diagnose_cmd+=(--profile "${AWS_PROFILE_NAME}")
  else
    diagnose_cmd+=(--host "${REMOTE_HOST}")
    [[ -n "${IDENTITY_FILE}" ]] && diagnose_cmd+=(--identity-file "${IDENTITY_FILE}")
  fi
  run_logged_stage "verify remote runtime via diagnostics" "${diagnose_cmd[@]}"
  run_logged_stage "verify scanner integration" run_scanner_verify "${public_origin}"
}

run_scanner_verify() {
  local public_origin="$1"
  local scanner_enabled
  scanner_enabled="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_ENABLED")")"
  [[ "${scanner_enabled}" == "true" ]] || {
    info "scanner integration disabled; skipping scanner verify"
    return 0
  }

  SC_TRANSPORT_MODE="${TRANSPORT}"
  SC_TRANSPORT_DRY_RUN="${DRY_RUN}"
  SC_TRANSPORT_REMOTE_HOST="${REMOTE_HOST}"
  SC_TRANSPORT_INSTANCE_ID="${INSTANCE_ID}"
  SC_TRANSPORT_IDENTITY_FILE="${IDENTITY_FILE}"
  SC_TRANSPORT_AWS_REGION="${AWS_REGION_NAME}"
  SC_TRANSPORT_AWS_PROFILE="${AWS_PROFILE_NAME}"
  sc_transport_init

  local remote_verify_script
  remote_verify_script="set -euo pipefail
cd '${REMOTE_DIR}/rendered'
set -a
. ./.env.runtime
set +a
if [[ \"\${SECURITY_CENTER_SCAN_SCANNER_ENABLED:-false}\" != \"true\" ]]; then
  echo 'PASS: scanner runtime disabled in rendered env'
  exit 0
fi
scanner_base=\"\${SECURITY_CENTER_SCAN_SCANNER_BASE_URL:?scanner base url missing}\"
scanner_token=\"\${SECURITY_CENTER_SCAN_SCANNER_API_TOKEN:?scanner api token missing}\"
scanner_host=\"\$(python3 - \"\$scanner_base\" <<'PY'
from urllib.parse import urlparse
import sys
print(urlparse(sys.argv[1]).hostname or '')
PY
)\"
test -n \"\$scanner_host\"
getent hosts \"\$scanner_host\" >/dev/null
echo \"PASS: scanner DNS resolves: \$scanner_host\"
health_url=\"\${scanner_base%/}/healthz\"
ca_env=''
if [[ -n \"\${SC_DAST_SSL_CERT_FILE:-}\" ]]; then
  if [[ -n \"\${SC_DAST_CA_BUNDLE_HOST_PATH:-}\" && \"\${SC_DAST_CA_BUNDLE_HOST_PATH}\" != __RESOLVE_FROM_SSM__ ]]; then
    test -s \"\${SC_DAST_CA_BUNDLE_HOST_PATH}\"
  fi
  ca_env=\"SSL_CERT_FILE=\${SC_DAST_SSL_CERT_FILE}\"
fi
docker compose --env-file .env.runtime -f docker-compose.enterprise.yml exec -T backend sh -lc \"test -z '\${SC_DAST_SSL_CERT_FILE:-}' || test -s '\${SC_DAST_SSL_CERT_FILE}'\"
docker compose --env-file .env.runtime -f docker-compose.enterprise.yml exec -T backend sh -lc \"\$ca_env wget -T 10 -q -O - '\$health_url' >/dev/null\"
echo \"PASS: backend container can reach scanner health over HTTPS\"
probe_url=\"\${scanner_base%/}/api/v1/reachability/batches/__security_center_verify__\"
probe_output=\"\$(docker compose --env-file .env.runtime -f docker-compose.enterprise.yml exec -T backend sh -lc \"\$ca_env wget -T 10 -S -q -O /tmp/scanner-probe.out --header='Authorization: Bearer \$scanner_token' --header='X-Request-Timestamp: \$(date -u +%Y-%m-%dT%H:%M:%SZ)' '\$probe_url' 2>&1 || true\")\"
if printf '%s' \"\$probe_output\" | grep -Eq 'HTTP/[0-9.]+ 401|HTTP/[0-9.]+ 403'; then
  echo \"FAIL: scanner token probe returned unauthorized/forbidden\" >&2
  exit 1
fi
echo \"PASS: scanner bearer token probe did not return 401/403\"
"
  sc_transport_run "${remote_verify_script}"

  local callback_status
  callback_status="$(curl -k -sS --connect-timeout 10 --max-time 20 -o /dev/null -w '%{http_code}' -X POST "${public_origin}/api/v1/scans/callback" || true)"
  case "${callback_status}" in
    400|401|415|422) info "callback endpoint reachable with expected unauthenticated/client-error status ${callback_status}" ;;
    2*) info "callback endpoint reachable with status ${callback_status}" ;;
    *) die "callback endpoint returned unexpected status ${callback_status:-000} for ${public_origin}/api/v1/scans/callback" ;;
esac

  local scanner_sg backend_sg
  scanner_sg="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_SCANNER_SECURITY_GROUP_ID")")"
  backend_sg="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_SECURITY_GROUP_ID")")"
  if [[ -n "${scanner_sg}" ]]; then
    ensure_aws_session
    local sg_json
    sg_json="$(aws ec2 describe-security-groups --region "${AWS_REGION_NAME}" --group-ids "${scanner_sg}" --output json)"
    python3 - "${sg_json}" "${backend_sg}" <<'PY'
import json
import sys

doc = json.loads(sys.argv[1])
backend_sg = sys.argv[2].strip()
groups = doc.get("SecurityGroups", [])
if not groups:
    raise SystemExit("scanner security group not found")
rules = groups[0].get("IpPermissions", [])
for rule in rules:
    if rule.get("IpProtocol") not in ("tcp", "-1"):
        continue
    from_port = rule.get("FromPort")
    to_port = rule.get("ToPort")
    covers_443 = rule.get("IpProtocol") == "-1" or (from_port is not None and to_port is not None and from_port <= 443 <= to_port)
    if not covers_443:
        continue
    for item in rule.get("IpRanges", []):
        if item.get("CidrIp") == "0.0.0.0/0":
            raise SystemExit("scanner security group exposes 443 to 0.0.0.0/0")
if backend_sg:
    allowed = False
    for rule in rules:
        if rule.get("IpProtocol") not in ("tcp", "-1"):
            continue
        from_port = rule.get("FromPort")
        to_port = rule.get("ToPort")
        covers_443 = rule.get("IpProtocol") == "-1" or (from_port is not None and to_port is not None and from_port <= 443 <= to_port)
        if not covers_443:
            continue
        for pair in rule.get("UserIdGroupPairs", []):
            if pair.get("GroupId") == backend_sg:
                allowed = True
    if not allowed:
        raise SystemExit(f"scanner security group does not allow backend security group {backend_sg} on 443")
PY
    info "scanner security group boundary verified: ${scanner_sg}"
  else
    warn "SC_DAST_SCANNER_SECURITY_GROUP_ID is not set; skipped scanner security group verification"
  fi

  local scanner_source_ip scanner_callback_source
  scanner_source_ip="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_SCANNER_SOURCE_IP")")"
  scanner_callback_source="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR")")"
  [[ -n "${scanner_callback_source}" ]] || scanner_callback_source="${scanner_source_ip}"
  if [[ -n "${scanner_callback_source}" ]]; then
    info "scanner callback source configured for E2E evidence comparison: ${scanner_callback_source}"
    if [[ -n "${backend_sg}" ]]; then
      local scanner_source_cidr asset_sg_json
      if ! scanner_source_cidr="$(normalize_scanner_source_cidr "${scanner_callback_source}")"; then
        die "invalid scanner callback ingress source: ${scanner_callback_source}"
      fi
      require_cmd aws
      ensure_aws_session
      asset_sg_json="$(aws ec2 describe-security-groups --region "${AWS_REGION_NAME}" --group-ids "${backend_sg}" --output json)"
      asset_sg_allows_scanner_cidr "${asset_sg_json}" "${scanner_source_cidr}"
      info "asset platform security group allows scanner callback ingress: ${scanner_source_cidr} -> ${backend_sg}:443/tcp"
    else
      warn "SC_EC2_SECURITY_GROUP_ID is not set; skipped asset platform callback ingress verification"
    fi
  fi
}

flip_cert_authenticator_to_webroot() {
  # Only meaningful after a fresh bootstrap: `run_cert` issues the first cert
  # via standalone (because nginx is not up yet at bootstrap step 4), which
  # leaves renewal/<host>.conf pinned to authenticator = standalone. Once nginx
  # is up and serving /.well-known/acme-challenge from the webroot, re-issue
  # once via webroot so future `certbot renew` runs no longer need to stop
  # nginx. --force-renewal is required because the cert is still fresh and
  # --keep-until-expiring would otherwise skip the re-issue. On hosts where the
  # conf is already webroot (e.g. the existing expired host repaired via
  # `enterprise.sh cert`), this is a no-op.
  local hostname webroot_dir renewal_conf auth remote_script acme_email
  hostname="$(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")"
  webroot_dir="$(get_env_value "${ENV_FILE}" "SC_ACME_WEBROOT")"
  webroot_dir="${webroot_dir:-/var/www/certbot}"
  acme_email="$(get_env_value "${ENV_FILE}" "SC_TLS_ACME_EMAIL")"
  renewal_conf="/etc/letsencrypt/renewal/${hostname}.conf"
  [[ -n "${hostname}" ]] || { warn "SC_PUBLIC_HOSTNAME missing; skipping webroot authenticator flip"; return 0; }

  SC_TRANSPORT_MODE="${TRANSPORT}"
  SC_TRANSPORT_DRY_RUN="${DRY_RUN}"
  SC_TRANSPORT_REMOTE_HOST="${REMOTE_HOST}"
  SC_TRANSPORT_INSTANCE_ID="${INSTANCE_ID}"
  SC_TRANSPORT_IDENTITY_FILE="${IDENTITY_FILE}"
  SC_TRANSPORT_AWS_REGION="${AWS_REGION_NAME}"
  SC_TRANSPORT_AWS_PROFILE="${AWS_PROFILE_NAME}"
  sc_transport_init

  if [[ "${DRY_RUN}" == "true" ]]; then
    echo "DRY-RUN remote ($(sc_transport_target_label)): grep authenticator ${renewal_conf}; if not webroot, sudo certbot certonly --webroot -w '${webroot_dir}' --non-interactive --force-renewal -d '${hostname}'"
    return 0
  fi

  remote_script="$(cat <<EOF
set -euo pipefail
renewal_conf='${renewal_conf}'
webroot_dir='${webroot_dir}'
hostname='${hostname}'
acme_email='${acme_email}'
if [[ ! -f "\${renewal_conf}" ]]; then
  echo "no-renewal-conf"
  exit 0
fi
auth="\$(awk -F= '/^[[:space:]]*authenticator[[:space:]]*=/{gsub(/[[:space:]]/,"",\$2); print \$2}' "\${renewal_conf}" | tail -n1)"
if [[ "\${auth}" == "webroot" ]]; then
  echo "already-webroot"
  exit 0
fi
echo "flipping:\${auth}"
if [[ -n "\${acme_email}" ]]; then
  sudo certbot certonly --webroot -w "\${webroot_dir}" --non-interactive --agree-tos --force-renewal -m "\${acme_email}" -d "\${hostname}"
else
  sudo certbot certonly --webroot -w "\${webroot_dir}" --non-interactive --agree-tos --force-renewal -d "\${hostname}"
fi
auth="\$(awk -F= '/^[[:space:]]*authenticator[[:space:]]*=/{gsub(/[[:space:]]/,"",\$2); print \$2}' "\${renewal_conf}" | tail -n1)"
echo "now:\${auth:-<empty>}"
EOF
)"
  local flip_output
  if ! flip_output="$(sc_transport_run "bash -lc $(printf '%q' "${remote_script}")" 2>&1)"; then
    warn "webroot authenticator flip command failed; cert renewal may still require nginx to be stopped"
    printf '%s\n' "${flip_output}" >&2
    return 0
  fi

  case "${flip_output}" in
    *already-webroot*)
      info "certbot renewal conf already uses webroot authenticator; nothing to flip"
      ;;
    *no-renewal-conf*)
      warn "no certbot renewal conf found at ${renewal_conf}; skipping flip"
      ;;
    *now:webroot*)
      info "certbot renewal conf now uses webroot authenticator"
      ;;
    *)
      warn "certbot renewal conf authenticator flip outcome unclear; output:"
      printf '%s\n' "${flip_output}" >&2
      ;;
  esac
}

run_bootstrap() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  elif [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" == "true" ]]; then
    warn "skipping aws sso login because --dry-run was requested"
  fi

  info "bootstrap step 1/5: initialize or refresh enterprise env"
  interactive_init

  local configure_secrets_now="false"
  local sandbox_auth_mode
  sandbox_auth_mode="$(trim "$(get_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE")")"
  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    [[ "${sandbox_auth_mode}" == "true" ]] || configure_secrets_now="true"
  else
    if [[ "${sandbox_auth_mode}" == "true" ]]; then
      prompt_yes_no configure_secrets_now "Configure SSM secrets now (MySQL/Redis/JWT and optional real Auth0/Feishu values)" "n"
    else
      prompt_yes_no configure_secrets_now "Configure SSM secrets now (MySQL/Redis/JWT and real Auth0/Feishu values)" "y"
    fi
  fi
  if [[ "${configure_secrets_now}" == "true" ]]; then
    info "bootstrap step 1.5/5: write or generate SSM secrets"
    run_secrets
  else
    info "bootstrap step 1.5/5: skipping interactive SSM secret write"
    info "  - provision will still auto-generate missing MySQL/Redis/JWT secrets"
    info "  - real Auth0/Feishu values can be written later with ./scripts/enterprise/enterprise.sh secrets"
  fi

  info "bootstrap step 2/5: provision or reuse AWS resources"
  run_provision

  local target_ip
  target_ip="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_PUBLIC_IP")")"
  info "bootstrap step 3/5: configure DNS"
  info "  - create or update an A record:"
  info "    ${target_ip:-<public-ip-pending>}"
  info "  - hostname: $(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")"
  [[ -n "${target_ip}" ]] || die "bootstrap requires SC_EC2_PUBLIC_IP after provision; rerun provision if the instance did not receive a public IP"
  if [[ "${DNS_READY}" != "true" ]]; then
    if [[ "${NON_INTERACTIVE}" == "true" ]]; then
      die "bootstrap in non-interactive mode requires --dns-ready after the DNS record is live"
    fi
    info "waiting for the public DNS record to propagate before certificate issuance"
    wait_for_dns_propagation "$(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")" "${target_ip}"
  fi

  info "bootstrap step 4/5: issue or renew Let's Encrypt certificate"
  run_cert

  info "bootstrap step 5/5: deploy application and verify"
  local workflow
  workflow="$(get_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW")"
  if [[ "${workflow}" == "build-deploy" ]]; then
    BUILD_ON_REMOTE="true"
  else
    BUILD_ON_REMOTE="false"
  fi
  RESOLVE_SSM="true"
  run_deploy_common
  run_verify

  info "bootstrap step 6/6: flip certbot authenticator to webroot (no-op if already webroot)"
  flip_cert_authenticator_to_webroot
}

run_rollout() {
  ensure_env_file
  choose_profile
  ensure_region_selected
  load_env_runtime_defaults
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  elif [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" == "true" ]]; then
    warn "skipping aws sso login because --dry-run was requested"
  fi

  info "rollout step 1/6: validate enterprise env"
  run_validate

  if env_truthy SC_DEPLOY_AUTO_WRITE_SECRETS false; then
    info "rollout step 2/6: write missing deployment secrets"
    local previous_generate_missing="${GENERATE_MISSING_SECRETS}"
    if env_truthy SC_DEPLOY_AUTO_GENERATE_MISSING_SECRETS true; then
      GENERATE_MISSING_SECRETS="true"
    fi
    run_secrets
    GENERATE_MISSING_SECRETS="${previous_generate_missing}"
  else
    info "rollout step 2/6: skipping secrets write (SC_DEPLOY_AUTO_WRITE_SECRETS=false)"
  fi

  info "rollout step 3/6: provision or reuse AWS resources"
  run_provision

  local asset_handoff_file
  asset_handoff_file="$(trim "$(get_env_value "${ENV_FILE}" "SC_DAST_ASSET_HANDOFF_FILE")")"
  if [[ -n "${asset_handoff_file}" ]]; then
    info "rollout step 4/6: export asset-platform handoff for scanner"
    local previous_scanner_output="${SCANNER_OUTPUT_FILE}"
    SCANNER_OUTPUT_FILE="$(resolve_path "${asset_handoff_file}")"
    run_scanner_export
    SCANNER_OUTPUT_FILE="${previous_scanner_output}"
  else
    info "rollout step 4/6: skipping scanner handoff export (SC_DAST_ASSET_HANDOFF_FILE is blank)"
  fi

  info "rollout step 5/6: deploy application"
  local workflow
  workflow="$(trim "$(get_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW")")"
  if [[ "${workflow}" == "build-deploy" ]]; then
    BUILD_ON_REMOTE="true"
  else
    BUILD_ON_REMOTE="false"
  fi
  RESOLVE_SSM="true"
  run_deploy_common

  if env_truthy SC_DEPLOY_AUTO_VERIFY true; then
    info "rollout step 6/6: verify deployment"
    run_verify
  else
    info "rollout step 6/6: skipping verify (SC_DEPLOY_AUTO_VERIFY=false)"
  fi

  info "rollout post-step: flip certbot authenticator to webroot (no-op if already webroot)"
  flip_cert_authenticator_to_webroot

  info "rollout completed"
  info "  - cleanup preview: ./scripts/enterprise/enterprise.sh cleanup --env-file ${ENV_FILE} --region ${AWS_REGION_NAME}"
  if [[ -n "${asset_handoff_file}" ]]; then
    info "  - scanner handoff file: ${asset_handoff_file}"
  fi
}

run_scanner_rollout() {
  ensure_env_file
  [[ -n "${SCANNER_HANDOFF_FILE}" ]] || die "scanner-rollout requires --handoff-file <path>"
  choose_profile
  ensure_region_selected
  load_env_runtime_defaults
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  elif [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" == "true" ]]; then
    warn "skipping aws sso login because --dry-run was requested"
  fi

  info "scanner rollout step 1/4: import scanner handoff"
  if [[ "${DRY_RUN}" == "true" ]]; then
    SCANNER_APPLY="false"
    run_scanner_config
    info "dry-run only. Would next authorize scanner ingress, redeploy with --resolve-ssm, and verify scanner connectivity."
    return 0
  fi

  run_logged_stage "preflight scanner handoff dependencies" preflight_scanner_handoff_dependencies

  SCANNER_APPLY="true"
  run_scanner_config
  load_env_runtime_defaults

  info "scanner rollout step 2/4: authorize scanner callback ingress"
  run_scanner_ingress

  info "scanner rollout step 3/4: redeploy application with scanner runtime config"
  local workflow
  workflow="$(trim "$(get_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW")")"
  if [[ "${workflow}" == "build-deploy" ]]; then
    BUILD_ON_REMOTE="true"
  else
    BUILD_ON_REMOTE="false"
  fi
  RESOLVE_SSM="true"
  run_deploy_common

  info "scanner rollout step 4/4: verify deployment and scanner connectivity"
  run_verify
}

run_preflight() {
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    [[ -n "${INSTANCE_ID}" ]] || die "remote instance must be resolved before preflight"
    [[ -n "${S3_BUCKET}" ]] || die "ssm preflight requires SC_S3_ARTIFACT_BUCKET or --s3-bucket"
  else
    [[ -n "${REMOTE_HOST}" ]] || die "remote host must be resolved before preflight"
  fi

  local temp_dir
  temp_dir="$(mktemp -d /tmp/security-center-preflight.XXXXXX)"

  if [[ "${RESOLVE_SSM}" == "true" ]]; then
    "${DEPLOY_HELPER}" --render --env-file "${ENV_FILE}" --output-dir "${temp_dir}" --resolve-ssm >/dev/null
  else
    "${DEPLOY_HELPER}" --render --env-file "${ENV_FILE}" --output-dir "${temp_dir}" >/dev/null
  fi

  local cert_fullchain cert_privkey
  cert_fullchain="$(grep '^SC_TLS_CERT_FULLCHAIN=' "${temp_dir}/.env.runtime" | cut -d= -f2-)"
  cert_privkey="$(grep '^SC_TLS_CERT_PRIVKEY=' "${temp_dir}/.env.runtime" | cut -d= -f2-)"
  local sandbox_auth_mode environment_mode skip_tls_checks
  sandbox_auth_mode="$(trim "$(get_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE")")"
  environment_mode="$(trim "$(get_env_value "${ENV_FILE}" "SC_ENVIRONMENT_MODE")")"
  skip_tls_checks="false"
  if [[ "${sandbox_auth_mode}" == "true" || "${environment_mode}" == "test" ]]; then
    skip_tls_checks="true"
  fi
  local uploads_enabled uploads_bucket uploads_region uploads_prefix uploads_cors_origins
  uploads_enabled="$(grep '^SC_UPLOADS_ENABLED=' "${temp_dir}/.env.runtime" | cut -d= -f2-)"
  uploads_bucket="$(grep '^SC_UPLOADS_BUCKET=' "${temp_dir}/.env.runtime" | cut -d= -f2-)"
  uploads_region="$(grep '^SC_UPLOADS_REGION=' "${temp_dir}/.env.runtime" | cut -d= -f2-)"
  uploads_prefix="$(grep '^SC_UPLOADS_PREFIX=' "${temp_dir}/.env.runtime" | cut -d= -f2-)"
  uploads_cors_origins="$(grep '^SC_UPLOADS_CORS_ALLOWED_ORIGINS=' "${temp_dir}/.env.runtime" | cut -d= -f2-)"

  info "preflight summary:"
  info "  - transport: ${TRANSPORT}"
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    info "  - instance id: ${INSTANCE_ID}"
  else
    info "  - remote host: ${REMOTE_HOST}"
  fi
  info "  - remote dir: ${REMOTE_DIR}"
  info "  - cert fullchain path: ${cert_fullchain}"
  info "  - cert privkey path: ${cert_privkey}"
  info "  - uploads enabled: ${uploads_enabled:-false}"
  if [[ "${uploads_enabled}" == "true" ]]; then
    info "  - uploads bucket: ${uploads_bucket}"
    info "  - uploads region: ${uploads_region}"
    info "  - uploads prefix: ${uploads_prefix}"
    info "  - uploads cors origins: ${uploads_cors_origins}"
  fi

  SC_TRANSPORT_MODE="${TRANSPORT}"
  SC_TRANSPORT_DRY_RUN="${DRY_RUN}"
  SC_TRANSPORT_REMOTE_HOST="${REMOTE_HOST}"
  SC_TRANSPORT_INSTANCE_ID="${INSTANCE_ID}"
  SC_TRANSPORT_IDENTITY_FILE="${IDENTITY_FILE}"
  SC_TRANSPORT_AWS_REGION="${AWS_REGION_NAME}"
  SC_TRANSPORT_AWS_PROFILE="${AWS_PROFILE_NAME}"

  if [[ "${DRY_RUN}" == "true" ]]; then
    echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): check docker installed"
    echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): check docker compose available"
    echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): ensure ${REMOTE_DIR} and release directories are writable"
    echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): ensure at least 2 GiB disk headroom"
    if [[ "${skip_tls_checks}" == "true" ]]; then
      echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): skip TLS cert checks for sandbox/test deploys"
    else
      echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): verify ${cert_fullchain} exists"
      echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): verify ${cert_privkey} exists"
    fi
    if [[ "${TRANSPORT}" == "ssm" ]]; then
      echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): check aws CLI available on instance"
      echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): check S3 staging bucket access for ${S3_BUCKET}/${S3_PREFIX}"
    fi
    if [[ "${uploads_enabled}" == "true" ]]; then
      echo "DRY-RUN preflight (local): check upload bucket exists: ${uploads_bucket}"
      echo "DRY-RUN preflight (local): check upload bucket CORS allows PUT/GET/HEAD for ${uploads_cors_origins}"
      echo "DRY-RUN preflight (local): check upload bucket remains distinct from artifact bucket ${S3_BUCKET}"
      echo "DRY-RUN remote preflight ($(sc_transport_target_label) via ${TRANSPORT}): check instance role can list ${uploads_bucket}/${uploads_prefix}*"
    fi
    rm -rf "${temp_dir}"
    return 0
  fi

  if [[ "${uploads_enabled}" == "true" ]]; then
    ensure_aws_session
    aws s3api head-bucket --bucket "${uploads_bucket}" >/dev/null 2>&1 || die "upload bucket not reachable: ${uploads_bucket}"
    local cors_json=""
    cors_json="$(aws s3api get-bucket-cors --bucket "${uploads_bucket}" --region "${uploads_region}" --output json 2>/dev/null || true)"
    [[ -n "${cors_json}" ]] || die "upload bucket CORS is missing for ${uploads_bucket}"
    python3 - "${cors_json}" "${uploads_cors_origins}" <<'PY' || die "upload bucket CORS does not include the expected origins and methods"
import json
import sys

raw = sys.argv[1]
expected = [item.strip() for item in sys.argv[2].split(",") if item.strip()]
doc = json.loads(raw)
rules = doc.get("CORSRules", [])
methods = set()
origins = set()
for rule in rules:
    methods.update(rule.get("AllowedMethods", []))
    origins.update(rule.get("AllowedOrigins", []))
required_methods = {"PUT", "GET", "HEAD"}
if not required_methods.issubset(methods):
    raise SystemExit(1)
if expected and not set(expected).issubset(origins):
    raise SystemExit(1)
PY
    [[ -z "${uploads_bucket}" || "${uploads_bucket}" != "${S3_BUCKET}" ]] || die "upload bucket must not reuse the S3 artifact bucket"
  fi

  sc_transport_init
  local remote_preflight_script
  remote_preflight_script="set -euo pipefail
check() {
  local label=\"\$1\"
  shift
  if \"\$@\"; then
    echo \"PASS: \${label}\"
  else
    echo \"FAIL: \${label}\" >&2
    exit 1
  fi
}
check \"docker installed\" command -v docker
check \"docker compose available\" docker compose version
mkdir -p '${REMOTE_DIR}' '${REMOTE_DIR}/releases' '${REMOTE_DIR}/rendered'
tmpfile=\"\$(mktemp '${REMOTE_DIR}/.preflight.XXXXXX')\"
rm -f \"\${tmpfile}\"
echo \"PASS: deployment directory writable\"
avail_kb=\"\$(df -Pk '${REMOTE_DIR}' | awk 'NR==2 {print \$4}')\"
if [[ -z \"\${avail_kb}\" || \"\${avail_kb}\" -lt 2097152 ]]; then
  echo \"FAIL: disk headroom below 2 GiB for ${REMOTE_DIR}\" >&2
  exit 1
fi
echo \"PASS: disk headroom check (\${avail_kb} KB available)\"
if [[ '${skip_tls_checks}' == 'true' ]]; then
  echo \"PASS: TLS certificate checks skipped for sandbox/test deploys\"
else
  check \"TLS fullchain present\" test -f '${cert_fullchain}'
  check \"TLS privkey present\" test -f '${cert_privkey}'
fi
if [[ '${TRANSPORT}' == 'ssm' ]]; then
  check \"aws CLI available\" command -v aws
fi
"
  if [[ "${uploads_enabled}" == "true" ]]; then
    remote_preflight_script+="
aws s3api list-objects-v2 --bucket '${uploads_bucket}' --prefix '${uploads_prefix}' --max-keys 1 >/dev/null
echo \"PASS: upload bucket access check ${uploads_bucket}/${uploads_prefix}*\"
"
  fi
  sc_transport_run "${remote_preflight_script}"
  rm -rf "${temp_dir}"
}

run_diagnose() {
  choose_profile
  load_env_runtime_defaults
  if [[ "${TRANSPORT}" == "ssm" && "${NON_INTERACTIVE}" == "true" && -z "${INSTANCE_ID}" ]]; then
    die "non-interactive diagnose requires --instance-id for ssm transport"
  fi
  if [[ "${TRANSPORT}" == "ssh" && "${NON_INTERACTIVE}" == "true" && -z "${REMOTE_HOST}" ]]; then
    die "non-interactive diagnose requires --host for ssh transport"
  fi
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  choose_remote_target
  local -a cmd=("${DIAGNOSE_HELPER}" --transport "${TRANSPORT}")
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    cmd+=(--instance-id "${INSTANCE_ID}" --region "${AWS_REGION_NAME}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  else
    cmd+=(--host "${REMOTE_HOST}")
    [[ -n "${IDENTITY_FILE}" ]] && cmd+=(--identity-file "${IDENTITY_FILE}")
  fi
  if [[ "${REMOTE_DIR_EXPLICIT}" == "true" ]]; then
    cmd+=(--remote-dir "${REMOTE_DIR}")
  else
    cmd+=(--remote-dir "${REMOTE_DIR}/rendered")
  fi
  "${cmd[@]}"
}

run_cleanup() {
  ensure_env_file
  choose_profile
  load_env_runtime_defaults
  [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
  if [[ -n "${AWS_PROFILE_NAME}" && "${DRY_RUN}" != "true" ]]; then
    ensure_aws_session
  fi

  local -a cmd=("${CLEANUP_HELPER}" --env-file "${ENV_FILE}" --transport "${TRANSPORT}" --remote-dir "${REMOTE_DIR}")
  if [[ "${TRANSPORT}" == "ssm" ]]; then
    [[ -n "${INSTANCE_ID}" ]] && cmd+=(--instance-id "${INSTANCE_ID}")
    cmd+=(--region "${AWS_REGION_NAME}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  else
    [[ -n "${REMOTE_HOST}" ]] && cmd+=(--host "${REMOTE_HOST}")
    [[ -n "${IDENTITY_FILE}" ]] && cmd+=(--identity-file "${IDENTITY_FILE}")
    [[ -n "${AWS_REGION_NAME}" ]] && cmd+=(--region "${AWS_REGION_NAME}")
    [[ -n "${AWS_PROFILE_NAME}" ]] && cmd+=(--profile "${AWS_PROFILE_NAME}")
  fi
  [[ "${FORCE_CLEANUP}" == "true" ]] && cmd+=(--force)
  [[ "${YES_CLEANUP}" == "true" ]] && cmd+=(--yes)
  [[ "${DRY_RUN}" == "true" ]] && cmd+=(--dry-run)
  [[ "${KEEP_CERTS}" == "true" ]] && cmd+=(--keep-certs)
  [[ "${REVOKE_CERTS}" == "true" ]] && cmd+=(--revoke-certs)
  [[ "${KEEP_BUCKETS}" == "true" ]] && cmd+=(--keep-buckets)
  [[ "${KEEP_SSM}" == "true" ]] && cmd+=(--keep-ssm)
  "${cmd[@]}"
}

run_inventory() {
  FORCE_CLEANUP="false"
  YES_CLEANUP="false"
  run_cleanup
}

wizard() {
  echo "Security Center enterprise wizard"
  echo
  echo "Choose flow:"
  echo "  1. Start test deployment"
  echo "  2. Start production deployment"
  echo "  3. Advanced tools"
  prompt_value wizard_mode "Mode number" "1"

  choose_profile
  ensure_region_selected
  if [[ -n "${AWS_PROFILE_NAME}" ]]; then
    if [[ "${DRY_RUN}" != "true" ]]; then
      ensure_aws_session
    else
      warn "skipping aws sso login because --dry-run was requested"
    fi
  fi

  if [[ -f "${ENV_FILE}" ]]; then
    local recommended_workflow=""
    recommended_workflow="$(get_env_value "${ENV_FILE}" "SC_DEPLOY_WORKFLOW")"
    if [[ -n "${recommended_workflow}" ]]; then
      info "recommended deploy workflow from env: ${recommended_workflow}"
    fi
  fi

  case "${wizard_mode}" in
    1)
      info "starting guided test deployment"
      WIZARD_SELECTED_MODE="test"
      set_env_value "${ENV_FILE}" "SC_ENVIRONMENT_MODE" "test"
      set_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE" "true"
      run_bootstrap
      ;;
    2)
      info "starting guided production deployment"
      WIZARD_SELECTED_MODE="prod"
      set_env_value "${ENV_FILE}" "SC_ENVIRONMENT_MODE" "prod"
      set_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE" "false"
      run_bootstrap
      ;;
    3)
      echo "Advanced tools:"
      echo "  1. rollout from env"
      echo "  2. init env only"
      echo "  3. provision EC2"
      echo "  4. write MySQL/Redis/JWT/Auth0/Feishu values into SSM"
      echo "  5. validate"
      echo "  6. render"
      echo "  7. cert (Let's Encrypt)"
      echo "  8. deploy (prebuilt images)"
      echo "  9. build-deploy (build images on EC2)"
      echo "  10. bootstrap first-time rollout"
      echo "  11. inventory deployment resources"
      echo "  12. cleanup sandbox resources"
      echo "  13. preflight"
      echo "  14. diagnose"
      echo "  15. verify"
      echo "  16. checklist"
      echo "  17. promote user to admin"
      echo "  18. export scanner handoff"
      echo "  19. import scanner handoff"
      echo "  20. authorize scanner callback ingress"
      echo "  21. scanner rollout from handoff"
      prompt_value wizard_choice "Advanced action number" "10"
      case "${wizard_choice}" in
        1)
          run_rollout
          ;;
        2)
          interactive_init
          ;;
        3)
          run_provision
          ;;
        4)
          run_secrets
          ;;
        5)
          run_validate
          ;;
        6)
          prompt_yes_no RESOLVE_SSM "Resolve SSM secrets while rendering" "n"
          run_render
          ;;
        7)
          run_cert
          ;;
        8)
          prompt_yes_no RESOLVE_SSM "Resolve SSM secrets during deploy" "y"
          info "wizard note: preflight will run automatically before deploy"
          run_deploy_common
          ;;
        9)
          BUILD_ON_REMOTE="true"
          prompt_yes_no RESOLVE_SSM "Resolve SSM secrets during deploy" "y"
          info "wizard note: preflight will run automatically before build-deploy"
          run_deploy_common
          ;;
        10)
          prompt_yes_no DNS_READY "DNS already points to the new EC2 target" "n"
          run_bootstrap
          ;;
        11)
          run_inventory
          ;;
        12)
          FORCE_CLEANUP="false"
          run_cleanup
          ;;
        13)
          choose_remote_target
          run_preflight
          ;;
        14)
          run_diagnose
          ;;
        15)
          run_verify
          ;;
        16)
          "${DEPLOY_HELPER}" --print-checklist
          ;;
        17)
          prompt_value PROMOTE_EMAIL "User email to promote as admin" ""
          [[ -n "${PROMOTE_EMAIL}" ]] || die "user email is required"
          run_promote_admin
          ;;
        18)
          prompt_optional_value SCANNER_OUTPUT_FILE "Output file for asset-to-scanner handoff (blank prints to stdout)" ""
          [[ -n "${SCANNER_OUTPUT_FILE}" ]] && SCANNER_OUTPUT_FILE="$(resolve_path "${SCANNER_OUTPUT_FILE}")"
          run_scanner_export
          ;;
        19)
          prompt_value SCANNER_HANDOFF_FILE "Scanner-to-asset handoff file" ""
          [[ -n "${SCANNER_HANDOFF_FILE}" ]] || die "scanner handoff file is required"
          SCANNER_HANDOFF_FILE="$(resolve_path "${SCANNER_HANDOFF_FILE}")"
          prompt_yes_no SCANNER_APPLY "Apply scanner handoff updates to enterprise env" "n"
          run_scanner_config
          ;;
        20)
          run_scanner_ingress
          ;;
        21)
          prompt_value SCANNER_HANDOFF_FILE "Scanner-to-asset handoff file" ""
          [[ -n "${SCANNER_HANDOFF_FILE}" ]] || die "scanner handoff file is required"
          SCANNER_HANDOFF_FILE="$(resolve_path "${SCANNER_HANDOFF_FILE}")"
          run_scanner_rollout
          ;;
        *)
          die "unknown advanced action: ${wizard_choice}"
          ;;
      esac
      ;;
    *)
      die "unknown mode: ${wizard_mode}"
      ;;
  esac
}

COMMAND="wizard"
ENV_FILE="${DEFAULT_ENV_FILE}"
AWS_PROFILE_NAME="${AWS_PROFILE:-}"
AWS_REGION_NAME="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}"
TRANSPORT="ssm"
REMOTE_HOST=""
INSTANCE_ID=""
SSH_USER="ec2-user"
IDENTITY_FILE=""
REMOTE_DIR="${DEFAULT_REMOTE_DIR}"
REMOTE_DIR_EXPLICIT="false"
SOURCE_DIR="${DEFAULT_SOURCE_DIR}"
OUTPUT_DIR="${SCRIPT_DIR}/rendered"
S3_BUCKET=""
S3_PREFIX=""
DB_BACKUP_S3_BUCKET=""
DB_BACKUP_S3_PREFIX=""
DB_BACKUP_S3_REGION=""
RESOLVE_SSM="false"
DRY_RUN="false"
SKIP_UPLOAD="false"
SKIP_BUILD="false"
SKIP_UP="false"
SKIP_DB_MIGRATIONS="false"
SKIP_DB_BACKUP="false"
ALLOW_SCHEMA_DRIFT="false"
BUILD_ON_REMOTE="false"
NON_INTERACTIVE="false"
DNS_READY="false"
FORCE_CLEANUP="false"
YES_CLEANUP="false"
KEEP_CERTS="false"
REVOKE_CERTS="false"
KEEP_BUCKETS="false"
KEEP_SSM="false"
PROMOTE_EMAIL=""
SCANNER_OUTPUT_FILE=""
SCANNER_HANDOFF_FILE=""
SCANNER_APPLY="false"
SCANNER_ALLOW_TOKEN_PARAM_UPDATE="false"
GENERATE_MISSING_SECRETS="false"
SECRET_EXTRA_ARGS=()

while (($# > 0)); do
  case "$1" in
    wizard|init|provision|secrets|scanner-export|scanner-config|scanner-ingress|rollout|scanner-rollout|promote-admin|validate|render|cert|deploy|build-deploy|db-backup|bootstrap|inventory|cleanup|verify|preflight|diagnose|checklist|profiles|help)
      COMMAND="$1"
      shift
      ;;
    --env-file)
      ENV_FILE="$2"
      shift 2
      ;;
    --profile)
      AWS_PROFILE_NAME="$2"
      shift 2
      ;;
    --transport)
      TRANSPORT="$2"
      shift 2
      ;;
    --region)
      AWS_REGION_NAME="$2"
      shift 2
      ;;
    --host)
      REMOTE_HOST="$2"
      shift 2
      ;;
    --instance-id)
      INSTANCE_ID="$2"
      shift 2
      ;;
    --identity-file)
      IDENTITY_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --remote-dir)
      REMOTE_DIR="$2"
      REMOTE_DIR_EXPLICIT="true"
      shift 2
      ;;
    --source-dir)
      SOURCE_DIR="$(resolve_path "$2")"
      shift 2
      ;;
    --s3-bucket)
      S3_BUCKET="$2"
      shift 2
      ;;
    --s3-prefix)
      S3_PREFIX="$2"
      shift 2
      ;;
    --backup-s3-bucket)
      DB_BACKUP_S3_BUCKET="$2"
      shift 2
      ;;
    --backup-s3-prefix)
      DB_BACKUP_S3_PREFIX="$2"
      shift 2
      ;;
    --backup-s3-region)
      DB_BACKUP_S3_REGION="$2"
      shift 2
      ;;
    --output-dir)
      OUTPUT_DIR="$(resolve_path "$2")"
      shift 2
      ;;
    --email)
      PROMOTE_EMAIL="$2"
      shift 2
      ;;
    --output-file)
      SCANNER_OUTPUT_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --handoff-file)
      SCANNER_HANDOFF_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --apply)
      SCANNER_APPLY="true"
      shift
      ;;
    --allow-token-param-update)
      SCANNER_ALLOW_TOKEN_PARAM_UPDATE="true"
      shift
      ;;
    --generate-missing-secrets)
      GENERATE_MISSING_SECRETS="true"
      shift
      ;;
    --resolve-ssm)
      RESOLVE_SSM="true"
      shift
      ;;
    --dns-ready)
      DNS_READY="true"
      shift
      ;;
    --dry-run)
      DRY_RUN="true"
      shift
      ;;
    --skip-db-migrations)
      SKIP_DB_MIGRATIONS="true"
      shift
      ;;
    --skip-db-backup)
      SKIP_DB_BACKUP="true"
      shift
      ;;
    --allow-schema-drift)
      ALLOW_SCHEMA_DRIFT="true"
      shift
      ;;
    --force)
      FORCE_CLEANUP="true"
      shift
      ;;
    --yes)
      YES_CLEANUP="true"
      shift
      ;;
    --keep-certs)
      KEEP_CERTS="true"
      shift
      ;;
    --revoke-certs)
      REVOKE_CERTS="true"
      shift
      ;;
    --keep-buckets)
      KEEP_BUCKETS="true"
      shift
      ;;
    --keep-ssm)
      KEEP_SSM="true"
      shift
      ;;
    --skip-upload)
      SKIP_UPLOAD="true"
      shift
      ;;
    --skip-build)
      SKIP_BUILD="true"
      shift
      ;;
    --skip-up)
      SKIP_UP="true"
      shift
      ;;
    --non-interactive)
      NON_INTERACTIVE="true"
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    --mysql-root-password|--mysql-app-password|--redis-password|--jwt-access-secret|--jwt-refresh-secret|--auth0-client-secret|--feishu-org-app-id|--feishu-org-app-secret|--feishu-alert-app-id|--feishu-alert-app-secret|--dast-api-token|--dast-callback-token)
      SECRET_EXTRA_ARGS+=("$1" "$2")
      shift 2
      ;;
    *)
      die "unknown argument: $1"
      ;;
  esac
done

ENV_FILE="$(resolve_path "${ENV_FILE}")"
load_env_runtime_defaults
[[ -n "${IDENTITY_FILE}" && ! -f "${IDENTITY_FILE}" ]] && die "identity file not found: ${IDENTITY_FILE}"

case "${COMMAND}" in
  wizard)
    wizard
    ;;
  init)
    interactive_init
    ;;
  provision)
    run_provision
    ;;
  secrets)
    run_secrets
    ;;
  scanner-export)
    run_scanner_export
    ;;
  scanner-config)
    run_scanner_config
    ;;
  scanner-ingress)
    run_scanner_ingress
    ;;
  rollout)
    run_rollout
    ;;
  scanner-rollout)
    run_scanner_rollout
    ;;
  promote-admin)
    run_promote_admin
    ;;
  validate)
    run_validate
    ;;
  render)
    run_render
    ;;
  cert)
    run_cert
    ;;
  deploy)
    run_deploy_common
    ;;
  build-deploy)
    BUILD_ON_REMOTE="true"
    run_deploy_common
    ;;
  db-backup)
    run_db_backup
    ;;
  bootstrap)
    run_bootstrap
    ;;
  cleanup)
    run_cleanup
    ;;
  inventory)
    run_inventory
    ;;
  verify)
    run_verify
    ;;
  preflight)
    ensure_env_file
    choose_profile
    load_env_runtime_defaults
    if [[ "${TRANSPORT}" == "ssm" && "${NON_INTERACTIVE}" == "true" && -z "${INSTANCE_ID}" ]]; then
      die "non-interactive preflight requires --instance-id for ssm transport"
    fi
    if [[ "${TRANSPORT}" == "ssh" && "${NON_INTERACTIVE}" == "true" && -z "${REMOTE_HOST}" ]]; then
      die "non-interactive preflight requires --host for ssh transport"
    fi
    [[ -n "${AWS_REGION_NAME}" ]] && export AWS_REGION="${AWS_REGION_NAME}" AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
    [[ -n "${AWS_PROFILE_NAME}" ]] && export AWS_PROFILE="${AWS_PROFILE_NAME}" AWS_SDK_LOAD_CONFIG=1
    if [[ -n "${AWS_PROFILE_NAME}" ]]; then
      ensure_aws_session
    fi
    choose_remote_target
    run_preflight
    ;;
  diagnose)
    run_diagnose
    ;;
  checklist)
    "${DEPLOY_HELPER}" --print-checklist
    ;;
  profiles)
    list_profiles
    ;;
  help)
    usage
    ;;
  *)
    die "unknown command: ${COMMAND}"
    ;;
esac
