#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
DEFAULT_ENV_FILE="${SCRIPT_DIR}/enterprise.env"
DEFAULT_KEY_DIR="${SCRIPT_DIR}/.state/keys"
DEFAULT_AMI_SSM_PARAM="/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
DEFAULT_INSTANCE_TYPE="t3.medium"
DEFAULT_VOLUME_SIZE_GB="40"
DEFAULT_SSH_USER="ec2-user"
DEFAULT_REMOTE_TRANSPORT="ssm"
DEFAULT_DOCKER_COMPOSE_VERSION="2.36.2"

usage() {
  cat <<'EOF'
Provision one Security Center enterprise EC2 target.

This helper creates or reuses:
  - Security Group
  - IAM role + instance profile
  - EC2 instance
  - Elastic IP
  - S3 artifact bucket for ssm transport
  - optional private S3 upload bucket for vulnerability images

SSH key pair is only created when ssh fallback is explicitly enabled.

It also persists the resulting identifiers back into the enterprise env file.

Usage:
  ./scripts/enterprise/provision-enterprise-ec2.sh --env-file ./scripts/enterprise/enterprise.env

Options:
  --env-file <path>        Enterprise env file. Default: scripts/enterprise/enterprise.env
  --region <name>          AWS region override
  --profile <name>         AWS CLI profile override
  --key-dir <path>         Local directory used to store generated SSH keys
  --transport <mode>       Remote transport to prepare. Default: ssm
  --dry-run                Print planned AWS actions without changing resources
  --help                   Show this help
EOF
}

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

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

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

resolve_path() {
  local path="$1"
  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"
}

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

slugify() {
  local value
  value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
  value="$(printf '%s' "$value" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
  printf '%s' "${value:-security-center-enterprise}"
}

normalize_s3_bucket_name() {
  local raw="${1:-}"
  local normalized

  normalized="$(printf '%s' "${raw}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9.-]+/-/g; s/^[.-]+//; s/[.-]+$//; s/-+/-/g; s/\.+/./g')"
  normalized="${normalized:0:63}"
  normalized="$(printf '%s' "${normalized}" | sed -E 's/^[.-]+//; s/[.-]+$//')"

  if [[ -z "${normalized}" ]]; then
    normalized="security-center-artifacts"
  fi

  printf '%s' "${normalized}"
}

common_tag_value() {
  local key="$1"
  case "${key}" in
    Project)
      printf '%s' "security-center"
      ;;
    ManagedBy)
      printf '%s' "scripts/enterprise"
      ;;
    StackId)
      printf '%s' "${SC_EC2_NAME_PREFIX}"
      ;;
    PublicHostname)
      printf '%s' "${SC_PUBLIC_HOSTNAME}"
      ;;
    SSMPathPrefix)
      printf '%s' "${SC_SSM_PATH_PREFIX%/}"
      ;;
    *)
      return 1
      ;;
  esac
}

tag_args_with_optional_name() {
  local name_value="${1:-}"
  local -a args=(
    "Key=Project,Value=$(common_tag_value Project)"
    "Key=ManagedBy,Value=$(common_tag_value ManagedBy)"
    "Key=StackId,Value=$(common_tag_value StackId)"
    "Key=PublicHostname,Value=$(common_tag_value PublicHostname)"
    "Key=SSMPathPrefix,Value=$(common_tag_value SSMPathPrefix)"
  )
  if [[ -n "${name_value}" ]]; then
    args=("Key=Name,Value=${name_value}" "${args[@]}")
  fi
  printf '%s\n' "${args[@]}"
}

apply_ec2_tags() {
  local resource_id="$1"
  local name_value="${2:-}"
  [[ -n "${resource_id}" ]] || return 0
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would apply enterprise tags to EC2 resource ${resource_id}"
    return 0
  fi
  local -a cmd=(aws ec2 create-tags --region "${AWS_REGION_NAME}" --resources "${resource_id}" --tags)
  local item=""
  while IFS= read -r item; do
    [[ -n "${item}" ]] || continue
    cmd+=("${item}")
  done < <(tag_args_with_optional_name "${name_value}")
  "${cmd[@]}" >/dev/null
}

apply_iam_tags() {
  local resource_kind="$1"
  local resource_name="$2"
  local name_value="${3:-}"
  [[ -n "${resource_name}" ]] || return 0
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would apply enterprise tags to IAM ${resource_kind} ${resource_name}"
    return 0
  fi
  local -a cmd=(aws iam)
  case "${resource_kind}" in
    role)
      cmd+=(tag-role --role-name "${resource_name}" --tags)
      ;;
    instance-profile)
      cmd+=(tag-instance-profile --instance-profile-name "${resource_name}" --tags)
      ;;
    *)
      die "unsupported iam tag resource kind: ${resource_kind}"
      ;;
  esac
  local item=""
  while IFS= read -r item; do
    [[ -n "${item}" ]] || continue
    cmd+=("${item}")
  done < <(tag_args_with_optional_name "${name_value}")
  "${cmd[@]}" >/dev/null
}

apply_ssm_parameter_tags() {
  local parameter_name="$1"
  [[ -n "${parameter_name}" ]] || return 0
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would apply enterprise tags to SSM parameter ${parameter_name}"
    return 0
  fi
  local -a cmd=(aws ssm add-tags-to-resource --region "${AWS_REGION_NAME}" --resource-type Parameter --resource-id "${parameter_name}" --tags)
  local item=""
  while IFS= read -r item; do
    [[ -n "${item}" ]] || continue
    cmd+=("${item}")
  done < <(tag_args_with_optional_name "")
  "${cmd[@]}" >/dev/null
}

apply_bucket_tags() {
  local bucket_name="$1"
  local bucket_purpose="$2"
  [[ -n "${bucket_name}" ]] || return 0
  local tag_file=""
  tag_file="$(mktemp /tmp/security-center-bucket-tags.XXXXXX.json)"
  python3 - "${bucket_name}" "${bucket_purpose}" "${SC_EC2_NAME_PREFIX}" "${SC_PUBLIC_HOSTNAME}" "${SC_SSM_PATH_PREFIX%/}" > "${tag_file}" <<'PY'
import json
import sys

bucket_name, purpose, stack_id, hostname, ssm_prefix = sys.argv[1:6]
tags = [
    {"Key": "Name", "Value": bucket_name},
    {"Key": "Project", "Value": "security-center"},
    {"Key": "ManagedBy", "Value": "scripts/enterprise"},
    {"Key": "StackId", "Value": stack_id},
    {"Key": "PublicHostname", "Value": hostname},
    {"Key": "SSMPathPrefix", "Value": ssm_prefix},
    {"Key": "Purpose", "Value": purpose},
]
print(json.dumps({"TagSet": tags}))
PY
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would apply enterprise tags to S3 bucket ${bucket_name}"
    rm -f "${tag_file}"
    return 0
  fi
  aws s3api put-bucket-tagging \
    --bucket "${bucket_name}" \
    --tagging "file://${tag_file}" >/dev/null
  rm -f "${tag_file}"
}

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
}

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 = path.read_text().splitlines() if path.exists() else []
found = False
out = []
for raw in lines:
    if raw.strip().startswith(f"{key}="):
        out.append(f"{key}={value}")
        found = True
    else:
        out.append(raw)
if not found:
    if out and out[-1].strip():
        out.append("")
    out.append(f"{key}={value}")
path.write_text("\n".join(out) + "\n")
PY
}

aws_cli() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    echo "DRY-RUN aws $*"
    return 0
  fi
  aws "$@"
}

