#!/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"

usage() {
  cat <<'EOF'
Manage independent scanner handoff files for the enterprise deployment.

Commands:
  export                 Print or write asset-platform values for scanner deployment
  import                 Read scanner deployment handoff values and update enterprise env

Export usage:
  ./scripts/enterprise/scanner-handoff-enterprise.sh export --env-file ./scripts/enterprise/enterprise.env --output-file ./asset-to-scanner.env

Import usage:
  ./scripts/enterprise/scanner-handoff-enterprise.sh import --env-file ./scripts/enterprise/enterprise.env --handoff-file ./scanner-to-asset.env [--apply]

Options:
  --env-file <path>              Enterprise env file. Default: scripts/enterprise/enterprise.env
  --output-file <path>           Export target file. Defaults to stdout.
  --handoff-file <path>          Scanner-to-asset handoff env file for import.
  --apply                        Apply import updates. Without this, import is a dry-run.
  --allow-token-param-update     Allow scanner handoff to update token parameter names.
  --help                         Show this help.
EOF
}

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

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

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"
}

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 = []
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
}

handoff_value() {
  local file="$1"
  local key="$2"
  get_env_value "${file}" "${key}"
}

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

validate_https_origin() {
  local key="$1"
  local value="$2"
  python3 - "$key" "$value" <<'PY'
from urllib.parse import urlparse
import ipaddress
import sys

key, raw = sys.argv[1], sys.argv[2].strip()
parsed = urlparse(raw)
if parsed.scheme != "https" or not parsed.netloc:
    raise SystemExit(f"{key} must be an https:// origin")
if parsed.path not in ("", "/") or parsed.params or parsed.query or parsed.fragment:
    raise SystemExit(f"{key} must not include path/query/fragment")
host = (parsed.hostname or "").lower()
if host in {"localhost", "0.0.0.0"}:
    raise SystemExit(f"{key} must not point to localhost")
try:
    ip = ipaddress.ip_address(host)
except ValueError:
    ip = None
if ip and (ip.is_loopback or ip.is_unspecified):
    raise SystemExit(f"{key} must not point to loopback/unspecified IP")
PY
}

normalize_https_origin() {
  local key="$1"
  local value="$2"
  value="${value%/}"
  validate_https_origin "${key}" "${value}"
  printf '%s' "${value}"
}

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

validate_sg_id() {
  local key="$1"
  local value="$2"
  [[ -z "${value}" || "${value}" =~ ^sg-[a-zA-Z0-9]+$ ]] || die "${key} must be blank or an sg-* security group id"
}

validate_ip() {
  local key="$1"
  local value="$2"
  [[ -z "${value}" ]] && return 0
  python3 - "$key" "$value" <<'PY'
import ipaddress
import sys
try:
    ipaddress.ip_address(sys.argv[2])
except ValueError:
    raise SystemExit(f"{sys.argv[1]} must be a valid IP address")
PY
}

validate_cidr() {
  local key="$1"
  local value="$2"
  [[ -z "${value}" ]] && return 0
  python3 - "$key" "$value" <<'PY'
import ipaddress
import sys
try:
    network = ipaddress.ip_network(sys.argv[2], strict=False)
except ValueError:
    raise SystemExit(f"{sys.argv[1]} must be a valid IP CIDR")
if network.version != 4:
    raise SystemExit(f"{sys.argv[1]} currently supports IPv4 CIDRs only")
PY
}

