#!/usr/bin/env bash
set -euo pipefail

INSTALLER_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${ENV_FILE:-$INSTALLER_ROOT/.env}"
ENV_EXAMPLE="$INSTALLER_ROOT/.env.example"
COMPOSE_FILE="$INSTALLER_ROOT/compose/docker-compose.yml"
MANIFEST_FILE="${MANIFEST_FILE:-$INSTALLER_ROOT/PACKAGE-MANIFEST.env}"
declare -a ENV_TEMPLATE_KEYS=()
declare -A ENV_TEMPLATE_KEY_SET=()
declare -A INVOCATION_ENV_OVERRIDES=()
declare -A REUSED_ENV_KEYS=()
declare -A ENV_PARSED_VALUES=()

log() {
  printf '[ours-rp-installer] %s\n' "$*"
}

warn() {
  printf '[ours-rp-installer][WARN] %s\n' "$*" >&2
}

die() {
  printf '[ours-rp-installer][ERROR] %s\n' "$*" >&2
  exit 1
}

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

normalize_arch() {
  case "$1" in
    amd64|x86_64|linux/amd64)
      printf 'amd64\n'
      ;;
    arm64|aarch64|linux/arm64)
      printf 'arm64\n'
      ;;
    *)
      return 1
      ;;
  esac
}

platform_for_arch() {
  printf 'linux/%s\n' "$1"
}

env_file_has_key() {
  local env_path="$1"
  local key="$2"
  [[ -f "$env_path" ]] && grep -Eq "^${key}=" "$env_path"
}

load_env_template_keys() {
  local env_path="$1"
  local line
  local key

  [[ -f "$env_path" ]] || die "missing environment template: $env_path"
  ENV_TEMPLATE_KEYS=()
  ENV_TEMPLATE_KEY_SET=()

  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%$'\r'}"
    [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
    [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]] || die "invalid environment assignment in $env_path: $line"
    key="${BASH_REMATCH[1]}"
    [[ -z "${ENV_TEMPLATE_KEY_SET[$key]+x}" ]] || die "duplicate environment key in $env_path: $key"
    ENV_TEMPLATE_KEYS+=("$key")
    ENV_TEMPLATE_KEY_SET["$key"]=1
  done < "$env_path"

  (( ${#ENV_TEMPLATE_KEYS[@]} > 0 )) || die "no environment assignments found in $env_path"
}

template_has_env_key() {
  local key="$1"
  [[ -n "${ENV_TEMPLATE_KEY_SET[$key]+x}" ]]
}

env_file_assignment_keys() {
  local env_path="$1"
  local line

  [[ -f "$env_path" ]] || die "missing environment file: $env_path"
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%$'\r'}"
    [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]] || continue
    printf '%s\n' "${BASH_REMATCH[1]}"
  done < "$env_path"
}