load_env_defaults() {
  ENV_FILE="$(resolve_path "${ENV_FILE}")"
  [[ -f "${ENV_FILE}" ]] || die "env file not found: ${ENV_FILE}"

  SC_PUBLIC_HOSTNAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")")"
  SC_AWS_REGION_VALUE="$(trim "$(get_env_value "${ENV_FILE}" "SC_AWS_REGION")")"
  SC_SSM_PATH_PREFIX="$(trim "$(get_env_value "${ENV_FILE}" "SC_SSM_PATH_PREFIX")")"
  SC_SANDBOX_AUTH_MODE="$(trim "$(get_env_value "${ENV_FILE}" "SC_SANDBOX_AUTH_MODE")")"
  SC_AUTH0_DOMAIN="$(trim "$(get_env_value "${ENV_FILE}" "SC_AUTH0_DOMAIN")")"
  SC_TLS_ACME_EMAIL="$(trim "$(get_env_value "${ENV_FILE}" "SC_TLS_ACME_EMAIL")")"
  SC_EC2_INSTANCE_TYPE="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_TYPE")")"
  SC_EC2_VOLUME_SIZE_GB="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_VOLUME_SIZE_GB")")"
  SC_EC2_SWAP_SIZE_GB="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_SWAP_SIZE_GB")")"
  SC_EC2_NAME_PREFIX="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_NAME_PREFIX")")"
  SC_HTTPS_INGRESS_CIDRS="$(trim "$(get_env_value "${ENV_FILE}" "SC_HTTPS_INGRESS_CIDRS")")"
  SC_HTTP_INGRESS_CIDRS="$(trim "$(get_env_value "${ENV_FILE}" "SC_HTTP_INGRESS_CIDRS")")"
  SC_SSH_INGRESS_CIDRS="$(trim "$(get_env_value "${ENV_FILE}" "SC_SSH_INGRESS_CIDRS")")"
  SC_REMOTE_TRANSPORT="$(trim "$(get_env_value "${ENV_FILE}" "SC_REMOTE_TRANSPORT")")"
  SC_ENABLE_SSH_FALLBACK="$(trim "$(get_env_value "${ENV_FILE}" "SC_ENABLE_SSH_FALLBACK")")"
  SC_EC2_INSTANCE_ID="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_ID")")"
  SC_EC2_EIP_ALLOCATION_ID="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_EIP_ALLOCATION_ID")")"
  SC_EC2_SECURITY_GROUP_ID="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_SECURITY_GROUP_ID")")"
  SC_EC2_HTTPS_INGRESS_CIDRS="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_HTTPS_INGRESS_CIDRS")")"
  SC_EC2_HTTP_INGRESS_CIDRS="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_HTTP_INGRESS_CIDRS")")"
  SC_EC2_SSH_INGRESS_CIDRS="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_SSH_INGRESS_CIDRS")")"
  SC_EC2_INSTANCE_PROFILE_NAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_PROFILE_NAME")")"
  SC_EC2_IAM_ROLE_NAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_IAM_ROLE_NAME")")"
  SC_EC2_KEY_PAIR_NAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_KEY_PAIR_NAME")")"
  SC_EC2_IDENTITY_FILE="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_IDENTITY_FILE")")"
  SC_EC2_SSH_USER="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_SSH_USER")")"
  SC_EC2_AMI_ID="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_AMI_ID")")"
  SC_S3_ARTIFACT_BUCKET="$(trim "$(get_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_BUCKET")")"
  SC_S3_ARTIFACT_PREFIX="$(trim "$(get_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_PREFIX")")"
  SC_UPLOADS_ENABLED="$(trim "$(get_env_value "${ENV_FILE}" "SC_UPLOADS_ENABLED")")"
  SC_UPLOADS_BUCKET="$(trim "$(get_env_value "${ENV_FILE}" "SC_UPLOADS_BUCKET")")"
  SC_UPLOADS_REGION="$(trim "$(get_env_value "${ENV_FILE}" "SC_UPLOADS_REGION")")"
  SC_UPLOADS_PREFIX="$(trim "$(get_env_value "${ENV_FILE}" "SC_UPLOADS_PREFIX")")"
  SC_UPLOADS_CORS_ALLOWED_ORIGINS="$(trim "$(get_env_value "${ENV_FILE}" "SC_UPLOADS_CORS_ALLOWED_ORIGINS")")"
  SC_DOCKER_COMPOSE_VERSION="$(trim "$(get_env_value "${ENV_FILE}" "SC_DOCKER_COMPOSE_VERSION")")"

  [[ -n "${SC_PUBLIC_HOSTNAME}" ]] || die "SC_PUBLIC_HOSTNAME is required in ${ENV_FILE}"
  [[ -n "${SC_SSM_PATH_PREFIX}" ]] || die "SC_SSM_PATH_PREFIX is required in ${ENV_FILE}"

  if [[ -z "${AWS_REGION_NAME}" ]]; then
    AWS_REGION_NAME="${SC_AWS_REGION_VALUE:-}"
  fi
  [[ -n "${AWS_REGION_NAME}" ]] || die "AWS region is required via --region or SC_AWS_REGION"

  SC_EC2_INSTANCE_TYPE="${SC_EC2_INSTANCE_TYPE:-$DEFAULT_INSTANCE_TYPE}"
  SC_EC2_VOLUME_SIZE_GB="${SC_EC2_VOLUME_SIZE_GB:-$DEFAULT_VOLUME_SIZE_GB}"
  SC_EC2_SWAP_SIZE_GB="${SC_EC2_SWAP_SIZE_GB:-4}"
  SC_EC2_SSH_USER="${SC_EC2_SSH_USER:-$DEFAULT_SSH_USER}"
  SC_EC2_HTTPS_INGRESS_CIDRS="${SC_EC2_HTTPS_INGRESS_CIDRS:-${SC_HTTPS_INGRESS_CIDRS:-}}"
  SC_EC2_HTTP_INGRESS_CIDRS="${SC_EC2_HTTP_INGRESS_CIDRS:-${SC_HTTP_INGRESS_CIDRS:-}}"
  SC_EC2_SSH_INGRESS_CIDRS="${SC_EC2_SSH_INGRESS_CIDRS:-${SC_SSH_INGRESS_CIDRS:-}}"
  SC_REMOTE_TRANSPORT="${SC_REMOTE_TRANSPORT:-$TRANSPORT}"
  [[ -n "${SC_REMOTE_TRANSPORT}" ]] || SC_REMOTE_TRANSPORT="${DEFAULT_REMOTE_TRANSPORT}"
  [[ "${SC_REMOTE_TRANSPORT}" == "ssm" || "${SC_REMOTE_TRANSPORT}" == "ssh" ]] || die "SC_REMOTE_TRANSPORT must be ssm or ssh"
  SC_ENABLE_SSH_FALLBACK="${SC_ENABLE_SSH_FALLBACK:-false}"

  if [[ -z "${SC_EC2_NAME_PREFIX}" ]]; then
    local derived
    derived="security-center-$(slugify "${SC_PUBLIC_HOSTNAME%%.*}")"
    SC_EC2_NAME_PREFIX="${derived}"
  fi
  SC_EC2_KEY_PAIR_NAME="${SC_EC2_KEY_PAIR_NAME:-${SC_EC2_NAME_PREFIX}-key}"
  SC_EC2_IAM_ROLE_NAME="${SC_EC2_IAM_ROLE_NAME:-${SC_EC2_NAME_PREFIX}-role}"
  SC_EC2_INSTANCE_PROFILE_NAME="${SC_EC2_INSTANCE_PROFILE_NAME:-${SC_EC2_NAME_PREFIX}-profile}"
  SC_EC2_IDENTITY_FILE="${SC_EC2_IDENTITY_FILE:-${KEY_DIR}/${SC_EC2_KEY_PAIR_NAME}.pem}"
  if [[ -z "${SC_S3_ARTIFACT_PREFIX}" ]]; then
    SC_S3_ARTIFACT_PREFIX="${SC_EC2_NAME_PREFIX}/artifacts"
  fi
  SC_UPLOADS_ENABLED="${SC_UPLOADS_ENABLED:-false}"
  SC_UPLOADS_REGION="${SC_UPLOADS_REGION:-${AWS_REGION_NAME}}"
  SC_UPLOADS_PREFIX="${SC_UPLOADS_PREFIX:-vuln-uploads/}"
  SC_DOCKER_COMPOSE_VERSION="${SC_DOCKER_COMPOSE_VERSION:-${DEFAULT_DOCKER_COMPOSE_VERSION}}"
  if [[ -z "${SC_SANDBOX_AUTH_MODE}" && "${SC_AUTH0_DOMAIN}" == "placeholder.auth0.local" ]]; then
    SC_SANDBOX_AUTH_MODE="true"
  fi
}

ssm_param_name() {
  local suffix="$1"
  local trimmed_prefix="${SC_SSM_PATH_PREFIX%/}"
  printf '%s%s' "${trimmed_prefix}" "${suffix}"
}

generate_secret_value() {
  local kind="${1:-token}"
  python3 - "${kind}" <<'PY'
import secrets
import string
import sys

kind = sys.argv[1]
if kind == "password":
    alphabet = string.ascii_letters + string.digits + "-_.!@#%^&*+="
    print("".join(secrets.choice(alphabet) for _ in range(32)))
elif kind == "mysql_root":
    alphabet = string.ascii_letters + string.digits + "!@#%^&*-_+="
    print("".join(secrets.choice(alphabet) for _ in range(40)))
else:
    print(secrets.token_urlsafe(48))
PY
}