validate_ssm_param() {
  local key="$1"
  local value="$2"
  [[ -z "${value}" || "${value}" == /* ]] || die "${key} must be blank or an absolute SSM parameter name"
}

ssm_param_arn() {
  local region="$1"
  local account_id="$2"
  local name="$3"
  [[ -n "${region}" ]] || region="<region>"
  [[ -n "${account_id}" ]] || account_id="<account-id>"
  [[ "${name}" == /* ]] || name="/${name}"
  printf 'arn:aws:ssm:%s:%s:parameter%s' "${region}" "${account_id}" "${name}"
}

validate_optional_bool() {
  local key="$1"
  local value="$2"
  case "${value}" in
    ""|true|false|TRUE|FALSE|1|0|yes|no|YES|NO|y|n|Y|N)
      return 0
      ;;
    *)
      die "${key} must be true or false when provided"
      ;;
  esac
}

write_export_content() {
  local env_file="$1"
  local public_hostname callback_origin instance_id vpc_id backend_sg api_param callback_param callback_url aws_region account_id kms_key_arn api_param_arn callback_param_arn
  public_hostname="$(trim "$(get_env_value "${env_file}" "SC_PUBLIC_HOSTNAME")")"
  callback_origin="$(trim "$(get_env_value "${env_file}" "SC_DAST_CALLBACK_ORIGIN")")"
  instance_id="$(trim "$(get_env_value "${env_file}" "SC_EC2_INSTANCE_ID")")"
  vpc_id="$(trim "$(get_env_value "${env_file}" "SC_EC2_VPC_ID")")"
  backend_sg="$(trim "$(get_env_value "${env_file}" "SC_EC2_SECURITY_GROUP_ID")")"
  api_param="$(trim "$(get_env_value "${env_file}" "SC_DAST_API_TOKEN_PARAM")")"
  callback_param="$(trim "$(get_env_value "${env_file}" "SC_DAST_CALLBACK_TOKEN_PARAM")")"
  aws_region="$(trim "$(get_env_value "${env_file}" "SC_AWS_REGION")")"
  account_id="$(trim "$(get_env_value "${env_file}" "SC_AWS_ACCOUNT_ID")")"
  kms_key_arn="$(trim "$(get_env_value "${env_file}" "SC_DAST_TOKEN_KMS_KEY_ARN")")"
  if [[ -z "${callback_origin}" ]]; then
    [[ -n "${public_hostname}" ]] || die "SC_PUBLIC_HOSTNAME is required for scanner export when SC_DAST_CALLBACK_ORIGIN is blank"
    callback_origin="https://${public_hostname}"
  fi
  callback_origin="$(normalize_https_origin "SC_DAST_CALLBACK_ORIGIN" "${callback_origin}")"
  [[ -n "${api_param}" ]] || die "SC_DAST_API_TOKEN_PARAM is required for scanner export"
  [[ -n "${callback_param}" ]] || die "SC_DAST_CALLBACK_TOKEN_PARAM is required for scanner export"
  callback_url="${callback_origin}/api/v1/scans/callback"
  api_param_arn="$(ssm_param_arn "${aws_region}" "${account_id}" "${api_param}")"
  callback_param_arn="$(ssm_param_arn "${aws_region}" "${account_id}" "${callback_param}")"

  cat <<EOF
# Asset platform -> scanner deployment handoff.
# Generated from ${env_file}.
# Token mode: scanner EC2 role reads these asset-owned SSM SecureString parameters.
DAST_ASSET_PLATFORM_EC2_INSTANCE_ID=${instance_id}
DAST_ASSET_PLATFORM_VPC_ID=${vpc_id}
DAST_ASSET_PLATFORM_BACKEND_SECURITY_GROUP_ID=${backend_sg}
DAST_CALLBACK_URL=${callback_url}
DAST_ASSET_PLATFORM_AWS_REGION=${aws_region}
DAST_ASSET_PLATFORM_AWS_ACCOUNT_ID=${account_id}
DAST_SSM_PARAM_API_TOKEN=${api_param}
DAST_SSM_PARAM_CALLBACK_TOKEN=${callback_param}
DAST_ASSET_PLATFORM_TOKEN_SSM_PARAMETER_ARNS=${api_param_arn},${callback_param_arn}
DAST_ASSET_PLATFORM_TOKEN_KMS_KEY_ARN=${kms_key_arn}

# Scanner EC2 instance profile requirement for strategy A:
# - Allow ssm:GetParameter on DAST_ASSET_PLATFORM_TOKEN_SSM_PARAMETER_ARNS.
# - If DAST_ASSET_PLATFORM_TOKEN_KMS_KEY_ARN is set, also allow kms:Decrypt on that KMS key.
# - The handoff intentionally does not contain token plaintext.
EOF
}

run_export() {
  [[ -f "${ENV_FILE}" ]] || die "env file not found: ${ENV_FILE}"
  if [[ -n "${OUTPUT_FILE}" ]]; then
    write_export_content "${ENV_FILE}" >"${OUTPUT_FILE}"
    chmod 0600 "${OUTPUT_FILE}"
    info "wrote scanner handoff export: ${OUTPUT_FILE}"
  else
    write_export_content "${ENV_FILE}"
  fi
}

plan_update() {
  local key="$1"
  local value="$2"
  [[ -n "${value}" ]] || return 0
  UPDATES+=("${key}=${value}")
}

run_import() {
  [[ -f "${ENV_FILE}" ]] || die "env file not found: ${ENV_FILE}"
  [[ -n "${HANDOFF_FILE}" ]] || die "import requires --handoff-file"
  [[ -f "${HANDOFF_FILE}" ]] || die "handoff file not found: ${HANDOFF_FILE}"

  local base_url ca_param max_targets scanner_sg source_ip source_cidr api_param callback_param use_public_ca
  base_url="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_BASE_URL DAST_PRIVATE_ADVERTISED_ENDPOINT)"
  ca_param="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_CA_BUNDLE_PARAM DAST_SSM_PARAM_CA_BUNDLE)"
  max_targets="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_MAX_TARGETS_PER_BATCH DAST_MAX_TARGETS_PER_BATCH)"
  scanner_sg="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_SCANNER_SECURITY_GROUP_ID DAST_SCANNER_SECURITY_GROUP_ID)"
  source_ip="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_SCANNER_SOURCE_IP DAST_SCANNER_SOURCE_IP DAST_EC2_EIP_ADDRESS)"
  source_cidr="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR)"
  api_param="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_API_TOKEN_PARAM DAST_SSM_PARAM_API_TOKEN)"
  callback_param="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_CALLBACK_TOKEN_PARAM DAST_SSM_PARAM_CALLBACK_TOKEN)"
  use_public_ca="$(first_handoff_value "${HANDOFF_FILE}" SC_DAST_USE_PUBLIC_CA DAST_USE_PUBLIC_CA)"

  [[ -n "${base_url}" ]] || die "handoff must include SC_DAST_BASE_URL or DAST_PRIVATE_ADVERTISED_ENDPOINT"
  validate_https_origin "SC_DAST_BASE_URL" "${base_url}"
  [[ -z "${max_targets}" ]] || validate_positive_integer "SC_DAST_MAX_TARGETS_PER_BATCH" "${max_targets}"
  validate_sg_id "SC_DAST_SCANNER_SECURITY_GROUP_ID" "${scanner_sg}"
  validate_ip "SC_DAST_SCANNER_SOURCE_IP" "${source_ip}"
  validate_cidr "SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR" "${source_cidr}"
  validate_ssm_param "SC_DAST_CA_BUNDLE_PARAM" "${ca_param}"
  validate_optional_bool "SC_DAST_USE_PUBLIC_CA" "${use_public_ca}"

  if [[ -n "${api_param}" || -n "${callback_param}" ]]; then
    validate_ssm_param "SC_DAST_API_TOKEN_PARAM" "${api_param}"
    validate_ssm_param "SC_DAST_CALLBACK_TOKEN_PARAM" "${callback_param}"
    [[ "${ALLOW_TOKEN_PARAM_UPDATE}" == "true" ]] || die "handoff includes token parameter names; rerun with --allow-token-param-update if this is intentional"
    [[ "${api_param}" != "${callback_param}" ]] || die "scanner API and callback token params must be different"
  fi

  UPDATES=()
  plan_update SC_DAST_ENABLED true
  plan_update SC_DAST_BASE_URL "${base_url}"
  plan_update SC_DAST_CA_BUNDLE_PARAM "${ca_param}"
  plan_update SC_DAST_MAX_TARGETS_PER_BATCH "${max_targets}"
  plan_update SC_DAST_SCANNER_SECURITY_GROUP_ID "${scanner_sg}"
  plan_update SC_DAST_SCANNER_SOURCE_IP "${source_ip}"
  plan_update SC_DAST_ASSET_CALLBACK_INGRESS_SOURCE_CIDR "${source_cidr}"
  plan_update SC_DAST_USE_PUBLIC_CA "${use_public_ca}"
  if [[ "${ALLOW_TOKEN_PARAM_UPDATE}" == "true" ]]; then
    plan_update SC_DAST_API_TOKEN_PARAM "${api_param}"
    plan_update SC_DAST_CALLBACK_TOKEN_PARAM "${callback_param}"
  fi

  info "scanner handoff import plan:"
  local item key value current
  for item in "${UPDATES[@]}"; do
    key="${item%%=*}"
    value="${item#*=}"
    current="$(trim "$(get_env_value "${ENV_FILE}" "${key}")")"
    if [[ "${current}" == "${value}" ]]; then
      info "  - ${key}: unchanged"
    else
      info "  - ${key}: ${current:-<blank>} -> ${value}"
    fi
  done

  if [[ "${APPLY}" != "true" ]]; then
    info "dry-run only. Rerun with --apply to update ${ENV_FILE}."
    return 0
  fi

  for item in "${UPDATES[@]}"; do
    key="${item%%=*}"
    value="${item#*=}"
    set_env_value "${ENV_FILE}" "${key}" "${value}"
  done
  info "updated scanner integration settings in ${ENV_FILE}"
  info "next: run enterprise render/deploy with --resolve-ssm, then enterprise verify"
}

COMMAND="${1:-}"
[[ -n "${COMMAND}" ]] || {
  usage
  exit 1
}
shift || true

ENV_FILE="${DEFAULT_ENV_FILE}"
OUTPUT_FILE=""
HANDOFF_FILE=""
APPLY="false"
ALLOW_TOKEN_PARAM_UPDATE="false"
declare -a UPDATES=()

while (($# > 0)); do
  case "$1" in
    --env-file)
      ENV_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --output-file)
      OUTPUT_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --handoff-file)
      HANDOFF_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --apply)
      APPLY="true"
      shift
      ;;
    --allow-token-param-update)
      ALLOW_TOKEN_PARAM_UPDATE="true"
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      die "unknown argument: $1"
      ;;
  esac
done

case "${COMMAND}" in
  export)
    run_export
    ;;
  import)
    run_import
    ;;
  *)
    die "unknown command: ${COMMAND}"
    ;;
esac