dotenv_decode_value() {
  local raw_value="$1"
  local quote
  local body
  local decoded=""
  local character
  local next_character
  local index=0

  [[ -n "$raw_value" ]] || {
    printf '\n'
    return 0
  }

  quote="${raw_value:0:1}"
  case "$quote" in
    "'")
      [[ "${raw_value: -1}" == "'" && ${#raw_value} -ge 2 ]] || return 1
      body="${raw_value:1:${#raw_value}-2}"
      while (( index < ${#body} )); do
        character="${body:index:1}"
        if [[ "$character" == "\\" && $((index + 1)) -lt ${#body} ]]; then
          next_character="${body:index + 1:1}"
          case "$next_character" in
            "'")
              decoded+="$next_character"
              ((index += 2))
              continue
              ;;
          esac
        fi
        decoded+="$character"
        ((index += 1))
      done
      ;;
    '"')
      [[ "${raw_value: -1}" == '"' && ${#raw_value} -ge 2 ]] || return 1
      body="${raw_value:1:${#raw_value}-2}"
      while (( index < ${#body} )); do
        character="${body:index:1}"
        if [[ "$character" == '$' && $((index + 1)) -lt ${#body} && "${body:index + 1:1}" == '$' ]]; then
          decoded+='$'
          ((index += 2))
          continue
        fi
        if [[ "$character" == "\\" && $((index + 1)) -lt ${#body} ]]; then
          next_character="${body:index + 1:1}"
          case "$next_character" in
            '"'|\\|'$'|'`')
              decoded+="$next_character"
              ((index += 2))
              continue
              ;;
            n)
              decoded+=$'\n'
              ((index += 2))
              continue
              ;;
            r)
              decoded+=$'\r'
              ((index += 2))
              continue
              ;;
            t)
              decoded+=$'\t'
              ((index += 2))
              continue
              ;;
          esac
        fi
        decoded+="$character"
        ((index += 1))
      done
      ;;
    *)
      decoded="$raw_value"
      ;;
  esac

  printf '%s' "$decoded"
}

parse_env_file() {
  local env_path="$1"
  local line
  local parsed_key
  local raw_value
  local parsed_value

  [[ -f "$env_path" ]] || die "missing environment file: $env_path"
  ENV_PARSED_VALUES=()
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%$'\r'}"
    [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
    [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || return 2
    parsed_key="${BASH_REMATCH[1]}"
    raw_value="${BASH_REMATCH[2]}"
    parsed_value="$(dotenv_decode_value "$raw_value")" || return 2
    ENV_PARSED_VALUES["$parsed_key"]="$parsed_value"
  done < "$env_path"
}

env_file_value() {
  local env_path="$1"
  local key="$2"

  parse_env_file "$env_path"

  [[ -n "${ENV_PARSED_VALUES[$key]+x}" ]] || return 3
  printf '%s' "${ENV_PARSED_VALUES[$key]}"
}

env_file_set_value() {
  local env_path="$1"
  local key="$2"
  local value="$3"
  local tmp_env
  local encoded_value

  [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "environment value for $key contains an unsupported newline"
  encoded_value="$(env_file_encode_value "$value")"
  if ! env_file_has_key "$env_path" "$key"; then
    printf '%s=%s\n' "$key" "$encoded_value" >> "$env_path"
    return 0
  fi
  tmp_env="$(mktemp)"
  ENV_FILE_REPLACEMENT="$key=$encoded_value" awk -v key="$key" '
    BEGIN { done=0 }
    $0 ~ "^" key "=" { print ENVIRON["ENV_FILE_REPLACEMENT"]; done=1; next }
    { print }
    END { if (!done) print ENVIRON["ENV_FILE_REPLACEMENT"] }
  ' "$env_path" > "$tmp_env"
  mv "$tmp_env" "$env_path"
}

env_file_encode_value() {
  local value="$1"
  local escaped="$value"

  [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "environment value contains an unsupported newline"
  escaped="${escaped//\\/\\\\}"
  escaped="${escaped//\"/\\\"}"
  escaped="${escaped//\$/\$\$}"
  printf '"%s"' "$escaped"
}

load_env_file() {
  local env_path="$1"
  local key

  parse_env_file "$env_path" || die "unable to parse environment file: $env_path"
  for key in "${!ENV_PARSED_VALUES[@]}"; do
    printf -v "$key" '%s' "${ENV_PARSED_VALUES[$key]}"
    export "$key"
  done
}

validate_env_file_contract() {
  local env_path="$1"
  local invalid_keys=""
  local key

  [[ -f "$env_path" ]] || die "missing environment file: $env_path"
  (( ${#ENV_TEMPLATE_KEYS[@]} > 0 )) || die "environment template keys were not loaded"

  if ! parse_env_file "$env_path"; then
    die "unable to parse environment file: $env_path"
  fi

  for key in "${ENV_TEMPLATE_KEYS[@]}"; do
    if [[ -z "${ENV_PARSED_VALUES[$key]+x}" || -z "${ENV_PARSED_VALUES[$key]}" ]]; then
      invalid_keys+="$key"$'\n'
    fi
  done

  [[ -z "$invalid_keys" ]] || die "environment contract requires non-empty values in $env_path: ${invalid_keys//$'\n'/,}"
}

capture_invocation_env_overrides() {
  local key

  (( ${#ENV_TEMPLATE_KEYS[@]} > 0 )) || die "environment template keys were not loaded"
  INVOCATION_ENV_OVERRIDES=()
  for key in "${ENV_TEMPLATE_KEYS[@]}"; do
    if [[ -v "$key" ]]; then
      INVOCATION_ENV_OVERRIDES["$key"]="${!key}"
    fi
  done
}

log_env_key_group() {
  local group="$1"
  shift
  local rendered="none"
  if (( $# > 0 )); then
    local IFS=,
    rendered="$*"
  fi
  log "$group=$rendered"
}

env_flag_enabled() {
  case "${1:-0}" in
    1|true|TRUE|yes|YES|on|ON)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

detect_host_arch() {
  local raw_arch
  raw_arch="$(uname -m)"
  normalize_arch "$raw_arch" || die "unsupported host architecture: $raw_arch"
}

load_manifest() {
  MANIFEST_PACKAGE_ARCH_RAW=""
  MANIFEST_PACKAGE_PLATFORM_RAW=""
  if [[ -f "$MANIFEST_FILE" ]]; then
    set -a
    # shellcheck disable=SC1090
    source "$MANIFEST_FILE"
    set +a
    MANIFEST_PACKAGE_ARCH_RAW="${PACKAGE_ARCH:-${package_arch:-}}"
    MANIFEST_PACKAGE_PLATFORM_RAW="${PACKAGE_PLATFORM:-${package_platform:-}}"
  fi
}

effective_package_arch() {
  if [[ -n "${PACKAGE_ARCH:-}" ]]; then
    normalize_arch "$PACKAGE_ARCH" || die "unsupported PACKAGE_ARCH=${PACKAGE_ARCH}"
    return 0
  fi
  if [[ -n "${PACKAGE_PLATFORM:-}" ]]; then
    normalize_arch "$PACKAGE_PLATFORM" || die "unsupported PACKAGE_PLATFORM=${PACKAGE_PLATFORM}"
    return 0
  fi
  if [[ -n "${RPKI_PLATFORM:-}" ]]; then
    normalize_arch "$RPKI_PLATFORM" || die "unsupported RPKI_PLATFORM=${RPKI_PLATFORM}"
    return 0
  fi
  if [[ -n "${METRICS_PLATFORM:-}" ]]; then
    normalize_arch "$METRICS_PLATFORM" || die "unsupported METRICS_PLATFORM=${METRICS_PLATFORM}"
    return 0
  fi
  if [[ -n "${MONITOR_PLATFORM:-}" ]]; then
    normalize_arch "$MONITOR_PLATFORM" || die "unsupported MONITOR_PLATFORM=${MONITOR_PLATFORM}"
    return 0
  fi
  die "unable to determine package architecture from manifest or env"
}

arch_mode() {
  local host_arch package_arch
  host_arch="$(detect_host_arch)"
  package_arch="$(effective_package_arch)"
  if [[ "$host_arch" == "$package_arch" ]]; then
    printf 'native\n'
    return 0
  fi
  if env_flag_enabled "${ALLOW_CROSS_ARCH:-0}"; then
    printf 'cross-arch-enabled\n'
    return 0
  fi
  printf 'mismatch-blocked\n'
}

assert_arch_compatibility() {
  local host_arch package_arch package_platform mode
  host_arch="$(detect_host_arch)"
  package_arch="$(effective_package_arch)"
  package_platform="${PACKAGE_PLATFORM:-${RPKI_PLATFORM:-$(platform_for_arch "$package_arch")}}"
  mode="$(arch_mode)"
  case "$mode" in
    native)
      return 0
      ;;
    cross-arch-enabled)
      log "cross-arch execution explicitly enabled: host_arch=$host_arch package_arch=$package_arch package_platform=$package_platform"
      return 0
      ;;
    mismatch-blocked)
      die "package architecture mismatch: host_arch=$host_arch package_arch=$package_arch package_platform=$package_platform. Set ALLOW_CROSS_ARCH=1 in $ENV_FILE to allow explicit QEMU/binfmt emulation."
      ;;
    *)
      die "unknown arch compatibility mode: $mode"
      ;;
  esac
}

load_env() {
  load_manifest
  [[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE"
  load_env_template_keys "$ENV_EXAMPLE"
  validate_env_file_contract "$ENV_EXAMPLE"
  if [[ ! -f "$ENV_FILE" ]]; then
    cp "$ENV_EXAMPLE" "$ENV_FILE"
    log "created .env from .env.example"
  fi
  validate_env_file_contract "$ENV_FILE"
  load_env_file "$ENV_FILE"

  if [[ -n "${MANIFEST_PACKAGE_ARCH_RAW:-}" ]]; then
    manifest_package_arch="$(normalize_arch "$MANIFEST_PACKAGE_ARCH_RAW")" || die "unsupported manifest package arch: $MANIFEST_PACKAGE_ARCH_RAW"
    if [[ -n "${PACKAGE_ARCH:-}" ]]; then
      env_package_arch="$(normalize_arch "$PACKAGE_ARCH")" || die "unsupported env package arch: $PACKAGE_ARCH"
      [[ "$env_package_arch" == "$manifest_package_arch" ]] || die "env PACKAGE_ARCH=$env_package_arch mismatches manifest PACKAGE_ARCH=$manifest_package_arch"
    fi
    PACKAGE_ARCH="$manifest_package_arch"
  else
    if [[ -z "${PACKAGE_ARCH:-}" ]]; then
      PACKAGE_ARCH="$(effective_package_arch)"
    else
      PACKAGE_ARCH="$(normalize_arch "$PACKAGE_ARCH")"
    fi
  fi
  if [[ -n "${MANIFEST_PACKAGE_PLATFORM_RAW:-}" ]]; then
    manifest_package_platform="$MANIFEST_PACKAGE_PLATFORM_RAW"
    if [[ -n "${PACKAGE_PLATFORM:-}" && "$PACKAGE_PLATFORM" != "$manifest_package_platform" ]]; then
      die "env PACKAGE_PLATFORM=$PACKAGE_PLATFORM mismatches manifest PACKAGE_PLATFORM=$manifest_package_platform"
    fi
    PACKAGE_PLATFORM="$manifest_package_platform"
  else
    PACKAGE_PLATFORM="${PACKAGE_PLATFORM:-$(platform_for_arch "$PACKAGE_ARCH")}"
  fi

}

compose_cmd() {
  docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" "$@"
}

validate_rtr_report_dir() {
  load_env
  [[ -n "${RTR_REPORT_DIR:-}" ]] || return 0
  [[ "$RTR_REPORT_DIR" = /* ]] || die "RTR_REPORT_DIR must be an absolute host path: $RTR_REPORT_DIR"
  [[ -d "$RTR_REPORT_DIR" ]] || die "RTR_REPORT_DIR does not exist or is not a directory: $RTR_REPORT_DIR"
}

validate_rtr_registration_dir() {
  local report_dir="$1"

  [[ -n "$report_dir" ]] || die "--report-dir is required"
  [[ "$report_dir" = /* ]] || die "--report-dir must be an absolute host path: $report_dir"
  [[ "$report_dir" =~ ^/[A-Za-z0-9._/@:+,-]+$ ]] || die "--report-dir contains unsupported characters: $report_dir"
  [[ -d "$report_dir" ]] || die "--report-dir does not exist: $report_dir"
  [[ -r "$report_dir" ]] || die "--report-dir is not readable: $report_dir"
}

rtr_report_file_count() {
  local report_dir="$1"
  {
    find "$report_dir" -maxdepth 1 -type f \( \
      -name 'rtr-source-*.json' -o \
      -name 'rtr-runtime-*.json' -o \
      -name 'rtr-clients-*.json' \
    \) -print 2>/dev/null || true
  } | wc -l | tr -d '[:space:]'
}

latest_rtr_report_file() {
  local report_dir="$1"
  {
    find "$report_dir" -maxdepth 1 -type f \( \
      -name 'rtr-source-*.json' -o \
      -name 'rtr-runtime-*.json' -o \
      -name 'rtr-clients-*.json' \
    \) -printf '%T@ %p\n' 2>/dev/null || true
  } | sort -nr | awk 'NR == 1 { sub(/^[^ ]+ /, ""); print }'
}

replace_env_key_file() {
  local env_path="$1"
  local key="$2"
  local value="$3"
  local temp_path

  [[ "$key" =~ ^[A-Z][A-Z0-9_]*$ ]] || die "invalid environment key: $key"
  [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "environment value must be single-line"

  temp_path="$(mktemp "${env_path}.tmp.XXXXXX")"
  awk -v key="$key" -v value="$value" '
    BEGIN { replaced = 0 }
    $0 ~ "^" key "=" {
      print key "=" value
      replaced = 1
      next
    }
    { print }
    END {
      if (!replaced) {
        print key "=" value
      }
    }
  ' "$env_path" > "$temp_path"
  chmod --reference="$env_path" "$temp_path" 2>/dev/null || true
  mv "$temp_path" "$env_path"
}

wait_for_endpoint() {
  local url="$1"
  local timeout_secs="$2"
  local elapsed=0

  while (( elapsed < timeout_secs )); do
    if endpoint_ok "$url"; then
      return 0
    fi
    sleep 2
    elapsed=$((elapsed + 2))
  done

  return 1
}

create_data_dirs() {
  load_env
  mkdir -p \
    "$HOST_DATA_DIR/state" \
    "$HOST_DATA_DIR/runs" \
    "$HOST_DATA_DIR/logs" \
    "$HOST_DATA_DIR/tmp" \
    "$HOST_DATA_DIR/empty-rtr-report" \
    "$HOST_DATA_DIR/prometheus" \
    "$HOST_DATA_DIR/grafana"
  validate_rtr_report_dir
  chmod 755 "$HOST_DATA_DIR" "$HOST_DATA_DIR/state" "$HOST_DATA_DIR/runs" "$HOST_DATA_DIR/logs" "$HOST_DATA_DIR/tmp" || true
  chmod 777 "$HOST_DATA_DIR/prometheus" "$HOST_DATA_DIR/grafana" || true
}

latest_run_dir() {
  load_env
  find "$HOST_DATA_DIR/runs" -maxdepth 1 -mindepth 1 -type d -name 'run_*' 2>/dev/null | sort | tail -1
}

latest_success_run_dir() {
  load_env
  find "$HOST_DATA_DIR/runs" -maxdepth 2 -type f -path '*/run-summary.json' 2>/dev/null \
    | while read -r summary; do
        if jq -e '.status == "success"' "$summary" >/dev/null 2>&1; then
          dirname "$summary"
        fi
      done | sort | tail -1
}

has_success_run() {
  [[ -n "$(latest_success_run_dir)" ]]
}

print_run_summary() {
  local run_dir="$1"
  local summary="$run_dir/run-summary.json"
  local meta="$run_dir/run-meta.json"
  local timing="$run_dir/stage-timing.json"
  local process_time="$run_dir/process-time.txt"
  local vrps_file="$run_dir/vrps.csv"
  local vaps_file="$run_dir/vaps.csv"
  local status="unknown"
  local sync_mode="unknown"
  local wall_ms="null"
  local validation_ms="null"
  local repo_sync_ms="null"
  local max_rss_kb="null"
  local publication_points="null"
  local vrps="null"
  local vaps="null"
  local warnings="null"
  [[ -f "$summary" ]] || {
    warn "missing run-summary.json in $run_dir"
    return 1
  }
  status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)"
  wall_ms="$(jq -r '.wallMs // .wall_ms // "null"' "$summary" 2>/dev/null || echo null)"
  warnings="$(jq -r '.warningCount // .warnings // "null"' "$summary" 2>/dev/null || echo null)"
  if [[ -f "$meta" ]]; then
    sync_mode="$(jq -r '.sync_mode // .syncMode // "unknown"' "$meta" 2>/dev/null || echo unknown)"
    status="$(jq -r --arg fallback "$status" '.status // $fallback' "$meta" 2>/dev/null || echo "$status")"
  fi
  if [[ -f "$timing" ]]; then
    validation_ms="$(jq -r '.validation_ms // "null"' "$timing" 2>/dev/null || echo null)"
    repo_sync_ms="$(jq -r '.repo_sync_ms_total // "null"' "$timing" 2>/dev/null || echo null)"
    publication_points="$(jq -r '.publication_points // "null"' "$timing" 2>/dev/null || echo null)"
  fi
  if [[ -f "$process_time" ]]; then
    max_rss_kb="$(awk -F': ' '/Maximum resident set size/ {print $2; found=1} END {if (!found) print "null"}' "$process_time")"
  fi
  if [[ -f "$vrps_file" ]]; then
    vrps="$(( $(wc -l < "$vrps_file") > 0 ? $(wc -l < "$vrps_file") - 1 : 0 ))"
  fi
  if [[ -f "$vaps_file" ]]; then
    vaps="$(( $(wc -l < "$vaps_file") > 0 ? $(wc -l < "$vaps_file") - 1 : 0 ))"
  fi
  jq -n \
    --arg run "$(basename "$run_dir")" \
    --arg status "$status" \
    --arg syncMode "$sync_mode" \
    --argjson wallMs "$wall_ms" \
    --argjson validationMs "$validation_ms" \
    --argjson repoSyncMs "$repo_sync_ms" \
    --argjson maxRssKb "$max_rss_kb" \
    --argjson vrps "$vrps" \
    --argjson vaps "$vaps" \
    --argjson publicationPoints "$publication_points" \
    --argjson warnings "$warnings" \
    '{run:$run,status:$status,syncMode:$syncMode,wallMs:$wallMs,validationMs:$validationMs,repoSyncMs:$repoSyncMs,maxRssKb:$maxRssKb,vrps:$vrps,vaps:$vaps,publicationPoints:$publicationPoints,warnings:$warnings}'
}

wait_for_new_success_run() {
  local before_latest="$1"
  local timeout_secs="$2"
  local start_epoch now run_dir summary meta status meta_status
  start_epoch="$(date +%s)"
  while true; do
    run_dir="$(latest_run_dir || true)"
    if [[ -n "$run_dir" && "$run_dir" != "$before_latest" ]]; then
      summary="$run_dir/run-summary.json"
      meta="$run_dir/run-meta.json"
      if [[ -f "$summary" ]]; then
        status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)"
        if [[ "$status" == "success" ]]; then
          meta_status="unknown"
          if [[ -f "$meta" ]]; then
            meta_status="$(jq -r '.status // "unknown"' "$meta" 2>/dev/null || echo unknown)"
          fi
          if [[ "$meta_status" == "success" ]]; then
            print_run_summary "$run_dir" || true
            return 0
          fi
        fi
        if [[ "$status" == "failed" || "$status" == "error" ]]; then
          print_run_summary "$run_dir" || true
          die "run failed: $run_dir"
        fi
      fi
    fi
    now="$(date +%s)"
    if (( now - start_epoch > timeout_secs )); then
      die "timed out waiting for first successful run after ${timeout_secs}s"
    fi
    sleep 10
  done
}

docker_compose_available() {
  docker compose version >/dev/null 2>&1
}

install_docker_if_missing() {
  if command -v docker >/dev/null 2>&1 && docker_compose_available && command -v jq >/dev/null 2>&1 && command -v rsync >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
    log "docker and docker compose are already installed"
    return 0
  fi
  if [[ "${SKIP_DEP_INSTALL:-0}" == "1" ]]; then
    die "docker/docker compose missing and SKIP_DEP_INSTALL=1"
  fi
  if ! command -v apt-get >/dev/null 2>&1; then
    die "docker/docker compose missing; automatic install currently supports apt-get only"
  fi
  log "installing missing runtime packages via apt"
  apt-get update
  DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl jq rsync gzip tar docker.io
  if ! docker_compose_available; then
    if apt-cache show docker-compose-v2 >/dev/null 2>&1; then
      DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-v2
    elif apt-cache show docker-compose-plugin >/dev/null 2>&1; then
      DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin
    elif apt-cache show docker-compose >/dev/null 2>&1; then
      DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose
    fi
  fi
  systemctl enable --now docker >/dev/null 2>&1 || true
  docker_compose_available || die "docker compose is still unavailable after install"
}

load_installer_images() {
  load_env
  require_cmd docker
  shopt -s nullglob
  local image load_output desired_tag
  local found=0
  for image in "$INSTALLER_ROOT"/images/*.tar "$INSTALLER_ROOT"/images/*.tar.gz; do
    found=1
    log "loading docker image: $image"
    if [[ "$image" == *.gz ]]; then
      load_output="$(gzip -dc "$image" | docker load)"
    else
      load_output="$(docker load -i "$image")"
    fi
    printf '%s\n' "$load_output"
    desired_tag=""
    case "$(basename "$image")" in
      "${image_tar:-}")
        desired_tag="${image_tag:-$RPKI_IMAGE}"
        ;;
      "${metrics_image_tar:-}")
        desired_tag="${metrics_image:-$METRICS_IMAGE}"
        ;;
      "${prometheus_image_tar:-}")
        desired_tag="${prometheus_image:-$PROMETHEUS_IMAGE}"
        ;;
      "${grafana_image_tar:-}")
        desired_tag="${grafana_image:-$GRAFANA_IMAGE}"
        ;;
    esac
    if [[ -n "$desired_tag" ]]; then
      ensure_loaded_image_tag "$desired_tag" "$load_output" "$image"
    fi
  done
  shopt -u nullglob
  (( found == 1 )) || warn "no image tar found under $INSTALLER_ROOT/images"
}

ensure_loaded_image_tag() {
  local desired_tag="$1"
  local load_output="$2"
  local image_path="$3"
  local loaded_ref=""
  local loaded_id=""
  local desired_id=""
  loaded_ref="$(printf '%s\n' "$load_output" | awk -F'Loaded image: ' '/Loaded image: / {print $2; exit}')"
  if [[ -z "$loaded_ref" ]]; then
    loaded_ref="$(printf '%s\n' "$load_output" | awk -F'Loaded image ID: ' '/Loaded image ID: / {print $2; exit}')"
  fi
  [[ -n "$loaded_ref" ]] || die "unable to determine loaded image reference for $image_path"
  loaded_id="$(docker image inspect --format '{{.Id}}' "$loaded_ref" 2>/dev/null || true)"
  desired_id="$(docker image inspect --format '{{.Id}}' "$desired_tag" 2>/dev/null || true)"
  if [[ -n "$loaded_id" && -n "$desired_id" && "$loaded_id" == "$desired_id" ]]; then
    return 0
  fi
  log "tagging loaded image for package use: source=$loaded_ref target=$desired_tag"
  docker tag "$loaded_ref" "$desired_tag"
}

ensure_binfmt_if_needed() {
  require_cmd docker
  load_env
  local host_arch package_arch
  host_arch="$(detect_host_arch)"
  package_arch="$(effective_package_arch)"
  if [[ "$host_arch" == "$package_arch" ]]; then
    return 0
  fi
  assert_arch_compatibility
  log "host arch is $host_arch; enabling binfmt/qemu for package arch $package_arch"
  docker run --rm --privileged tonistiigi/binfmt --install "$package_arch"
}

verify_runtime_image() {
  load_env
  require_cmd docker
  log "verifying runtime image $RPKI_IMAGE on $RPKI_PLATFORM"
  verify_image_platform "$RPKI_IMAGE" "$RPKI_PLATFORM" "runtime"
  verify_image_usage_output "$RPKI_IMAGE" "$RPKI_PLATFORM" "runtime" /opt/ours-rp/bin/rpki --help
}

verify_metrics_image() {
  load_env
  require_cmd docker
  log "verifying metrics image $METRICS_IMAGE on $METRICS_PLATFORM"
  verify_image_platform "$METRICS_IMAGE" "$METRICS_PLATFORM" "metrics"
  verify_image_usage_output "$METRICS_IMAGE" "$METRICS_PLATFORM" "metrics" /opt/ours-rp/bin/rpki_artifact_metrics --help
  verify_metrics_image_rpki_client
}

verify_metrics_image_rpki_client() {
  local output
  local status
  set +e
  output="$(docker run --rm --platform "$METRICS_PLATFORM" "$METRICS_IMAGE" /opt/ours-rp/bin/rpki-client -V 2>&1)"
  status=$?
  set -e
  [[ "$status" == "0" ]] || die "metrics image rpki-client -V failed: image=$METRICS_IMAGE exit=$status output=$output"
  grep -q "rpki-client" <<<"$output" || die "metrics image rpki-client -V unexpected output: $output"
}

verify_image_platform() {
  local image="$1"
  local expected_platform="$2"
  local role="$3"
  local actual_platform
  docker image inspect "$image" >/dev/null
  actual_platform="$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null || echo unknown)"
  [[ "$actual_platform" == "$expected_platform" ]] || die "$role image platform mismatch: image=$image expected=$expected_platform actual=$actual_platform"
}

verify_image_usage_output() {
  local image="$1"
  local expected_platform="$2"
  local role="$3"
  shift 3
  local help_path
  local status
  help_path="$(mktemp "${TMPDIR:-/tmp}/ours-rp-image-help.XXXXXX")"
  set +e
  docker run --rm --platform "$expected_platform" "$image" "$@" >"$help_path" 2>&1
  status=$?
  set -e
  if [[ "$status" != "0" && "$status" != "1" ]]; then
    cat "$help_path" >&2 || true
    rm -f "$help_path"
    die "$role image command failed: image=$image platform=$expected_platform exit=$status"
  fi
  grep -q '^Usage' "$help_path" || {
    cat "$help_path" >&2 || true
    rm -f "$help_path"
    die "$role image did not emit usage output: image=$image platform=$expected_platform exit=$status"
  }
  head -5 "$help_path" || true
  rm -f "$help_path"
}

verify_monitor_images() {
  load_env
  require_cmd docker
  verify_image_platform "$PROMETHEUS_IMAGE" "$MONITOR_PLATFORM" "prometheus"
  verify_image_platform "$GRAFANA_IMAGE" "$MONITOR_PLATFORM" "grafana"
}

endpoint_ok() {
  local url="$1"
  curl -fsS --max-time 5 "$url" >/dev/null 2>&1
}