put_ssm_parameter_if_missing() {
  local name="$1"
  local type="$2"
  local value="$3"

  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would create SSM parameter ${name} (${type}) if it does not already exist"
    return 0
  fi

  if aws ssm get-parameter --name "${name}" --region "${AWS_REGION_NAME}" >/dev/null 2>&1; then
    apply_ssm_parameter_tags "${name}"
    return 0
  fi

  aws ssm put-parameter \
    --name "${name}" \
    --type "${type}" \
    --value "${value}" \
    --overwrite \
    --region "${AWS_REGION_NAME}" >/dev/null
  apply_ssm_parameter_tags "${name}"
  info "created missing SSM parameter ${name}"
}

ensure_minimum_ssm_parameters() {
  local mysql_root_name mysql_app_name redis_name jwt_access_name jwt_refresh_name
  mysql_root_name="$(ssm_param_name "/mysql/root_password")"
  mysql_app_name="$(ssm_param_name "/mysql/app_password")"
  redis_name="$(ssm_param_name "/redis/password")"
  jwt_access_name="$(ssm_param_name "/jwt/access_secret")"
  jwt_refresh_name="$(ssm_param_name "/jwt/refresh_secret")"

  put_ssm_parameter_if_missing "${mysql_root_name}" "SecureString" "$(generate_secret_value mysql_root)"
  put_ssm_parameter_if_missing "${mysql_app_name}" "SecureString" "$(generate_secret_value password)"
  put_ssm_parameter_if_missing "${redis_name}" "SecureString" "$(generate_secret_value password)"
  put_ssm_parameter_if_missing "${jwt_access_name}" "SecureString" "$(generate_secret_value token)"
  put_ssm_parameter_if_missing "${jwt_refresh_name}" "SecureString" "$(generate_secret_value token)"

  if is_truthy "${SC_SANDBOX_AUTH_MODE:-false}"; then
    put_ssm_parameter_if_missing "$(ssm_param_name "/auth0/client_secret")" "SecureString" "sandbox-auth0-disabled"
    put_ssm_parameter_if_missing "$(ssm_param_name "/feishu/org_sync/app_id")" "SecureString" "sandbox-feishu-org-app-id"
    put_ssm_parameter_if_missing "$(ssm_param_name "/feishu/org_sync/app_secret")" "SecureString" "sandbox-feishu-org-app-secret"
    put_ssm_parameter_if_missing "$(ssm_param_name "/feishu/alerts/app_id")" "SecureString" "sandbox-feishu-alert-app-id"
    put_ssm_parameter_if_missing "$(ssm_param_name "/feishu/alerts/app_secret")" "SecureString" "sandbox-feishu-alert-app-secret"
  fi
}

ensure_aws_context() {
  require_bin aws
  require_bin python3
  export AWS_SDK_LOAD_CONFIG=1
  export AWS_REGION="${AWS_REGION_NAME}"
  export AWS_DEFAULT_REGION="${AWS_REGION_NAME}"
  if [[ -n "${AWS_PROFILE_NAME}" ]]; then
    export AWS_PROFILE="${AWS_PROFILE_NAME}"
  fi
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "skipping AWS session check because --dry-run was requested"
    return 0
  fi
  aws sts get-caller-identity >/dev/null 2>&1 || die "AWS session is not active; run aws sso login first"
}

resolve_account_id() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    ACCOUNT_ID="123456789012"
    return 0
  fi
  ACCOUNT_ID="$(aws sts get-caller-identity --query 'Account' --output text)"
  [[ -n "${ACCOUNT_ID}" && "${ACCOUNT_ID}" != "None" ]] || die "failed to resolve AWS account id"
}

resolve_default_vpc() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    DEFAULT_VPC_ID="vpc-dryrun-default"
    return 0
  fi
  DEFAULT_VPC_ID="$(aws ec2 describe-vpcs \
    --region "${AWS_REGION_NAME}" \
    --filters "Name=isDefault,Values=true" \
    --query 'Vpcs[0].VpcId' \
    --output text)"
  [[ -n "${DEFAULT_VPC_ID}" && "${DEFAULT_VPC_ID}" != "None" ]] || die "default VPC not found in ${AWS_REGION_NAME}"
}

resolve_default_subnet() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    DEFAULT_SUBNET_ID="subnet-dryrun-default"
    return 0
  fi
  DEFAULT_SUBNET_ID="$(aws ec2 describe-subnets \
    --region "${AWS_REGION_NAME}" \
    --filters "Name=vpc-id,Values=${DEFAULT_VPC_ID}" "Name=default-for-az,Values=true" \
    --query 'Subnets[0].SubnetId' \
    --output text)"
  if [[ -z "${DEFAULT_SUBNET_ID}" || "${DEFAULT_SUBNET_ID}" == "None" ]]; then
    DEFAULT_SUBNET_ID="$(aws ec2 describe-subnets \
      --region "${AWS_REGION_NAME}" \
      --filters "Name=vpc-id,Values=${DEFAULT_VPC_ID}" \
      --query 'Subnets[0].SubnetId' \
      --output text)"
  fi
  [[ -n "${DEFAULT_SUBNET_ID}" && "${DEFAULT_SUBNET_ID}" != "None" ]] || die "no subnet found in default VPC ${DEFAULT_VPC_ID}"
}

resolve_ami() {
  if [[ -n "${SC_EC2_AMI_ID}" ]]; then
    AMI_ID="${SC_EC2_AMI_ID}"
    return 0
  fi
  if [[ "${DRY_RUN}" == "true" ]]; then
    AMI_ID="ami-dry-run-amazon-linux-2023"
    return 0
  fi
  AMI_ID="$(aws ssm get-parameter \
    --region "${AWS_REGION_NAME}" \
    --name "${DEFAULT_AMI_SSM_PARAM}" \
    --query 'Parameter.Value' \
    --output text)"
  [[ -n "${AMI_ID}" && "${AMI_ID}" != "None" ]] || die "failed to resolve default Amazon Linux 2023 AMI"
}

ensure_security_group() {
  local allow_ssh="false"
  if [[ "${SC_REMOTE_TRANSPORT}" == "ssh" || "${SC_ENABLE_SSH_FALLBACK}" == "true" ]]; then
    allow_ssh="true"
  fi
  if [[ "${allow_ssh}" == "true" && -z "${SC_EC2_SSH_INGRESS_CIDRS}" ]]; then
    die "SSH fallback requires SC_EC2_SSH_INGRESS_CIDRS; refusing to open SSH without an explicit CIDR"
  fi

  if [[ "${DRY_RUN}" == "true" ]]; then
    SECURITY_GROUP_ID="${SC_EC2_SECURITY_GROUP_ID:-sg-dryrun-${SC_EC2_NAME_PREFIX}}"
    info "would create or reuse security group ${SC_EC2_NAME_PREFIX}-sg in ${DEFAULT_VPC_ID}"
    if [[ -z "${SC_EC2_SECURITY_GROUP_ID}" && -z "${SC_EC2_HTTPS_INGRESS_CIDRS}" ]]; then
      die "new security groups require SC_EC2_HTTPS_INGRESS_CIDRS, for example the enterprise VPN CIDR; refusing to default HTTPS to 0.0.0.0/0"
    fi
    if [[ -n "${SC_EC2_HTTPS_INGRESS_CIDRS}" ]]; then
      info "would authorize HTTPS ingress from ${SC_EC2_HTTPS_INGRESS_CIDRS} on ${SECURITY_GROUP_ID}"
    else
      info "would keep existing HTTPS ingress unchanged on ${SECURITY_GROUP_ID}"
    fi
    if [[ -n "${SC_EC2_HTTP_INGRESS_CIDRS}" ]]; then
      info "would authorize optional HTTP ingress from ${SC_EC2_HTTP_INGRESS_CIDRS} on ${SECURITY_GROUP_ID}"
    else
      info "would not authorize HTTP ingress by default"
    fi
    if [[ "${allow_ssh}" == "true" ]]; then
      info "would authorize SSH ingress from ${SC_EC2_SSH_INGRESS_CIDRS} on ${SECURITY_GROUP_ID}"
    else
      info "would keep SSH closed by default"
    fi
    return 0
  fi

  local reused_existing_sg="false"
  if [[ -n "${SC_EC2_SECURITY_GROUP_ID}" && "${SC_EC2_SECURITY_GROUP_ID}" != "None" ]]; then
    SECURITY_GROUP_ID="${SC_EC2_SECURITY_GROUP_ID}"
    reused_existing_sg="true"
  else
    SECURITY_GROUP_ID="$(aws ec2 describe-security-groups \
      --region "${AWS_REGION_NAME}" \
      --filters "Name=vpc-id,Values=${DEFAULT_VPC_ID}" "Name=group-name,Values=${SC_EC2_NAME_PREFIX}-sg" \
      --query 'SecurityGroups[0].GroupId' \
      --output text 2>/dev/null || true)"
    [[ "${SECURITY_GROUP_ID}" == "None" ]] && SECURITY_GROUP_ID=""
    [[ -n "${SECURITY_GROUP_ID}" ]] && reused_existing_sg="true"
  fi
  if [[ -z "${SECURITY_GROUP_ID}" ]]; then
    [[ -n "${SC_EC2_HTTPS_INGRESS_CIDRS}" ]] || die "new security groups require SC_EC2_HTTPS_INGRESS_CIDRS, for example the enterprise VPN CIDR; refusing to default HTTPS to 0.0.0.0/0"
    SECURITY_GROUP_ID="$(aws ec2 create-security-group \
      --region "${AWS_REGION_NAME}" \
      --group-name "${SC_EC2_NAME_PREFIX}-sg" \
      --description "Security Center enterprise ingress" \
      --vpc-id "${DEFAULT_VPC_ID}" \
      --tag-specifications "ResourceType=security-group,Tags=[{Key=Name,Value=${SC_EC2_NAME_PREFIX}-sg},{Key=Project,Value=security-center}]" \
      --query 'GroupId' \
      --output text)"
  fi
  apply_ec2_tags "${SECURITY_GROUP_ID}" "${SC_EC2_NAME_PREFIX}-sg"

  local permissions_json
  permissions_json="$(python3 - "${allow_ssh}" "${SC_EC2_HTTPS_INGRESS_CIDRS}" "${SC_EC2_HTTP_INGRESS_CIDRS}" "${SC_EC2_SSH_INGRESS_CIDRS}" <<'PY'
import ipaddress
import json
import sys

allow_ssh = sys.argv[1].strip().lower() == "true"
https_cidrs = sys.argv[2].strip()
http_cidrs = sys.argv[3].strip()
ssh_cidrs = sys.argv[4].strip()


def parse_cidrs(raw, name):
    cidrs = []
    for item in raw.replace(",", " ").split():
        try:
            network = ipaddress.ip_network(item, strict=False)
        except ValueError as exc:
            raise SystemExit(f"invalid {name} CIDR {item!r}: {exc}")
        if network.version != 4:
            raise SystemExit(f"{name} currently supports IPv4 CIDRs only: {item!r}")
        cidrs.append(network.with_prefixlen)
    return cidrs


def ip_ranges(cidrs, description):
    return [{"CidrIp": cidr, "Description": description} for cidr in cidrs]


https = parse_cidrs(https_cidrs, "SC_EC2_HTTPS_INGRESS_CIDRS")
http = parse_cidrs(http_cidrs, "SC_EC2_HTTP_INGRESS_CIDRS")
ssh = parse_cidrs(ssh_cidrs, "SC_EC2_SSH_INGRESS_CIDRS")
if allow_ssh and not ssh:
    raise SystemExit("SSH fallback requires SC_EC2_SSH_INGRESS_CIDRS")

rules = []
if allow_ssh:
    rules.append({
        "IpProtocol": "tcp",
        "FromPort": 22,
        "ToPort": 22,
        "IpRanges": ip_ranges(ssh, "Security Center SSH fallback"),
    })
if http:
    rules.append({
        "IpProtocol": "tcp",
        "FromPort": 80,
        "ToPort": 80,
        "IpRanges": ip_ranges(http, "Security Center optional HTTP"),
    })
if https:
    rules.append({
        "IpProtocol": "tcp",
        "FromPort": 443,
        "ToPort": 443,
        "IpRanges": ip_ranges(https, "Security Center HTTPS"),
    })
print(json.dumps(rules))
PY
)"
  if [[ "${permissions_json}" == "[]" ]]; then
    if [[ "${reused_existing_sg}" == "true" ]]; then
      info "no EC2 ingress CIDRs configured; preserving existing inbound rules on ${SECURITY_GROUP_ID}"
    else
      die "no EC2 ingress CIDRs configured for new security group ${SECURITY_GROUP_ID}"
    fi
  else
    aws ec2 authorize-security-group-ingress --region "${AWS_REGION_NAME}" --group-id "${SECURITY_GROUP_ID}" --ip-permissions \
      "${permissions_json}" >/dev/null 2>&1 || true
  fi
  if [[ "${allow_ssh}" != "true" ]]; then
    aws ec2 revoke-security-group-ingress --region "${AWS_REGION_NAME}" --group-id "${SECURITY_GROUP_ID}" --ip-permissions \
      '[{"IpProtocol":"tcp","FromPort":22,"ToPort":22,"IpRanges":[{"CidrIp":"0.0.0.0/0"}]}]' >/dev/null 2>&1 || true
  fi
}

ensure_iam_role_and_profile() {
  local assume_doc
  assume_doc="$(mktemp /tmp/security-center-assume-role.XXXXXX.json)"
  cat > "${assume_doc}" <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

  if [[ "${DRY_RUN}" == "true" ]]; then
    INSTANCE_PROFILE_NAME="${SC_EC2_INSTANCE_PROFILE_NAME}"
    ROLE_NAME="${SC_EC2_IAM_ROLE_NAME}"
    info "would create or reuse IAM role ${ROLE_NAME} and instance profile ${INSTANCE_PROFILE_NAME}"
    rm -f "${assume_doc}"
    return 0
  fi

  if ! aws iam get-role --role-name "${SC_EC2_IAM_ROLE_NAME}" >/dev/null 2>&1; then
    aws iam create-role \
      --role-name "${SC_EC2_IAM_ROLE_NAME}" \
      --assume-role-policy-document "file://${assume_doc}" >/dev/null
  fi
  apply_iam_tags "role" "${SC_EC2_IAM_ROLE_NAME}" "${SC_EC2_IAM_ROLE_NAME}"
  aws iam attach-role-policy \
    --role-name "${SC_EC2_IAM_ROLE_NAME}" \
    --policy-arn "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" >/dev/null 2>&1 || true

  if ! aws iam get-instance-profile --instance-profile-name "${SC_EC2_INSTANCE_PROFILE_NAME}" >/dev/null 2>&1; then
    aws iam create-instance-profile --instance-profile-name "${SC_EC2_INSTANCE_PROFILE_NAME}" >/dev/null
  fi
  apply_iam_tags "instance-profile" "${SC_EC2_INSTANCE_PROFILE_NAME}" "${SC_EC2_INSTANCE_PROFILE_NAME}"

  aws iam add-role-to-instance-profile \
    --instance-profile-name "${SC_EC2_INSTANCE_PROFILE_NAME}" \
    --role-name "${SC_EC2_IAM_ROLE_NAME}" >/dev/null 2>&1 || true

  ROLE_NAME="${SC_EC2_IAM_ROLE_NAME}"
  INSTANCE_PROFILE_NAME="${SC_EC2_INSTANCE_PROFILE_NAME}"
  rm -f "${assume_doc}"
}

ensure_artifact_bucket() {
  if [[ -z "${SC_S3_ARTIFACT_BUCKET}" ]]; then
    SC_S3_ARTIFACT_BUCKET="$(normalize_s3_bucket_name "${SC_EC2_NAME_PREFIX}-artifacts-${ACCOUNT_ID}-${AWS_REGION_NAME}")"
  fi

  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would create or reuse S3 artifact bucket ${SC_S3_ARTIFACT_BUCKET}"
    return 0
  fi

  if ! aws s3api head-bucket --bucket "${SC_S3_ARTIFACT_BUCKET}" >/dev/null 2>&1; then
    if [[ "${AWS_REGION_NAME}" == "us-east-1" ]]; then
      aws s3api create-bucket --bucket "${SC_S3_ARTIFACT_BUCKET}" >/dev/null
    else
      aws s3api create-bucket \
        --bucket "${SC_S3_ARTIFACT_BUCKET}" \
        --create-bucket-configuration "LocationConstraint=${AWS_REGION_NAME}" >/dev/null
    fi
  fi
  apply_bucket_tags "${SC_S3_ARTIFACT_BUCKET}" "artifact-staging"
}

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

ensure_upload_bucket() {
  if ! uploads_enabled; then
    return 0
  fi
  if [[ -z "${SC_UPLOADS_BUCKET}" ]]; then
    SC_UPLOADS_BUCKET="$(normalize_s3_bucket_name "${SC_EC2_NAME_PREFIX}-uploads-${ACCOUNT_ID}-${SC_UPLOADS_REGION}")"
  fi

  if [[ "${SC_UPLOADS_BUCKET}" == "${SC_S3_ARTIFACT_BUCKET}" ]]; then
    die "SC_UPLOADS_BUCKET must not reuse SC_S3_ARTIFACT_BUCKET; keep upload and artifact buckets separate"
  fi

  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would create or reuse S3 upload bucket ${SC_UPLOADS_BUCKET} in ${SC_UPLOADS_REGION}"
    return 0
  fi

  if ! aws s3api head-bucket --bucket "${SC_UPLOADS_BUCKET}" >/dev/null 2>&1; then
    if [[ "${SC_UPLOADS_REGION}" == "us-east-1" ]]; then
      aws s3api create-bucket --bucket "${SC_UPLOADS_BUCKET}" >/dev/null
    else
      aws s3api create-bucket \
        --bucket "${SC_UPLOADS_BUCKET}" \
        --create-bucket-configuration "LocationConstraint=${SC_UPLOADS_REGION}" >/dev/null
    fi
  fi
  apply_bucket_tags "${SC_UPLOADS_BUCKET}" "vulnerability-uploads"
}

ensure_upload_bucket_cors() {
  if ! uploads_enabled; then
    return 0
  fi

  local origins_json
  origins_json="$(python3 - "${SC_UPLOADS_CORS_ALLOWED_ORIGINS:-}" "${SC_PUBLIC_HOSTNAME}" <<'PY'
import json
import sys

raw = sys.argv[1].strip()
hostname = sys.argv[2].strip()
origins = [item.strip() for item in raw.split(",") if item.strip()]
if not origins and hostname:
    origins = [f"https://{hostname}"]
print(json.dumps(origins))
PY
)"

  local cors_file
  cors_file="$(mktemp /tmp/security-center-upload-cors.XXXXXX.json)"
  cat > "${cors_file}" <<EOF
{
  "CORSRules": [
    {
      "AllowedOrigins": ${origins_json},
      "AllowedMethods": ["PUT", "GET", "HEAD"],
      "AllowedHeaders": ["*"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 300
    }
  ]
}
EOF

  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would apply upload bucket CORS to ${SC_UPLOADS_BUCKET} with origins ${SC_UPLOADS_CORS_ALLOWED_ORIGINS:-https://${SC_PUBLIC_HOSTNAME}}"
    rm -f "${cors_file}"
    return 0
  fi

  aws s3api put-bucket-cors \
    --bucket "${SC_UPLOADS_BUCKET}" \
    --cors-configuration "file://${cors_file}" >/dev/null
  rm -f "${cors_file}"
}

ensure_role_policy_for_s3_artifacts() {
  local policy_file
  policy_file="$(mktemp /tmp/security-center-artifact-policy.XXXXXX.json)"
  cat > "${policy_file}" <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::${SC_S3_ARTIFACT_BUCKET}"]
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::${SC_S3_ARTIFACT_BUCKET}/${SC_S3_ARTIFACT_PREFIX}/*"]
    }
  ]
}
EOF
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would attach inline S3 artifact read policy to role ${SC_EC2_IAM_ROLE_NAME}"
    rm -f "${policy_file}"
    return 0
  fi
  aws iam put-role-policy \
    --role-name "${SC_EC2_IAM_ROLE_NAME}" \
    --policy-name "${SC_EC2_NAME_PREFIX}-artifact-read" \
    --policy-document "file://${policy_file}" >/dev/null
  rm -f "${policy_file}"
}

ensure_role_policy_for_uploads() {
  if ! uploads_enabled; then
    return 0
  fi

  local policy_file
  policy_file="$(mktemp /tmp/security-center-upload-policy.XXXXXX.json)"
  cat > "${policy_file}" <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::${SC_UPLOADS_BUCKET}"],
      "Condition": {
        "StringLike": {
          "s3:prefix": ["${SC_UPLOADS_PREFIX}*"]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": ["arn:aws:s3:::${SC_UPLOADS_BUCKET}/${SC_UPLOADS_PREFIX}*"]
    }
  ]
}
EOF
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would attach inline S3 upload policy to role ${SC_EC2_IAM_ROLE_NAME} scoped to ${SC_UPLOADS_BUCKET}/${SC_UPLOADS_PREFIX}*"
    rm -f "${policy_file}"
    return 0
  fi
  aws iam put-role-policy \
    --role-name "${SC_EC2_IAM_ROLE_NAME}" \
    --policy-name "${SC_EC2_NAME_PREFIX}-upload-access" \
    --policy-document "file://${policy_file}" >/dev/null
  rm -f "${policy_file}"
}

ensure_key_pair() {
  local want_ssh_fallback="false"
  if [[ "${SC_REMOTE_TRANSPORT}" == "ssh" || "${SC_ENABLE_SSH_FALLBACK}" == "true" ]]; then
    want_ssh_fallback="true"
  fi
  if [[ "${want_ssh_fallback}" != "true" ]]; then
    info "skipping SSH key pair creation because ssm is the active/default transport"
    return 0
  fi

  mkdir -p "${KEY_DIR}"
  chmod 700 "${KEY_DIR}" >/dev/null 2>&1 || true
  IDENTITY_FILE_PATH="$(resolve_path "${SC_EC2_IDENTITY_FILE}")"

  local key_exists_aws="false"
  if [[ "${DRY_RUN}" != "true" ]] && aws ec2 describe-key-pairs --region "${AWS_REGION_NAME}" --key-names "${SC_EC2_KEY_PAIR_NAME}" >/dev/null 2>&1; then
    key_exists_aws="true"
  fi

  if [[ -f "${IDENTITY_FILE_PATH}" ]]; then
    chmod 600 "${IDENTITY_FILE_PATH}" >/dev/null 2>&1 || true
  fi

  if [[ "${key_exists_aws}" == "true" && ! -f "${IDENTITY_FILE_PATH}" ]]; then
    die "AWS key pair ${SC_EC2_KEY_PAIR_NAME} already exists but local private key is missing: ${IDENTITY_FILE_PATH}"
  fi

  if [[ "${key_exists_aws}" != "true" ]]; then
    if [[ "${DRY_RUN}" == "true" ]]; then
      info "would create key pair ${SC_EC2_KEY_PAIR_NAME} and save ${IDENTITY_FILE_PATH}"
    else
      local material
      material="$(aws ec2 create-key-pair \
        --region "${AWS_REGION_NAME}" \
        --key-name "${SC_EC2_KEY_PAIR_NAME}" \
        --tag-specifications "ResourceType=key-pair,Tags=[{Key=Name,Value=${SC_EC2_KEY_PAIR_NAME}},{Key=Project,Value=security-center}]" \
        --query 'KeyMaterial' \
        --output text)"
      umask 077
      printf '%s\n' "${material}" > "${IDENTITY_FILE_PATH}"
      chmod 600 "${IDENTITY_FILE_PATH}"
    fi
  fi
  local key_pair_id=""
  if [[ "${DRY_RUN}" == "true" ]]; then
    apply_ec2_tags "key-dryrun-${SC_EC2_KEY_PAIR_NAME}" "${SC_EC2_KEY_PAIR_NAME}"
    return 0
  fi
  key_pair_id="$(aws ec2 describe-key-pairs \
    --region "${AWS_REGION_NAME}" \
    --key-names "${SC_EC2_KEY_PAIR_NAME}" \
    --query 'KeyPairs[0].KeyPairId' \
    --output text 2>/dev/null || true)"
  if [[ -n "${key_pair_id}" && "${key_pair_id}" != "None" ]]; then
    apply_ec2_tags "${key_pair_id}" "${SC_EC2_KEY_PAIR_NAME}"
  fi
}

bootstrap_host_script() {
  cat <<EOF
#!/bin/bash
set -euxo pipefail
dnf update -y
dnf install -y docker certbot awscli
systemctl enable amazon-ssm-agent
systemctl restart amazon-ssm-agent
# webroot for Let's Encrypt HTTP-01 validation: created here once, used later by
# cert-enterprise-letsencrypt.sh (and the renewal timer) to serve challenges.
mkdir -p /var/www/certbot
chmod 755 /var/www/certbot
# Note: the certbot-renew systemd timer is created and enabled explicitly by
# cert-enterprise-letsencrypt.sh once a certificate exists. We deliberately do
# NOT try to enable a timer here, because the unit shipped by the certbot
# package is not guaranteed to exist (snap/pip/newer-EPEL builds ship none),
# and silently skipping enablement is what previously left renewal disabled.
if ! swapon --show | grep -q '^/swapfile'; then
  fallocate -l ${SC_EC2_SWAP_SIZE_GB}G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=$(( ${SC_EC2_SWAP_SIZE_GB} * 1024 ))
  chmod 600 /swapfile
  mkswap /swapfile
  swapon /swapfile
  grep -q '^/swapfile ' /etc/fstab || echo '/swapfile swap swap defaults 0 0' >> /etc/fstab
fi
arch="\$(uname -m)"
case "\${arch}" in
  x86_64|aarch64) ;;
  *)
    echo "Unsupported architecture for Docker Compose plugin: \${arch}" >&2
    exit 1
    ;;
esac
compose_plugin_dir="/usr/local/lib/docker/cli-plugins"
compose_plugin_path="\${compose_plugin_dir}/docker-compose"
if ! docker compose version >/dev/null 2>&1; then
  mkdir -p "\${compose_plugin_dir}"
  curl -fsSL "https://github.com/docker/compose/releases/download/v${SC_DOCKER_COMPOSE_VERSION}/docker-compose-linux-\${arch}" -o "\${compose_plugin_path}"
  chmod +x "\${compose_plugin_path}"
fi
systemctl enable docker
systemctl restart docker
usermod -aG docker ec2-user || true
mkdir -p /opt/security-center/enterprise
chown -R ec2-user:ec2-user /opt/security-center /opt/security-center/enterprise
systemctl is-active amazon-ssm-agent >/dev/null 2>&1
docker compose version >/dev/null 2>&1
touch /opt/security-center/.bootstrap-complete
EOF
}

user_data_payload() {
  bootstrap_host_script
}

ensure_instance() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    INSTANCE_ID="${SC_EC2_INSTANCE_ID:-i-dryrun-${SC_EC2_NAME_PREFIX}}"
    info "would create or reuse EC2 instance ${SC_EC2_NAME_PREFIX}-ec2 using ${AMI_ID}"
    return 0
  fi

  INSTANCE_ID="${SC_EC2_INSTANCE_ID}"
  if [[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]]; then
    local state
    state="$(aws ec2 describe-instances \
      --region "${AWS_REGION_NAME}" \
      --instance-ids "${INSTANCE_ID}" \
      --query 'Reservations[0].Instances[0].State.Name' \
      --output text 2>/dev/null || true)"
    if [[ "${state}" == "stopped" ]]; then
      if [[ "${DRY_RUN}" == "true" ]]; then
        info "would start stopped instance ${INSTANCE_ID}"
      else
        aws ec2 start-instances --region "${AWS_REGION_NAME}" --instance-ids "${INSTANCE_ID}" >/dev/null
      fi
    fi
    if [[ -n "${state}" && "${state}" != "terminated" && "${state}" != "shutting-down" ]]; then
      return 0
    fi
    INSTANCE_ID=""
  fi

  if [[ -z "${INSTANCE_ID}" ]]; then
    INSTANCE_ID="$(aws ec2 describe-instances \
      --region "${AWS_REGION_NAME}" \
      --filters "Name=tag:Name,Values=${SC_EC2_NAME_PREFIX}-ec2" "Name=instance-state-name,Values=pending,running,stopping,stopped" \
      --query 'Reservations[0].Instances[0].InstanceId' \
      --output text 2>/dev/null || true)"
    [[ "${INSTANCE_ID}" == "None" ]] && INSTANCE_ID=""
    if [[ -n "${INSTANCE_ID}" ]]; then
      return 0
    fi
  fi
  local user_data_file
  user_data_file="$(mktemp /tmp/security-center-user-data.XXXXXX.sh)"
  user_data_payload > "${user_data_file}"
  local -a run_cmd=(aws ec2 run-instances
    --region "${AWS_REGION_NAME}"
    --image-id "${AMI_ID}"
    --instance-type "${SC_EC2_INSTANCE_TYPE}"
    --subnet-id "${DEFAULT_SUBNET_ID}"
    --security-group-ids "${SECURITY_GROUP_ID}"
    --iam-instance-profile "Name=${SC_EC2_INSTANCE_PROFILE_NAME}"
    --block-device-mappings "[{\"DeviceName\":\"/dev/xvda\",\"Ebs\":{\"VolumeSize\":${SC_EC2_VOLUME_SIZE_GB},\"VolumeType\":\"gp3\",\"DeleteOnTermination\":true}}]"
    --user-data "file://${user_data_file}"
    --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=${SC_EC2_NAME_PREFIX}-ec2},{Key=Project,Value=security-center}]"
    --query 'Instances[0].InstanceId'
    --output text)
  if [[ "${SC_REMOTE_TRANSPORT}" == "ssh" || "${SC_ENABLE_SSH_FALLBACK}" == "true" ]]; then
    run_cmd+=(--key-name "${SC_EC2_KEY_PAIR_NAME}")
  fi
  run_json="$("${run_cmd[@]}")"
  INSTANCE_ID="${run_json}"
  rm -f "${user_data_file}"
  apply_ec2_tags "${INSTANCE_ID}" "${SC_EC2_NAME_PREFIX}-ec2"
}

ensure_eip() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    ALLOCATION_ID="${SC_EC2_EIP_ALLOCATION_ID:-eipalloc-dryrun-${SC_EC2_NAME_PREFIX}}"
    PUBLIC_IP="${SC_EC2_PUBLIC_IP:-203.0.113.10}"
    info "would allocate or reuse Elastic IP ${ALLOCATION_ID} and associate it to ${INSTANCE_ID}"
    return 0
  fi

  ALLOCATION_ID="${SC_EC2_EIP_ALLOCATION_ID}"
  if [[ -z "${ALLOCATION_ID}" || "${ALLOCATION_ID}" == "None" ]]; then
    ALLOCATION_ID="$(aws ec2 describe-addresses \
      --region "${AWS_REGION_NAME}" \
      --filters "Name=tag:Name,Values=${SC_EC2_NAME_PREFIX}-eip" \
      --query 'Addresses[0].AllocationId' \
      --output text 2>/dev/null || true)"
    [[ "${ALLOCATION_ID}" == "None" ]] && ALLOCATION_ID=""
  fi
  if [[ -z "${ALLOCATION_ID}" ]]; then
    ALLOCATION_ID="$(aws ec2 allocate-address \
      --region "${AWS_REGION_NAME}" \
      --domain vpc \
      --tag-specifications "ResourceType=elastic-ip,Tags=[{Key=Name,Value=${SC_EC2_NAME_PREFIX}-eip},{Key=Project,Value=security-center}]" \
      --query 'AllocationId' \
      --output text)"
  fi
  apply_ec2_tags "${ALLOCATION_ID}" "${SC_EC2_NAME_PREFIX}-eip"
  aws ec2 associate-address \
    --region "${AWS_REGION_NAME}" \
    --instance-id "${INSTANCE_ID}" \
    --allocation-id "${ALLOCATION_ID}" \
    --allow-reassociation >/dev/null

  PUBLIC_IP="$(aws ec2 describe-addresses \
    --region "${AWS_REGION_NAME}" \
    --allocation-ids "${ALLOCATION_ID}" \
    --query 'Addresses[0].PublicIp' \
    --output text)"
  PUBLIC_DNS="$(aws ec2 describe-instances \
    --region "${AWS_REGION_NAME}" \
    --instance-ids "${INSTANCE_ID}" \
    --query 'Reservations[0].Instances[0].PublicDnsName' \
    --output text)"
}

wait_for_instance() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    PUBLIC_DNS="ec2-203-0-113-10.eu-west-2.compute.amazonaws.com"
    return 0
  fi
  aws ec2 wait instance-running --region "${AWS_REGION_NAME}" --instance-ids "${INSTANCE_ID}"
  aws ec2 wait instance-status-ok --region "${AWS_REGION_NAME}" --instance-ids "${INSTANCE_ID}"
  PUBLIC_DNS="$(aws ec2 describe-instances \
    --region "${AWS_REGION_NAME}" \
    --instance-ids "${INSTANCE_ID}" \
    --query 'Reservations[0].Instances[0].PublicDnsName' \
    --output text)"
  if [[ -z "${PUBLIC_IP:-}" || "${PUBLIC_IP:-}" == "None" ]]; then
    PUBLIC_IP="$(aws ec2 describe-instances \
      --region "${AWS_REGION_NAME}" \
      --instance-ids "${INSTANCE_ID}" \
      --query 'Reservations[0].Instances[0].PublicIpAddress' \
      --output text)"
  fi
}

wait_for_host_bootstrap() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    return 0
  fi

  local bootstrap_check="
set -euo pipefail
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1 && command -v certbot >/dev/null 2>&1 && command -v aws >/dev/null 2>&1 && test -d /opt/security-center/enterprise; then
  exit 0
fi
exit 1
"
  local bootstrap_script=""
  bootstrap_script="$(bootstrap_host_script)"

  if [[ "${SC_REMOTE_TRANSPORT}" == "ssm" ]]; then
    info "waiting for SSM registration and host bootstrap on ${INSTANCE_ID}"
    local ping_status=""
    local attempt=""
    for attempt in $(seq 1 60); do
      ping_status="$(aws ssm describe-instance-information \
        --region "${AWS_REGION_NAME}" \
        --filters "Key=InstanceIds,Values=${INSTANCE_ID}" \
        --query 'InstanceInformationList[0].PingStatus' \
        --output text 2>/dev/null || true)"
      if [[ "${ping_status}" == "Online" ]]; then
        break
      fi
      sleep 10
    done
    [[ "${ping_status}" == "Online" ]] || die "instance ${INSTANCE_ID} did not register with SSM in time"

    local bootstrap_params_file=""
    bootstrap_params_file="$(mktemp /tmp/security-center-bootstrap-params.XXXXXX.json)"
    python3 - "${bootstrap_script}" "${bootstrap_params_file}" <<'PY'
import json
import sys

script = sys.argv[1]
path = sys.argv[2]
commands = [
    "cat <<'EOF' >/tmp/security-center-host-bootstrap.sh\n" + script + "\nEOF",
    "bash /tmp/security-center-host-bootstrap.sh",
]
with open(path, "w", encoding="utf-8") as fh:
    json.dump({"commands": commands}, fh)
PY

    local command_id=""
    local command_status=""
    command_id="$(aws ssm send-command \
      --region "${AWS_REGION_NAME}" \
      --instance-ids "${INSTANCE_ID}" \
      --document-name "AWS-RunShellScript" \
      --comment "security-center host bootstrap" \
      --parameters "file://${bootstrap_params_file}" \
      --query 'Command.CommandId' \
      --output text)"
    rm -f "${bootstrap_params_file}"

    for attempt in $(seq 1 40); do
      command_status="$(aws ssm get-command-invocation \
        --region "${AWS_REGION_NAME}" \
        --command-id "${command_id}" \
        --instance-id "${INSTANCE_ID}" \
        --query 'Status' \
        --output text 2>/dev/null || true)"
      case "${command_status}" in
        Success)
          break
          ;;
        Pending|InProgress|Delayed|"")
          sleep 10
          ;;
        *)
          die "instance bootstrap readiness check failed via SSM with status: ${command_status}"
          ;;
      esac
    done
    [[ "${command_status}" == "Success" ]] || die "instance bootstrap readiness check timed out for ${INSTANCE_ID}"

    local verify_params_file=""
    verify_params_file="$(mktemp /tmp/security-center-bootstrap-verify.XXXXXX.json)"
    python3 - "${bootstrap_check}" "${verify_params_file}" <<'PY'
import json
import sys

script = sys.argv[1]
path = sys.argv[2]
with open(path, "w", encoding="utf-8") as fh:
    json.dump({"commands": [script]}, fh)
PY
    command_id="$(aws ssm send-command \
      --region "${AWS_REGION_NAME}" \
      --instance-ids "${INSTANCE_ID}" \
      --document-name "AWS-RunShellScript" \
      --comment "security-center bootstrap readiness verify" \
      --parameters "file://${verify_params_file}" \
      --query 'Command.CommandId' \
      --output text)"
    rm -f "${verify_params_file}"
    for attempt in $(seq 1 20); do
      command_status="$(aws ssm get-command-invocation \
        --region "${AWS_REGION_NAME}" \
        --command-id "${command_id}" \
        --instance-id "${INSTANCE_ID}" \
        --query 'Status' \
        --output text 2>/dev/null || true)"
      case "${command_status}" in
        Success)
          info "host bootstrap completed on ${INSTANCE_ID}"
          return 0
          ;;
        Pending|InProgress|Delayed|"")
          sleep 5
          ;;
        *)
          die "instance bootstrap verification failed via SSM with status: ${command_status}"
          ;;
      esac
    done
    die "instance bootstrap verification timed out for ${INSTANCE_ID}"
  fi

  if [[ "${SC_REMOTE_TRANSPORT}" == "ssh" || "${SC_ENABLE_SSH_FALLBACK}" == "true" ]]; then
    [[ -n "${PUBLIC_IP}" && "${PUBLIC_IP}" != "None" ]] || die "public ip is required for ssh bootstrap validation"
    [[ -n "${IDENTITY_FILE_PATH:-}" && -f "${IDENTITY_FILE_PATH}" ]] || die "ssh bootstrap validation requires a local identity file"
    info "waiting for SSH bootstrap readiness on ${SC_EC2_SSH_USER}@${PUBLIC_IP}"
    local ssh_opts=(
      -i "${IDENTITY_FILE_PATH}"
      -o StrictHostKeyChecking=no
      -o UserKnownHostsFile=/dev/null
      -o BatchMode=yes
      -o ConnectTimeout=10
    )
    ssh "${ssh_opts[@]}" "${SC_EC2_SSH_USER}@${PUBLIC_IP}" "bash -s" <<< "${bootstrap_script}"
    for attempt in $(seq 1 40); do
      if ssh "${ssh_opts[@]}" "${SC_EC2_SSH_USER}@${PUBLIC_IP}" "${bootstrap_check}" >/dev/null 2>&1; then
        info "host bootstrap completed on ${SC_EC2_SSH_USER}@${PUBLIC_IP}"
        return 0
      fi
      sleep 10
    done
    die "instance bootstrap readiness check timed out for ${SC_EC2_SSH_USER}@${PUBLIC_IP}"
  fi
}

persist_results() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "dry-run summary:"
    info "  - instance id: ${INSTANCE_ID}"
    info "  - public ip: ${PUBLIC_IP}"
    info "  - security group: ${SECURITY_GROUP_ID}"
    info "  - instance profile: ${SC_EC2_INSTANCE_PROFILE_NAME}"
    info "  - transport: ${SC_REMOTE_TRANSPORT}"
    info "  - s3 artifact bucket: ${SC_S3_ARTIFACT_BUCKET}"
    info "  - s3 artifact prefix: ${SC_S3_ARTIFACT_PREFIX}"
    if uploads_enabled; then
      info "  - uploads enabled: true"
      info "  - uploads bucket: ${SC_UPLOADS_BUCKET}"
      info "  - uploads region: ${SC_UPLOADS_REGION}"
      info "  - uploads prefix: ${SC_UPLOADS_PREFIX}"
      info "  - uploads cors origins: ${SC_UPLOADS_CORS_ALLOWED_ORIGINS:-https://${SC_PUBLIC_HOSTNAME}}"
    else
      info "  - uploads enabled: false"
    fi
    info "scanner handoff:"
    info "  - DAST_ASSET_PLATFORM_EC2_INSTANCE_ID=${INSTANCE_ID}"
    info "  - DAST_ASSET_PLATFORM_VPC_ID=${DEFAULT_VPC_ID}"
    info "  - DAST_ASSET_PLATFORM_BACKEND_SECURITY_GROUP_ID=${SECURITY_GROUP_ID}"
    info "  - DAST_CALLBACK_URL=https://${SC_PUBLIC_HOSTNAME}/api/v1/scans/callback"
    info "  - DAST_SSM_PARAM_API_TOKEN=$(get_env_value "${ENV_FILE}" "SC_DAST_API_TOKEN_PARAM")"
    info "  - DAST_SSM_PARAM_CALLBACK_TOKEN=$(get_env_value "${ENV_FILE}" "SC_DAST_CALLBACK_TOKEN_PARAM")"
    if [[ "${SC_REMOTE_TRANSPORT}" == "ssh" || "${SC_ENABLE_SSH_FALLBACK}" == "true" ]]; then
      info "  - key pair: ${SC_EC2_KEY_PAIR_NAME}"
      info "  - local identity file: ${IDENTITY_FILE_PATH}"
    fi
    info "  - dns A record target: ${PUBLIC_IP}"
    return 0
  fi

  set_env_value "${ENV_FILE}" "SC_REMOTE_TRANSPORT" "${SC_REMOTE_TRANSPORT}"
  set_env_value "${ENV_FILE}" "SC_ENABLE_SSH_FALLBACK" "${SC_ENABLE_SSH_FALLBACK}"
  set_env_value "${ENV_FILE}" "SC_AWS_ACCOUNT_ID" "${ACCOUNT_ID}"
  set_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_ID" "${INSTANCE_ID}"
  set_env_value "${ENV_FILE}" "SC_EC2_PUBLIC_IP" "${PUBLIC_IP}"
  set_env_value "${ENV_FILE}" "SC_EC2_PUBLIC_DNS" "${PUBLIC_DNS}"
  set_env_value "${ENV_FILE}" "SC_EC2_VPC_ID" "${DEFAULT_VPC_ID}"
  set_env_value "${ENV_FILE}" "SC_EC2_SECURITY_GROUP_ID" "${SECURITY_GROUP_ID}"
  set_env_value "${ENV_FILE}" "SC_EC2_EIP_ALLOCATION_ID" "${ALLOCATION_ID}"
  set_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_PROFILE_NAME" "${SC_EC2_INSTANCE_PROFILE_NAME}"
  set_env_value "${ENV_FILE}" "SC_EC2_IAM_ROLE_NAME" "${SC_EC2_IAM_ROLE_NAME}"
  set_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_BUCKET" "${SC_S3_ARTIFACT_BUCKET}"
  set_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_PREFIX" "${SC_S3_ARTIFACT_PREFIX}"
  set_env_value "${ENV_FILE}" "SC_UPLOADS_ENABLED" "${SC_UPLOADS_ENABLED}"
  if uploads_enabled; then
    set_env_value "${ENV_FILE}" "SC_UPLOADS_BUCKET" "${SC_UPLOADS_BUCKET}"
    set_env_value "${ENV_FILE}" "SC_UPLOADS_REGION" "${SC_UPLOADS_REGION}"
    set_env_value "${ENV_FILE}" "SC_UPLOADS_PREFIX" "${SC_UPLOADS_PREFIX}"
    set_env_value "${ENV_FILE}" "SC_UPLOADS_CORS_ALLOWED_ORIGINS" "${SC_UPLOADS_CORS_ALLOWED_ORIGINS:-https://${SC_PUBLIC_HOSTNAME}}"
  fi
  if [[ "${SC_REMOTE_TRANSPORT}" == "ssh" || "${SC_ENABLE_SSH_FALLBACK}" == "true" ]]; then
    set_env_value "${ENV_FILE}" "SC_EC2_KEY_PAIR_NAME" "${SC_EC2_KEY_PAIR_NAME}"
    set_env_value "${ENV_FILE}" "SC_EC2_IDENTITY_FILE" "${IDENTITY_FILE_PATH}"
  fi
  set_env_value "${ENV_FILE}" "SC_EC2_SSH_USER" "${SC_EC2_SSH_USER}"
  set_env_value "${ENV_FILE}" "SC_EC2_AMI_ID" "${AMI_ID}"
  set_env_value "${ENV_FILE}" "SC_EC2_INSTANCE_TYPE" "${SC_EC2_INSTANCE_TYPE}"
  set_env_value "${ENV_FILE}" "SC_EC2_VOLUME_SIZE_GB" "${SC_EC2_VOLUME_SIZE_GB}"
  set_env_value "${ENV_FILE}" "SC_EC2_SWAP_SIZE_GB" "${SC_EC2_SWAP_SIZE_GB}"
  set_env_value "${ENV_FILE}" "SC_EC2_NAME_PREFIX" "${SC_EC2_NAME_PREFIX}"

  info "enterprise EC2 target ready"
  info "  - instance id: ${INSTANCE_ID}"
  info "  - public ip: ${PUBLIC_IP}"
  info "  - public dns: ${PUBLIC_DNS:-<pending>}"
  info "  - transport: ${SC_REMOTE_TRANSPORT}"
  info "  - instance id: ${INSTANCE_ID}"
  info "  - s3 artifact bucket: ${SC_S3_ARTIFACT_BUCKET}"
  info "  - s3 artifact prefix: ${SC_S3_ARTIFACT_PREFIX}"
  if uploads_enabled; then
    info "  - uploads bucket: ${SC_UPLOADS_BUCKET}"
    info "  - uploads region: ${SC_UPLOADS_REGION}"
    info "  - uploads prefix: ${SC_UPLOADS_PREFIX}"
    info "  - uploads cors origins: ${SC_UPLOADS_CORS_ALLOWED_ORIGINS:-https://${SC_PUBLIC_HOSTNAME}}"
  fi
  if [[ "${SC_REMOTE_TRANSPORT}" == "ssh" || "${SC_ENABLE_SSH_FALLBACK}" == "true" ]]; then
    info "  - ssh target: ${SC_EC2_SSH_USER}@${PUBLIC_IP}"
    info "  - local identity file: ${IDENTITY_FILE_PATH}"
  fi
  info "scanner handoff:"
  info "  - DAST_ASSET_PLATFORM_EC2_INSTANCE_ID=${INSTANCE_ID}"
  info "  - DAST_ASSET_PLATFORM_VPC_ID=${DEFAULT_VPC_ID}"
  info "  - DAST_ASSET_PLATFORM_BACKEND_SECURITY_GROUP_ID=${SECURITY_GROUP_ID}"
  info "  - DAST_CALLBACK_URL=https://${SC_PUBLIC_HOSTNAME}/api/v1/scans/callback"
  info "  - DAST_SSM_PARAM_API_TOKEN=$(get_env_value "${ENV_FILE}" "SC_DAST_API_TOKEN_PARAM")"
  info "  - DAST_SSM_PARAM_CALLBACK_TOKEN=$(get_env_value "${ENV_FILE}" "SC_DAST_CALLBACK_TOKEN_PARAM")"
  info "  - add/update DNS A record for ${SC_PUBLIC_HOSTNAME} -> ${PUBLIC_IP}"
  if [[ -n "${SC_TLS_ACME_EMAIL}" ]]; then
    info "  - cert email already configured: ${SC_TLS_ACME_EMAIL}"
  else
    warn "SC_TLS_ACME_EMAIL is not set in ${ENV_FILE}; cert automation will require it later"
  fi
}

ENV_FILE="${DEFAULT_ENV_FILE}"
AWS_PROFILE_NAME="${AWS_PROFILE:-}"
AWS_REGION_NAME="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}"
KEY_DIR="${DEFAULT_KEY_DIR}"
TRANSPORT="${DEFAULT_REMOTE_TRANSPORT}"
DRY_RUN="false"

while (($# > 0)); do
  case "$1" in
    --env-file)
      ENV_FILE="$2"
      shift 2
      ;;
    --profile)
      AWS_PROFILE_NAME="$2"
      shift 2
      ;;
    --region)
      AWS_REGION_NAME="$2"
      shift 2
      ;;
    --key-dir)
      KEY_DIR="$(resolve_path "$2")"
      shift 2
      ;;
    --transport)
      TRANSPORT="$2"
      shift 2
      ;;
    --dry-run)
      DRY_RUN="true"
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      die "unknown argument: $1"
      ;;
  esac
done

KEY_DIR="$(resolve_path "${KEY_DIR}")"
load_env_defaults
SC_REMOTE_TRANSPORT="${TRANSPORT:-${SC_REMOTE_TRANSPORT}}"
ensure_aws_context
resolve_account_id
resolve_default_vpc
resolve_default_subnet
resolve_ami
ensure_security_group
ensure_iam_role_and_profile
ensure_artifact_bucket
ensure_role_policy_for_s3_artifacts
ensure_upload_bucket
ensure_upload_bucket_cors
ensure_role_policy_for_uploads
ensure_minimum_ssm_parameters
ensure_key_pair
ensure_instance
wait_for_instance
ensure_eip
wait_for_host_bootstrap
persist_results
