#!/usr/bin/env bash
set -Eeuo pipefail
trap 'status=$?; printf "\033[1;31mERROR:\033[0m installer failed near line %s: %s\n" "$LINENO" "$BASH_COMMAND" >&2; exit "$status"' ERR

DEFAULT_KAGEOS_IMAGE="docker.io/qiayanai/kageos:latest"
DEFAULT_KAGEOS_APP_BASE_IMAGE="docker.io/qiayanai/kagebase:latest"
CN_IMAGE_PREFIX="m.daocloud.io/docker.io"
CN_ALIYUN_REGISTRY="crpi-pp1889gb5d5betoy.cn-beijing.personal.cr.aliyuncs.com"
CN_ALIYUN_NAMESPACE="qiayanai"
CN_ALIYUN_IMAGE="${KAGEOS_CN_ALIYUN_IMAGE:-${CN_ALIYUN_REGISTRY}/${CN_ALIYUN_NAMESPACE}/kageos:latest}"
CN_ALIYUN_APP_BASE_IMAGE="${KAGEOS_CN_ALIYUN_APP_BASE_IMAGE:-${CN_ALIYUN_REGISTRY}/${CN_ALIYUN_NAMESPACE}/kagebase:latest}"
CN_TENCENT_IMAGE="${KAGEOS_CN_TENCENT_IMAGE:-}"
DEFAULT_KAGEOS_RELEASE_BASE_URL="https://downloads.kageos.ai/releases"
CN_KAGEOS_RELEASE_BASE_URL="https://downloads.kageos.com/releases"
DEFAULT_DISTRIBUTION_BASE_URL="https://kageos.ai"
CN_DISTRIBUTION_BASE_URL="https://kageos.com"

KAGEOS_IMAGE="${KAGEOS_IMAGE:-$DEFAULT_KAGEOS_IMAGE}"
KAGEOS_IMAGE_FALLBACK="${KAGEOS_IMAGE_FALLBACK:-}"
KAGEOS_IMAGE_FALLBACKS="${KAGEOS_IMAGE_FALLBACKS:-}"
KAGEOS_RELEASE_BASE_URL="${KAGEOS_RELEASE_BASE_URL:-}"
KAGEOS_RELEASE_VERSION="${KAGEOS_RELEASE_VERSION:-latest}"
KAGEOS_CN_TARBALL="${KAGEOS_CN_TARBALL:-0}"
KAGEOS_INSTALL_DIR="${KAGEOS_INSTALL_DIR:-/opt/kageos}"
KAGEOS_DATA_DIR="${KAGEOS_DATA_DIR:-}"
KAGEOS_CONTAINER_NAME="${KAGEOS_CONTAINER_NAME:-}"
KAGEOS_CONTAINER_ENGINE="${KAGEOS_CONTAINER_ENGINE:-auto}"
KAGEOS_INSTALL_DEPS="${KAGEOS_INSTALL_DEPS:-auto}"
KAGEOS_START_TIMEOUT="${KAGEOS_START_TIMEOUT:-1200}"
KAGEOS_AIO_PRINT_SECRETS="${KAGEOS_AIO_PRINT_SECRETS:-1}"
KAGEOS_APP_BASE_IMAGE="${KAGEOS_APP_BASE_IMAGE:-}"
KAGEOS_APP_BASE_PULL="${KAGEOS_APP_BASE_PULL:-}"
KAGEOS_APP_BASE_PULL_FALLBACK_BUILD="${KAGEOS_APP_BASE_PULL_FALLBACK_BUILD:-}"
KAGEOS_AIO_MYSQL_IMAGE="${KAGEOS_AIO_MYSQL_IMAGE:-}"
KAGEOS_AIO_NATS_IMAGE="${KAGEOS_AIO_NATS_IMAGE:-}"
KAGEOS_AIO_MINIO_IMAGE="${KAGEOS_AIO_MINIO_IMAGE:-}"
KAGEOS_AIO_RECREATE_INFRA="${KAGEOS_AIO_RECREATE_INFRA:-}"
KAGEOS_GOPROXY="${KAGEOS_GOPROXY:-}"
KAGEOS_GOSUMDB="${KAGEOS_GOSUMDB:-}"
KAGEOS_FIX_FORWARD_POLICY="${KAGEOS_FIX_FORWARD_POLICY:-auto}"
KAGEOS_CONFIG_DIR="${KAGEOS_CONFIG_DIR:-/etc/kageos}"
KAGEOS_DISTRIBUTION_BASE_URL="${KAGEOS_DISTRIBUTION_BASE_URL:-}"
KAGEOS_MANAGER_URL="${KAGEOS_MANAGER_URL:-}"
KAGEOS_MANAGER_SOURCE="${KAGEOS_MANAGER_SOURCE:-}"
KAGEOS_ENV_FILE="${KAGEOS_ENV_FILE:-}"
WEB_ASSETS_MODE="${KAGEOS_WEB_ASSETS_MODE:-embedded}"
WEB_S3_ENDPOINT="${KAGEOS_WEB_S3_ENDPOINT:-}"
WEB_S3_BUCKET="${KAGEOS_WEB_S3_BUCKET:-}"
WEB_CDN_URL="${KAGEOS_WEB_CDN_URL:-}"
WEB_S3_PREFIX="${KAGEOS_WEB_S3_PREFIX:-kageos/web}"
WEB_S3_REGION="${KAGEOS_WEB_S3_REGION:-}"
WEB_S3_ACCESS_KEY_ID="${KAGEOS_WEB_S3_ACCESS_KEY_ID:-}"
WEB_S3_SECRET_ACCESS_KEY="${KAGEOS_WEB_S3_SECRET_ACCESS_KEY:-}"
WEB_S3_SESSION_TOKEN="${KAGEOS_WEB_S3_SESSION_TOKEN:-}"
WEB_ASSET_BASE_URL=""
INSTANCE_ID="${KAGEOS_INSTANCE_ID:-}"
CONTAINER_HTTP_PORT=10001

BASE_URL="${KAGEOS_BASE_URL:-}"
PUBLIC_IP="${KAGEOS_PUBLIC_IP:-}"
TLS_MODE="${KAGEOS_TLS_MODE:-auto}"
HTTP_PORT="${KAGEOS_HTTP_PORT:-}"
HTTPS_PORT="${KAGEOS_HTTPS_PORT:-}"
TIMEZONE="${KAGEOS_TIMEZONE:-Asia/Shanghai}"
DEPLOY_USER="${KAGEOS_DEPLOY_USER:-}"
SKIP_PULL=0
DRY_RUN=0
USE_CN_MIRROR="${KAGEOS_CN:-0}"
USER_SET_IMAGE=0
HOST_FORWARD_POLICY=""
HOST_FORWARD_RULES_ADDED=0

usage() {
  cat <<'EOF'
kageos production one-command installer

Usage:
  curl -fsSL https://kageos.ai/install-prod.sh | sudo bash
  curl -fsSL https://kageos.com/install-prod.sh | sudo bash -s -- --cn

What it does:
  - Installs missing host basics and a container engine where possible.
  - Pulls the published kageos image; it does not clone the source repository.
  - Uses mainland China registries first, then public image fallbacks, when --cn is passed.
  - Creates the next isolated instance (instance-01, instance-02, ...).
  - Uses a fixed container port (10001) and the first free host port from 10001.
  - Installs the /usr/local/bin/kageos multi-instance manager.

Options:
  --base-url URL             Public URL for this instance. Defaults to the detected public IP.
  --instance INSTANCE        Explicit instance id. Normally generated automatically.
  --cn                       Use mainland China registry and public image fallbacks.
  --tls-mode MODE            auto, http, https, redirect, or external. Defaults to auto.
  --http-port PORT           Host HTTP port. Defaults to the first free port from 10001.
  --https-port PORT          HTTPS listen port. Defaults to the URL port or 443.
  --timezone TZ              Deployment timezone. Defaults to Asia/Shanghai.
  --install-dir DIR          Runtime directory. Defaults to /opt/kageos.
  --data-dir DIR             Persistent data directory. Defaults to <install-dir>/instances/<id>/data.
  --env-file FILE            Optional root-owned runtime environment file.
  --web-assets MODE          embedded (default) or s3. Use s3 only for high-traffic deployments.
  --web-s3-endpoint URL      S3-compatible upload endpoint. Required with --web-assets s3.
  --web-s3-bucket BUCKET     Existing bucket for immutable web assets.
  --web-cdn-url URL          Public CDN URL mapped to that bucket.
  --image IMAGE              Container image. Defaults to docker.io/qiayanai/kageos:latest.
  --container-name NAME      Container name. Defaults to kageos-<instance-id>.
  --container-engine ENGINE  auto, podman, or docker. Defaults to auto.
  --skip-deps                Do not install missing system dependencies.
  --skip-pull                Do not pull the image before starting.
  --no-fix-forward-policy    Do not add scoped host FORWARD rules when needed.
  --dry-run                  Print the resolved plan without changing the host.
  --help                     Show this help.

Examples:
  # Mainland China: automatically detect the public IP and use the domestic path.
  curl -fsSL https://kageos.com/install-prod.sh | sudo bash -s -- --cn

  # Global: automatically detect the public IP.
  curl -fsSL https://kageos.ai/install-prod.sh | sudo bash

  # Public domain with HTTPS bootstrap using a self-signed certificate.
  curl -fsSL https://kageos.ai/install-prod.sh | sudo bash -s -- --base-url https://app.example.com

  # Fast HTTP trial by server IP.
  curl -fsSL https://kageos.ai/install-prod.sh | sudo bash -s -- --base-url http://your-server-ip

  # Mainland China server, using optimized registry and public image fallbacks.
  curl -fsSL https://kageos.com/install-prod.sh | sudo bash -s -- --base-url http://your-server-ip --cn

Environment:
  KAGEOS_BASE_URL, KAGEOS_PUBLIC_IP, KAGEOS_TLS_MODE, KAGEOS_TIMEZONE, KAGEOS_HTTP_PORT,
  KAGEOS_HTTPS_PORT, KAGEOS_IMAGE, KAGEOS_INSTALL_DIR, KAGEOS_DATA_DIR,
  KAGEOS_ENV_FILE, KAGEOS_CONTAINER_ENGINE, KAGEOS_START_TIMEOUT, KAGEOS_RELEASE_BASE_URL,
  KAGEOS_DISTRIBUTION_BASE_URL, KAGEOS_MANAGER_URL,
  KAGEOS_RELEASE_VERSION, and KAGEOS_CN_TARBALL are supported. Set
  KAGEOS_CN_TARBALL=1 to opt in to release archive fallback.
  KAGEOS_APP_BASE_IMAGE, KAGEOS_APP_BASE_PULL, KAGEOS_AIO_MYSQL_IMAGE,
  KAGEOS_AIO_NATS_IMAGE, and KAGEOS_AIO_MINIO_IMAGE are forwarded into the
  all-in-one container when set.
  KAGEOS_GOPROXY and KAGEOS_GOSUMDB override the Go module download settings
  used when kageos builds workspace apps.
  KAGEOS_FIX_FORWARD_POLICY controls host FORWARD handling: auto, always, or never.
  Advanced S3/CDN mode also supports KAGEOS_WEB_ASSETS_MODE,
  KAGEOS_WEB_S3_ENDPOINT, KAGEOS_WEB_S3_BUCKET, KAGEOS_WEB_CDN_URL,
  KAGEOS_WEB_S3_PREFIX, KAGEOS_WEB_S3_REGION, KAGEOS_WEB_S3_ACCESS_KEY_ID,
  KAGEOS_WEB_S3_SECRET_ACCESS_KEY, and KAGEOS_WEB_S3_SESSION_TOKEN.
EOF
}

log() {
  printf '\033[1;36m==>\033[0m %s\n' "$*"
}

warn() {
  printf '\033[1;33mWARN:\033[0m %s\n' "$*" >&2
}

die() {
  printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2
  exit 1
}

is_ip_address() {
  local value="$1"
  [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ || "$value" == *:* ]]
}

detect_public_ip() {
  local detected="${PUBLIC_IP}"
  local endpoint

  if [[ -z "$detected" ]]; then
    for endpoint in \
      "https://4.ipw.cn" \
      "https://api.ipify.org" \
      "https://ifconfig.me/ip"; do
      detected="$(curl -4fsS --connect-timeout 3 --max-time 6 "$endpoint" 2>/dev/null || true)"
      detected="${detected//$'\r'/}"
      detected="${detected//$'\n'/}"
      if is_ip_address "$detected"; then
        break
      fi
      detected=""
    done
  fi

  if [[ -z "$detected" && -r /dev/tty && -w /dev/tty ]]; then
    printf 'Could not detect the public IP automatically. Enter the server IP: ' >/dev/tty
    IFS= read -r detected </dev/tty || true
  fi

  is_ip_address "$detected" || die "could not detect a public IP; rerun with --base-url http://your-server-ip"
  printf '%s\n' "$detected"
}

resolve_base_url() {
  [[ -z "$BASE_URL" ]] || return 0
  PUBLIC_IP="$(detect_public_ip)"
  BASE_URL="http://${PUBLIC_IP}"
  log "No --base-url provided; using detected server IP ${PUBLIC_IP}"
}

acquire_install_lock() {
  command -v flock >/dev/null 2>&1 || die "flock is required to serialize instance installation"
  mkdir -p "$KAGEOS_CONFIG_DIR"
  chmod 0700 "$KAGEOS_CONFIG_DIR"
  exec 9>"${KAGEOS_CONFIG_DIR%/}/install.lock"
  flock -n 9 || die "another kageos installation is already running"
}

need_value() {
  local option="$1"
  shift
  [[ $# -gt 0 && -n "${1:-}" ]] || die "$option requires a value"
}

validate_port() {
  local name="$1"
  local value="$2"
  if [[ ! "$value" =~ ^[0-9]+$ || "$value" -lt 1 || "$value" -gt 65535 ]]; then
    die "$name must be a TCP port between 1 and 65535"
  fi
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --base-url)
      shift
      need_value "--base-url" "$@"
      BASE_URL="$1"
      ;;
    --instance)
      shift
      need_value "--instance" "$@"
      INSTANCE_ID="$1"
      ;;
    --cn|--china|--mainland-china)
      USE_CN_MIRROR=1
      ;;
    --tls-mode)
      shift
      need_value "--tls-mode" "$@"
      TLS_MODE="$1"
      ;;
    --http-port)
      shift
      need_value "--http-port" "$@"
      validate_port "--http-port" "$1"
      HTTP_PORT="$1"
      ;;
    --https-port)
      shift
      need_value "--https-port" "$@"
      validate_port "--https-port" "$1"
      HTTPS_PORT="$1"
      ;;
    --timezone)
      shift
      need_value "--timezone" "$@"
      TIMEZONE="$1"
      ;;
    --user)
      shift
      need_value "--user" "$@"
      DEPLOY_USER="$1"
      ;;
    --install-dir)
      shift
      need_value "--install-dir" "$@"
      KAGEOS_INSTALL_DIR="$1"
      ;;
    --data-dir)
      shift
      need_value "--data-dir" "$@"
      KAGEOS_DATA_DIR="$1"
      ;;
    --env-file)
      shift
      need_value "--env-file" "$@"
      KAGEOS_ENV_FILE="$1"
      ;;
    --web-assets)
      shift
      need_value "--web-assets" "$@"
      WEB_ASSETS_MODE="$1"
      ;;
    --web-s3-endpoint)
      shift
      need_value "--web-s3-endpoint" "$@"
      WEB_S3_ENDPOINT="$1"
      ;;
    --web-s3-bucket)
      shift
      need_value "--web-s3-bucket" "$@"
      WEB_S3_BUCKET="$1"
      ;;
    --web-cdn-url)
      shift
      need_value "--web-cdn-url" "$@"
      WEB_CDN_URL="$1"
      ;;
    --image)
      shift
      need_value "--image" "$@"
      KAGEOS_IMAGE="$1"
      USER_SET_IMAGE=1
      ;;
    --container-name)
      shift
      need_value "--container-name" "$@"
      KAGEOS_CONTAINER_NAME="$1"
      ;;
    --container-engine)
      shift
      need_value "--container-engine" "$@"
      KAGEOS_CONTAINER_ENGINE="$1"
      ;;
    --skip-deps)
      KAGEOS_INSTALL_DEPS="never"
      ;;
    --skip-pull)
      SKIP_PULL=1
      ;;
    --fix-forward-policy)
      KAGEOS_FIX_FORWARD_POLICY="always"
      ;;
    --no-fix-forward-policy)
      KAGEOS_FIX_FORWARD_POLICY="never"
      ;;
    --dry-run)
      DRY_RUN=1
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      die "unsupported argument: $1"
      ;;
  esac
  shift
done

case "$KAGEOS_CONTAINER_ENGINE" in
  auto|podman|docker) ;;
  *) die "--container-engine must be auto, podman, or docker" ;;
esac

case "$KAGEOS_FIX_FORWARD_POLICY" in
  auto|always|never) ;;
  *) die "KAGEOS_FIX_FORWARD_POLICY must be auto, always, or never" ;;
esac

resolve_web_assets_config() {
  WEB_ASSETS_MODE="$(printf '%s' "$WEB_ASSETS_MODE" | tr '[:upper:]' '[:lower:]')"
  case "$WEB_ASSETS_MODE" in
    embedded) return 0 ;;
    s3) ;;
    *) die "--web-assets must be embedded or s3" ;;
  esac

  [[ -n "$WEB_S3_ENDPOINT" ]] || die "--web-s3-endpoint is required with --web-assets s3"
  [[ -n "$WEB_S3_BUCKET" ]] || die "--web-s3-bucket is required with --web-assets s3"
  [[ -n "$WEB_CDN_URL" ]] || die "--web-cdn-url is required with --web-assets s3"
  case "$WEB_S3_ENDPOINT" in http://*|https://*) ;; *) die "--web-s3-endpoint must use http or https" ;; esac
  case "$WEB_CDN_URL" in http://*|https://*) ;; *) die "--web-cdn-url must use http or https" ;; esac
  WEB_S3_ENDPOINT="${WEB_S3_ENDPOINT%/}"
  WEB_CDN_URL="${WEB_CDN_URL%/}"
  WEB_S3_PREFIX="${WEB_S3_PREFIX#/}"
  WEB_S3_PREFIX="${WEB_S3_PREFIX%/}"
  [[ -n "$WEB_S3_PREFIX" && "$WEB_S3_PREFIX" != *".."* ]] || die "KAGEOS_WEB_S3_PREFIX must be a clean object prefix"
  [[ "$WEB_S3_ENDPOINT$WEB_S3_BUCKET$WEB_CDN_URL$WEB_S3_PREFIX$WEB_S3_REGION" != *$'\n'* ]] || die "S3/CDN settings must not contain newlines"
  [[ "$WEB_S3_ENDPOINT$WEB_S3_BUCKET$WEB_CDN_URL$WEB_S3_PREFIX$WEB_S3_REGION" != *$'\r'* ]] || die "S3/CDN settings must not contain carriage returns"
}

resolve_web_assets_credentials() {
  [[ "$WEB_ASSETS_MODE" == "s3" && "$DRY_RUN" != "1" ]] || return 0
  if [[ -z "$WEB_S3_ACCESS_KEY_ID" && -r /dev/tty && -w /dev/tty ]]; then
    printf 'S3 Access Key: ' >/dev/tty
    IFS= read -r WEB_S3_ACCESS_KEY_ID </dev/tty || true
  fi
  if [[ -z "$WEB_S3_SECRET_ACCESS_KEY" && -r /dev/tty && -w /dev/tty ]]; then
    printf 'S3 Secret Key: ' >/dev/tty
    IFS= read -r -s WEB_S3_SECRET_ACCESS_KEY </dev/tty || true
    printf '\n' >/dev/tty
  fi
  [[ -n "$WEB_S3_ACCESS_KEY_ID" ]] || die "S3 access key is required; set KAGEOS_WEB_S3_ACCESS_KEY_ID or run interactively"
  [[ -n "$WEB_S3_SECRET_ACCESS_KEY" ]] || die "S3 secret key is required; set KAGEOS_WEB_S3_SECRET_ACCESS_KEY or run interactively"
  [[ "$WEB_S3_ACCESS_KEY_ID$WEB_S3_SECRET_ACCESS_KEY$WEB_S3_SESSION_TOKEN" != *$'\n'* ]] || die "S3 credentials must not contain newlines"
  [[ "$WEB_S3_ACCESS_KEY_ID$WEB_S3_SECRET_ACCESS_KEY$WEB_S3_SESSION_TOKEN" != *$'\r'* ]] || die "S3 credentials must not contain carriage returns"
}

resolve_web_assets_config

if [[ -z "$KAGEOS_DISTRIBUTION_BASE_URL" ]]; then
  if [[ "$USE_CN_MIRROR" == "1" || "$USE_CN_MIRROR" == "true" ]]; then
    KAGEOS_DISTRIBUTION_BASE_URL="$CN_DISTRIBUTION_BASE_URL"
  else
    KAGEOS_DISTRIBUTION_BASE_URL="$DEFAULT_DISTRIBUTION_BASE_URL"
  fi
fi
KAGEOS_DISTRIBUTION_BASE_URL="${KAGEOS_DISTRIBUTION_BASE_URL%/}"
[[ "$KAGEOS_DISTRIBUTION_BASE_URL" == https://* ]] || die "distribution base URL must use https"
KAGEOS_MANAGER_URL="${KAGEOS_MANAGER_URL:-${KAGEOS_DISTRIBUTION_BASE_URL}/kageos-manager.sh}"
if [[ -z "$KAGEOS_RELEASE_BASE_URL" ]]; then
  if [[ "$USE_CN_MIRROR" == "1" || "$USE_CN_MIRROR" == "true" ]]; then
    KAGEOS_RELEASE_BASE_URL="$CN_KAGEOS_RELEASE_BASE_URL"
  else
    KAGEOS_RELEASE_BASE_URL="$DEFAULT_KAGEOS_RELEASE_BASE_URL"
  fi
fi

apply_cn_mirror() {
  if [[ "$USE_CN_MIRROR" != "1" && "$USE_CN_MIRROR" != "true" ]]; then
    return 0
  fi

  if [[ "$KAGEOS_IMAGE" == "$DEFAULT_KAGEOS_IMAGE" ]]; then
    KAGEOS_IMAGE="$CN_ALIYUN_IMAGE"
    if [[ -n "$CN_TENCENT_IMAGE" ]]; then
      KAGEOS_IMAGE_FALLBACKS="${KAGEOS_IMAGE_FALLBACKS:+${KAGEOS_IMAGE_FALLBACKS} }${CN_TENCENT_IMAGE}"
    fi
  fi
  if [[ -z "$KAGEOS_APP_BASE_IMAGE" ]]; then
    KAGEOS_APP_BASE_IMAGE="$CN_ALIYUN_APP_BASE_IMAGE"
  fi
  if [[ -z "$KAGEOS_APP_BASE_PULL_FALLBACK_BUILD" ]]; then
    KAGEOS_APP_BASE_PULL_FALLBACK_BUILD=1
  fi
  if [[ -z "$KAGEOS_GOPROXY" ]]; then
    KAGEOS_GOPROXY="https://goproxy.cn,direct"
  fi
  if [[ -z "$KAGEOS_GOSUMDB" ]]; then
    KAGEOS_GOSUMDB="sum.golang.google.cn"
  fi
  if [[ -z "$KAGEOS_AIO_MYSQL_IMAGE" ]]; then
    KAGEOS_AIO_MYSQL_IMAGE="${CN_IMAGE_PREFIX}/library/mysql:latest"
  fi
  if [[ -z "$KAGEOS_AIO_NATS_IMAGE" ]]; then
    KAGEOS_AIO_NATS_IMAGE="${CN_IMAGE_PREFIX}/library/nats:latest"
  fi
  if [[ -z "$KAGEOS_AIO_MINIO_IMAGE" ]]; then
    KAGEOS_AIO_MINIO_IMAGE="${CN_IMAGE_PREFIX}/minio/minio:latest"
  fi
}

apply_cn_mirror

TLS_MODE="$(printf '%s' "$TLS_MODE" | tr '[:upper:]' '[:lower:]')"
case "$TLS_MODE" in
  auto|http|https|redirect|external) ;;
  *) die "--tls-mode must be auto, http, https, redirect, or external" ;;
esac

resolve_base_url
case "$BASE_URL" in
  http://*|https://*) ;;
  *) die "--base-url must start with http:// or https://" ;;
esac

if [[ -z "$DEPLOY_USER" ]]; then
  if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
    DEPLOY_USER="$SUDO_USER"
  else
    DEPLOY_USER="root"
  fi
fi

if [[ "$DEPLOY_USER" != "root" && "$DRY_RUN" != "1" ]]; then
  id "$DEPLOY_USER" >/dev/null 2>&1 || die "deploy user does not exist: $DEPLOY_USER"
fi

url_scheme="${BASE_URL%%://*}"
url_hostport="${BASE_URL#*://}"
url_hostport="${url_hostport%%/*}"
url_port=""
if [[ "$url_hostport" == *:* && "$url_hostport" != \[* ]]; then
  maybe_port="${url_hostport##*:}"
  if [[ "$maybe_port" =~ ^[0-9]+$ ]]; then
    url_port="$maybe_port"
  fi
fi

if [[ -z "$HTTP_PORT" ]]; then
  if [[ "$url_scheme" == "http" && -n "$url_port" ]]; then
    HTTP_PORT="$url_port"
  fi
fi
if [[ -z "$HTTPS_PORT" ]]; then
  if [[ "$url_scheme" == "https" && -n "$url_port" ]]; then
    HTTPS_PORT="$url_port"
  else
    HTTPS_PORT=443
  fi
fi
if [[ -n "$HTTP_PORT" ]]; then
  validate_port "HTTP port" "$HTTP_PORT"
fi
validate_port "HTTPS port" "$HTTPS_PORT"

if [[ "$TLS_MODE" == "auto" ]]; then
  if [[ "$url_scheme" == "https" ]]; then
    TLS_MODE="external"
  else
    TLS_MODE="http"
  fi
fi

if [[ "$TLS_MODE" == "http" && "$url_scheme" == "https" ]]; then
  warn "base URL is https:// but --tls-mode=http; use --tls-mode external if HTTPS terminates before kageos"
fi
if [[ "$TLS_MODE" == "redirect" && "$url_scheme" != "https" ]]; then
  die "--tls-mode redirect requires an https:// --base-url"
fi

case "$KAGEOS_START_TIMEOUT" in
  ''|*[!0-9]*) KAGEOS_START_TIMEOUT=1200 ;;
esac

detect_package_manager() {
  if command -v apt-get >/dev/null 2>&1; then
    echo apt
  elif command -v dnf >/dev/null 2>&1; then
    echo dnf
  elif command -v yum >/dev/null 2>&1; then
    echo yum
  elif command -v zypper >/dev/null 2>&1; then
    echo zypper
  elif command -v apk >/dev/null 2>&1; then
    echo apk
  else
    echo none
  fi
}

PKG_MANAGER="$(detect_package_manager)"
ENGINE=""

install_packages() {
  [[ "$KAGEOS_INSTALL_DEPS" != "never" ]] || return 1
  [[ "$PKG_MANAGER" != "none" ]] || return 1

  case "$PKG_MANAGER" in
    apt)
      export DEBIAN_FRONTEND=noninteractive
      apt-get update
      apt-get install -y "$@"
      ;;
    dnf)
      dnf install -y "$@"
      ;;
    yum)
      yum install -y "$@"
      ;;
    zypper)
      zypper --non-interactive install "$@"
      ;;
    apk)
      apk add --no-cache "$@"
      ;;
  esac
}

ensure_basic_tools() {
  local missing=()
  local cmd
  for cmd in curl; do
    command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
  done

  if [[ ${#missing[@]} -eq 0 ]]; then
    return
  fi

  log "Installing basic host tools"
  case "$PKG_MANAGER" in
    apt|dnf|yum|zypper|apk)
      install_packages ca-certificates curl || die "failed to install basic host tools"
      ;;
    none)
      die "missing required tools (${missing[*]}) and no supported package manager was found"
      ;;
  esac
}

install_engine() {
  local engine="$1"
  [[ "$KAGEOS_INSTALL_DEPS" != "never" ]] || return 1

  log "Installing ${engine}"
  case "$engine:$PKG_MANAGER" in
    podman:apt) install_packages podman fuse-overlayfs slirp4netns uidmap ;;
    podman:dnf|podman:yum) install_packages podman fuse-overlayfs slirp4netns ;;
    podman:zypper) install_packages podman fuse-overlayfs slirp4netns ;;
    podman:apk) install_packages podman fuse-overlayfs slirp4netns ;;
    docker:apt) install_packages docker.io ;;
    docker:dnf|docker:yum) install_packages docker ;;
    docker:zypper) install_packages docker ;;
    docker:apk) install_packages docker ;;
    *) return 1 ;;
  esac
}

engine_ready() {
  local engine="$1"
  command -v "$engine" >/dev/null 2>&1 || return 1
  "$engine" info >/dev/null 2>&1
}

select_engine() {
  if [[ "$KAGEOS_CONTAINER_ENGINE" == "podman" || "$KAGEOS_CONTAINER_ENGINE" == "docker" ]]; then
    if ! engine_ready "$KAGEOS_CONTAINER_ENGINE"; then
      install_engine "$KAGEOS_CONTAINER_ENGINE" || true
    fi
    engine_ready "$KAGEOS_CONTAINER_ENGINE" || die "${KAGEOS_CONTAINER_ENGINE} is not ready"
    ENGINE="$KAGEOS_CONTAINER_ENGINE"
    return
  fi

  if engine_ready podman; then
    ENGINE=podman
    return
  fi
  if engine_ready docker; then
    ENGINE=docker
    return
  fi

  install_engine podman || true
  if engine_ready podman; then
    ENGINE=podman
    return
  fi

  install_engine docker || true
  if engine_ready docker; then
    ENGINE=docker
    return
  fi

  die "podman or docker is required and could not be installed automatically"
}

enable_engine_autostart() {
  command -v systemctl >/dev/null 2>&1 || return 0

  case "$ENGINE" in
    podman)
      systemctl enable --now podman-restart.service >/dev/null 2>&1 || true
      ;;
    docker)
      systemctl enable --now docker >/dev/null 2>&1 || true
      ;;
  esac

  return 0
}

container_exists() {
  "$ENGINE" container inspect "$KAGEOS_CONTAINER_NAME" >/dev/null 2>&1
}

container_running() {
  [[ "$("$ENGINE" inspect -f '{{.State.Running}}' "$KAGEOS_CONTAINER_NAME" 2>/dev/null || true)" == "true" ]]
}

port_in_use() {
  local port="$1"
  if command -v ss >/dev/null 2>&1; then
    ss -lnt "( sport = :$port )" 2>/dev/null | awk 'NR > 1 { found=1 } END { exit found ? 0 : 1 }'
  elif command -v lsof >/dev/null 2>&1; then
    lsof -iTCP:"$port" -sTCP:LISTEN -P -n >/dev/null 2>&1
  elif command -v netstat >/dev/null 2>&1; then
    netstat -lnt 2>/dev/null | awk -v p=":$port" '$4 ~ p"$" { found=1 } END { exit found ? 0 : 1 }'
  else
    return 1
  fi
}

validate_instance_id() {
  [[ "$1" =~ ^instance-[0-9]{2,}$ ]] || die "--instance must look like instance-01"
}

configured_port_in_use() {
  local port="$1" file configured
  [[ -d "${KAGEOS_CONFIG_DIR}/instances" ]] || return 1
  for file in "${KAGEOS_CONFIG_DIR}"/instances/instance-*.env; do
    [[ -r "$file" ]] || continue
    configured="$(sed -n 's/^HOST_PORT=//p' "$file" | head -n 1)"
    configured="${configured//\'/}"
    [[ "$configured" == "$port" ]] && return 0
  done
  return 1
}

next_instance_number() {
  local value=""
  if [[ -r "${KAGEOS_CONFIG_DIR}/config.env" ]]; then
    value="$(sed -n 's/^NEXT_INSTANCE_NUMBER=//p' "${KAGEOS_CONFIG_DIR}/config.env" | head -n 1)"
    value="${value//\'/}"
  fi
  if [[ ! "$value" =~ ^[0-9]+$ || "$value" -lt 1 ]]; then
    value=1
  fi
  printf '%s\n' "$value"
}

append_port_to_http_base_url() {
  local url="$1" port="$2" authority suffix
  [[ "$url" == http://* ]] || { printf '%s\n' "$url"; return; }
  authority="${url#http://}"
  suffix=""
  if [[ "$authority" == */* ]]; then
    suffix="/${authority#*/}"
    authority="${authority%%/*}"
  fi
  if [[ "$authority" == *:* ]]; then
    printf '%s\n' "$url"
  else
    printf 'http://%s:%s%s\n' "$authority" "$port" "$suffix"
  fi
}

resolve_instance_plan() {
  local number candidate=10001 instance_config

  if [[ -z "$INSTANCE_ID" ]]; then
    number="$(next_instance_number)"
    printf -v INSTANCE_ID 'instance-%02d' "$number"
  fi
  validate_instance_id "$INSTANCE_ID"
  instance_config="${KAGEOS_CONFIG_DIR}/instances/${INSTANCE_ID}.env"
  [[ ! -e "$instance_config" ]] || die "instance already exists: ${INSTANCE_ID}; use 'sudo kageos update ${INSTANCE_ID}'"

  if [[ -z "$HTTP_PORT" ]]; then
    while port_in_use "$candidate" || configured_port_in_use "$candidate"; do
      candidate=$((candidate + 1))
      [[ "$candidate" -le 65535 ]] || die "no free TCP port is available from 10001"
    done
    HTTP_PORT="$candidate"
  elif configured_port_in_use "$HTTP_PORT"; then
    die "port ${HTTP_PORT} is already assigned to a kageos instance"
  fi

  if [[ -z "$KAGEOS_CONTAINER_NAME" ]]; then
    KAGEOS_CONTAINER_NAME="kageos-${INSTANCE_ID}"
  fi
  if [[ -z "$KAGEOS_DATA_DIR" ]]; then
    KAGEOS_DATA_DIR="${KAGEOS_INSTALL_DIR%/}/instances/${INSTANCE_ID}/data"
  fi
  BASE_URL="$(append_port_to_http_base_url "$BASE_URL" "$HTTP_PORT")"
}

show_port_owner() {
  local port="$1"
  if command -v ss >/dev/null 2>&1; then
    ss -lntp "( sport = :$port )" 2>/dev/null || true
  elif command -v lsof >/dev/null 2>&1; then
    lsof -iTCP:"$port" -sTCP:LISTEN -P -n || true
  fi
}

ensure_ports_available() {
  local port
  for port in "$HTTP_PORT"; do
    if port_in_use "$port"; then
      show_port_owner "$port" >&2
      die "port ${port} is already in use. Stop the existing service or pass --http-port with a free port."
    fi
  done

  if [[ "$TLS_MODE" == "https" || "$TLS_MODE" == "redirect" ]]; then
    if [[ "$HTTPS_PORT" != "$HTTP_PORT" ]] && port_in_use "$HTTPS_PORT"; then
      show_port_owner "$HTTPS_PORT" >&2
      die "port ${HTTPS_PORT} is already in use. Stop the existing service or pass --https-port with a free port."
    fi
  fi

  return 0
}

detect_host_forward_policy() {
  HOST_FORWARD_POLICY=""
  command -v iptables >/dev/null 2>&1 || return 0
  HOST_FORWARD_POLICY="$(iptables -S FORWARD 2>/dev/null | sed -n 's/^-P FORWARD //p' | head -n 1 || true)"
  return 0
}

host_forward_policy_may_block() {
  [[ "$HOST_FORWARD_POLICY" == "DROP" || "$HOST_FORWARD_POLICY" == "REJECT" ]]
}

add_host_forward_rule_for_port() {
  local port="$1"
  [[ -n "$port" ]] || return 0
  command -v iptables >/dev/null 2>&1 || {
    warn "iptables not found; skipped host FORWARD rule for port ${port}"
    return 0
  }

  if iptables -C FORWARD -p tcp --dport "$port" -m comment --comment "kageos public port ${port}" -j ACCEPT 2>/dev/null; then
    return 0
  fi

  log "Adding scoped host FORWARD rule for kageos port ${port}"
  if iptables -I FORWARD 1 -p tcp --dport "$port" -m comment --comment "kageos public port ${port}" -j ACCEPT 2>/dev/null; then
    HOST_FORWARD_RULES_ADDED=1
    return 0
  fi

  warn "failed to add scoped host FORWARD rule for port ${port}; if public access still times out, run: sudo iptables -P FORWARD ACCEPT"
  return 0
}

ensure_host_forward_policy() {
  host_forward_policy_may_block || return 0

  if [[ "$KAGEOS_FIX_FORWARD_POLICY" == "never" ]]; then
    warn "host FORWARD policy is ${HOST_FORWARD_POLICY}; public access to mapped container ports may time out even when localhost works."
    warn "If ${BASE_URL} is not reachable from the internet after install, verify cloud security groups first, then run: sudo iptables -P FORWARD ACCEPT"
    return 0
  fi

  warn "host FORWARD policy is ${HOST_FORWARD_POLICY}; adding scoped kageos port allow rule instead of changing the global policy."
  add_host_forward_rule_for_port "$HTTP_PORT"
  if [[ "$TLS_MODE" == "https" || "$TLS_MODE" == "redirect" ]]; then
    if [[ "$HTTPS_PORT" != "$HTTP_PORT" ]]; then
      add_host_forward_rule_for_port "$HTTPS_PORT"
    fi
  fi
  return 0
}

ensure_linger() {
  if [[ "$DEPLOY_USER" == "root" ]]; then
    return
  fi
  if ! command -v loginctl >/dev/null 2>&1; then
    warn "loginctl not found; skipped linger setup for ${DEPLOY_USER}"
    return
  fi

  local linger
  linger="$(loginctl show-user "$DEPLOY_USER" -p Linger --value 2>/dev/null || true)"
  if [[ "$linger" == "yes" ]]; then
    return 0
  fi

  log "Enabling systemd linger for ${DEPLOY_USER}"
  loginctl enable-linger "$DEPLOY_USER" || warn "failed to enable linger for ${DEPLOY_USER}"
  return 0
}

ensure_tls_files() {
  if [[ "$TLS_MODE" != "https" && "$TLS_MODE" != "redirect" ]]; then
    return
  fi

  local tls_dir="${KAGEOS_DATA_DIR%/}/tls"
  local cert="${tls_dir}/fullchain.pem"
  local key="${tls_dir}/privkey.pem"
  mkdir -p "$tls_dir"
  chmod 700 "$tls_dir"

  if [[ -s "$cert" && -s "$key" ]]; then
    log "TLS certificate already exists: ${tls_dir}"
    return
  fi

  command -v openssl >/dev/null 2>&1 || install_packages openssl || true
  command -v openssl >/dev/null 2>&1 || die "openssl is required to bootstrap HTTPS certificates"

  local host="${url_hostport%%:*}"
  host="${host#[}"
  host="${host%]}"
  local san="DNS:${host}"
  if [[ "$host" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    san="IP:${host}"
  fi

  log "Generating self-signed bootstrap certificate for ${host}"
  openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
    -keyout "$key" \
    -out "$cert" \
    -subj "/CN=${host}" \
    -addext "subjectAltName=${san}" >/dev/null 2>&1
  chmod 600 "$cert" "$key"
  warn "Generated a self-signed HTTPS certificate. Replace ${cert} and ${key} with trusted certificates before inviting users."
}

release_arch() {
  local machine
  machine="$(uname -m)"
  case "$machine" in
    x86_64|amd64) echo amd64 ;;
    aarch64|arm64) echo arm64 ;;
    *) return 1 ;;
  esac
}

sha256_file() {
  local file="$1"
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$file" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$file" | awk '{print $1}'
  else
    return 1
  fi
}

fetch_release_version() {
  local base_url="${KAGEOS_RELEASE_BASE_URL%/}"
  local version="$KAGEOS_RELEASE_VERSION"

  if [[ "$version" == "latest" ]]; then
    version="$(curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "${base_url}/latest.txt" | tr -d '[:space:]')"
  fi

  [[ -n "$version" ]] || return 1
  if [[ "$version" != v* ]]; then
    version="v${version}"
  fi
  [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z][0-9A-Za-z.-]*)?$ ]] || return 1
  printf '%s\n' "$version"
}

load_cn_release_archive() {
  [[ "$USE_CN_MIRROR" == "1" || "$USE_CN_MIRROR" == "true" ]] || return 1
  [[ "$KAGEOS_CN_TARBALL" != "0" && "$KAGEOS_CN_TARBALL" != "false" ]] || return 1
  [[ "$USER_SET_IMAGE" != "1" ]] || return 1
  [[ "$SKIP_PULL" != "1" ]] || return 1

  local arch version version_tag base_url artifact artifact_url checksum_url
  if ! arch="$(release_arch)"; then
    warn "unsupported architecture for release archive: $(uname -m)"
    return 1
  fi
  if ! version="$(fetch_release_version)"; then
    warn "failed to resolve kageos release version from ${KAGEOS_RELEASE_BASE_URL%/}"
    return 1
  fi

  version_tag="${version#v}"
  base_url="${KAGEOS_RELEASE_BASE_URL%/}/${version}"
  artifact="kageos-linux-${arch}.tar.gz"
  artifact_url="${base_url}/${artifact}"
  checksum_url="${artifact_url}.sha256"

  local download_dir archive checksum_file expected actual
  download_dir="${KAGEOS_DATA_DIR%/}/downloads"
  archive="${download_dir}/${artifact}"
  checksum_file="${download_dir}/${artifact}.sha256"
  mkdir -p "$download_dir"

  log "Resolving release checksum: ${checksum_url}"
  if ! curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 -o "$checksum_file" "$checksum_url"; then
    warn "failed to download release checksum; falling back to image registry"
    return 1
  fi
  expected="$(awk '{print $1}' "$checksum_file" 2>/dev/null || true)"
  if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then
    warn "invalid release checksum; falling back to image registry"
    return 1
  fi

  if [[ -s "$archive" ]]; then
    actual="$(sha256_file "$archive" 2>/dev/null || true)"
    if [[ "$actual" == "$expected" ]]; then
      log "Using cached release archive: ${archive}"
    else
      rm -f "$archive"
    fi
  fi

  if [[ ! -s "$archive" ]]; then
    log "Downloading kageos release archive: ${artifact_url}"
    if ! curl -fL --retry 3 --retry-delay 2 --connect-timeout 20 -o "$archive" "$artifact_url"; then
      warn "failed to download release archive; falling back to image registry"
      rm -f "$archive"
      return 1
    fi
  fi

  actual="$(sha256_file "$archive" 2>/dev/null || true)"
  if [[ "$actual" != "$expected" ]]; then
    warn "release archive checksum mismatch; falling back to image registry"
    rm -f "$archive"
    return 1
  fi

  if ! command -v gzip >/dev/null 2>&1; then
    install_packages gzip || true
  fi
  command -v gzip >/dev/null 2>&1 || {
    warn "gzip is required to load the release archive; falling back to image registry"
    return 1
  }

  log "Loading kageos image from release archive"
  if gzip -dc "$archive" | "$ENGINE" load; then
    KAGEOS_IMAGE="${DEFAULT_KAGEOS_IMAGE%:latest}:${version_tag}"
    SKIP_PULL=1
    log "Loaded image: ${KAGEOS_IMAGE}"
    return 0
  fi

  warn "failed to load release archive; falling back to image registry"
  return 1
}

try_pull_image() {
  local image="$1"
  log "Pulling image: ${image}"
  "$ENGINE" pull "$image"
}

pull_main_image() {
  [[ "$SKIP_PULL" != "1" ]] || return 0

  local images=("$KAGEOS_IMAGE")
  local image attempted=" "

  if [[ -n "$KAGEOS_IMAGE_FALLBACK" ]]; then
    images+=("$KAGEOS_IMAGE_FALLBACK")
  fi
  if [[ -n "$KAGEOS_IMAGE_FALLBACKS" ]]; then
    for image in $KAGEOS_IMAGE_FALLBACKS; do
      images+=("$image")
    done
  fi

  for image in "${images[@]}"; do
    [[ -n "$image" ]] || continue
    if [[ "$attempted" == *" ${image} "* ]]; then
      continue
    fi
    attempted="${attempted}${image} "

    if try_pull_image "$image"; then
      KAGEOS_IMAGE="$image"
      return 0
    fi
    warn "failed to pull ${image}; trying next source"
  done

  if load_cn_release_archive; then
    return 0
  fi

  if [[ "$attempted" != *" ${DEFAULT_KAGEOS_IMAGE} "* ]]; then
    warn "all configured image pulls failed; trying upstream image"
    if try_pull_image "$DEFAULT_KAGEOS_IMAGE"; then
      KAGEOS_IMAGE="$DEFAULT_KAGEOS_IMAGE"
      return 0
    fi
  fi

  die "failed to pull or load kageos image from all configured sources"
}

remove_existing_container() {
  if container_exists; then
    die "container already exists: ${KAGEOS_CONTAINER_NAME}"
  fi

  return 0
}

web_assets_release_for_image() {
  local image="$1" image_id release
  image_id="$("$ENGINE" image inspect -f '{{.Id}}' "$image" 2>/dev/null || true)"
  image_id="${image_id#sha256:}"
  release="$(printf '%s' "$image_id" | tr -c 'A-Za-z0-9._-' '-')"
  [[ -n "$release" ]] || die "could not resolve a release id for image ${image}"
  printf '%s\n' "$release"
}

publish_web_assets() {
  local image="$1" release publisher_env status
  WEB_ASSET_BASE_URL=""
  [[ "$WEB_ASSETS_MODE" == "s3" ]] || return 0

  release="$(web_assets_release_for_image "$image")"
  WEB_ASSET_BASE_URL="${WEB_CDN_URL}/${WEB_S3_PREFIX}/releases/${release}"
  publisher_env="$(mktemp "${KAGEOS_CONFIG_DIR%/}/.web-assets.XXXXXX")"
  chmod 0600 "$publisher_env"
  {
    printf 'KAGEOS_WEB_S3_ENDPOINT=%s\n' "$WEB_S3_ENDPOINT"
    printf 'KAGEOS_WEB_S3_BUCKET=%s\n' "$WEB_S3_BUCKET"
    printf 'KAGEOS_WEB_CDN_URL=%s\n' "$WEB_CDN_URL"
    printf 'KAGEOS_WEB_S3_PREFIX=%s\n' "$WEB_S3_PREFIX"
    printf 'KAGEOS_WEB_S3_REGION=%s\n' "$WEB_S3_REGION"
    printf 'KAGEOS_WEB_ASSETS_RELEASE=%s\n' "$release"
    printf 'KAGEOS_WEB_S3_ACCESS_KEY_ID=%s\n' "$WEB_S3_ACCESS_KEY_ID"
    printf 'KAGEOS_WEB_S3_SECRET_ACCESS_KEY=%s\n' "$WEB_S3_SECRET_ACCESS_KEY"
    printf 'KAGEOS_WEB_S3_SESSION_TOKEN=%s\n' "$WEB_S3_SESSION_TOKEN"
  } > "$publisher_env"

  log "Publishing immutable web assets to S3 before starting the new instance"
  set +e
  "$ENGINE" run --rm \
    --entrypoint /app/web-assets-publisher \
    --env-file "$publisher_env" \
    "$image"
  status=$?
  set -e
  rm -f "$publisher_env"
  [[ "$status" -eq 0 ]] || die "web asset upload failed; the kageos instance was not started"
}

start_container() {
  local env_args=()
  local env_file_args=()
  local port_args=("-p" "${HTTP_PORT}:${CONTAINER_HTTP_PORT}")
  mkdir -p "$KAGEOS_INSTALL_DIR" "$KAGEOS_DATA_DIR" "${KAGEOS_DATA_DIR%/}/tls"

  if [[ "$TLS_MODE" == "https" || "$TLS_MODE" == "redirect" ]]; then
    if [[ "$HTTPS_PORT" != "$HTTP_PORT" ]]; then
      port_args+=("-p" "${HTTPS_PORT}:${HTTPS_PORT}")
    fi
  fi
  if [[ -n "$KAGEOS_ENV_FILE" ]]; then
    [[ -r "$KAGEOS_ENV_FILE" ]] || die "runtime environment file is not readable: $KAGEOS_ENV_FILE"
    env_file_args=("--env-file" "$KAGEOS_ENV_FILE")
  fi

  append_optional_env() {
    local name="$1"
    local value="${!name:-}"
    if [[ -n "$value" ]]; then
      env_args+=("-e" "${name}=${value}")
    fi
    return 0
  }

  append_optional_env KAGEOS_APP_BASE_IMAGE
  append_optional_env KAGEOS_APP_BASE_PULL
  append_optional_env KAGEOS_APP_BASE_PULL_FALLBACK_BUILD
  append_optional_env KAGEOS_AIO_MYSQL_IMAGE
  append_optional_env KAGEOS_AIO_NATS_IMAGE
  append_optional_env KAGEOS_AIO_MINIO_IMAGE
  append_optional_env KAGEOS_AIO_RECREATE_INFRA
  if [[ -n "$KAGEOS_GOPROXY" ]]; then
    env_args+=("-e" "GOPROXY=${KAGEOS_GOPROXY}")
  fi
  if [[ -n "$KAGEOS_GOSUMDB" ]]; then
    env_args+=("-e" "GOSUMDB=${KAGEOS_GOSUMDB}")
  fi

  pull_main_image
  publish_web_assets "$KAGEOS_IMAGE"
  if [[ -n "$WEB_ASSET_BASE_URL" ]]; then
    env_args+=("-e" "KAGEOS_WEB_ASSET_BASE_URL=${WEB_ASSET_BASE_URL}")
  fi

  log "Starting kageos container: ${KAGEOS_CONTAINER_NAME}"
  "$ENGINE" run -d \
    --name "$KAGEOS_CONTAINER_NAME" \
    --privileged \
    "${port_args[@]}" \
    "${env_file_args[@]}" \
    --restart unless-stopped \
    -v "${KAGEOS_DATA_DIR}:/var/lib/kageos" \
    -v "${KAGEOS_DATA_DIR%/}/tls:/app/tls" \
    -e "CANONICAL_BASE_URL=${BASE_URL}" \
    -e "TLS_MODE=${TLS_MODE}" \
    -e "HTTP_PORT=${CONTAINER_HTTP_PORT}" \
    -e "HTTPS_PORT=${HTTPS_PORT}" \
    -e "TLS_CERT_FILE=/app/tls/fullchain.pem" \
    -e "TLS_KEY_FILE=/app/tls/privkey.pem" \
    -e "TZ=${TIMEZONE}" \
    -e "KAGEOS_AIO_DATA_DIR=/var/lib/kageos" \
    -e "KAGEOS_AIO_PRINT_SECRETS=${KAGEOS_AIO_PRINT_SECRETS}" \
    "${env_args[@]}" \
    "$KAGEOS_IMAGE" >/dev/null
}

write_instance_state() {
  local instances_dir="${KAGEOS_CONFIG_DIR%/}/instances"
  local instance_file="${instances_dir}/${INSTANCE_ID}.env"
  local instance_tmp global_tmp current_next instance_number new_next

  mkdir -p "$instances_dir"
  chmod 0700 "$KAGEOS_CONFIG_DIR" "$instances_dir"
  printf '%s\n' "$INSTANCE_ID" > "${KAGEOS_DATA_DIR%/}/.kageos-instance"
  chmod 0600 "${KAGEOS_DATA_DIR%/}/.kageos-instance"

  instance_tmp="$(mktemp "${instances_dir}/.${INSTANCE_ID}.XXXXXX")"
  {
    printf 'INSTANCE_ID=%q\n' "$INSTANCE_ID"
    printf 'ENGINE=%q\n' "$ENGINE"
    printf 'IMAGE=%q\n' "$KAGEOS_IMAGE"
    printf 'CONTAINER_NAME=%q\n' "$KAGEOS_CONTAINER_NAME"
    printf 'BASE_URL=%q\n' "$BASE_URL"
    printf 'TLS_MODE=%q\n' "$TLS_MODE"
    printf 'HOST_PORT=%q\n' "$HTTP_PORT"
    printf 'CONTAINER_HTTP_PORT=%q\n' "$CONTAINER_HTTP_PORT"
    printf 'HTTPS_PORT=%q\n' "$HTTPS_PORT"
    printf 'TIMEZONE=%q\n' "$TIMEZONE"
    printf 'INSTALL_DIR=%q\n' "$KAGEOS_INSTALL_DIR"
    printf 'DATA_DIR=%q\n' "$KAGEOS_DATA_DIR"
    printf 'ENV_FILE=%q\n' "$KAGEOS_ENV_FILE"
    printf 'AIO_PRINT_SECRETS=%q\n' "$KAGEOS_AIO_PRINT_SECRETS"
    printf 'APP_BASE_IMAGE=%q\n' "$KAGEOS_APP_BASE_IMAGE"
    printf 'APP_BASE_PULL=%q\n' "$KAGEOS_APP_BASE_PULL"
    printf 'APP_BASE_PULL_FALLBACK_BUILD=%q\n' "$KAGEOS_APP_BASE_PULL_FALLBACK_BUILD"
    printf 'AIO_MYSQL_IMAGE=%q\n' "$KAGEOS_AIO_MYSQL_IMAGE"
    printf 'AIO_NATS_IMAGE=%q\n' "$KAGEOS_AIO_NATS_IMAGE"
    printf 'AIO_MINIO_IMAGE=%q\n' "$KAGEOS_AIO_MINIO_IMAGE"
    printf 'AIO_RECREATE_INFRA=%q\n' "$KAGEOS_AIO_RECREATE_INFRA"
    printf 'GOPROXY_VALUE=%q\n' "$KAGEOS_GOPROXY"
    printf 'GOSUMDB_VALUE=%q\n' "$KAGEOS_GOSUMDB"
    printf 'FIX_FORWARD_POLICY=%q\n' "$KAGEOS_FIX_FORWARD_POLICY"
    printf 'CN_MODE=%q\n' "$USE_CN_MIRROR"
    printf 'DISTRIBUTION_BASE_URL=%q\n' "$KAGEOS_DISTRIBUTION_BASE_URL"
    printf 'WEB_ASSETS_MODE=%q\n' "$WEB_ASSETS_MODE"
    printf 'WEB_S3_ENDPOINT=%q\n' "$WEB_S3_ENDPOINT"
    printf 'WEB_S3_BUCKET=%q\n' "$WEB_S3_BUCKET"
    printf 'WEB_CDN_URL=%q\n' "$WEB_CDN_URL"
    printf 'WEB_S3_PREFIX=%q\n' "$WEB_S3_PREFIX"
    printf 'WEB_S3_REGION=%q\n' "$WEB_S3_REGION"
    printf 'WEB_S3_ACCESS_KEY_ID=%q\n' "$WEB_S3_ACCESS_KEY_ID"
    printf 'WEB_S3_SECRET_ACCESS_KEY=%q\n' "$WEB_S3_SECRET_ACCESS_KEY"
    printf 'WEB_S3_SESSION_TOKEN=%q\n' "$WEB_S3_SESSION_TOKEN"
  } > "$instance_tmp"
  chmod 0600 "$instance_tmp"
  mv "$instance_tmp" "$instance_file"

  current_next="$(next_instance_number)"
  instance_number="$((10#${INSTANCE_ID#instance-}))"
  new_next="$current_next"
  if (( instance_number >= current_next )); then
    new_next="$((instance_number + 1))"
  fi
  global_tmp="$(mktemp "${KAGEOS_CONFIG_DIR%/}/.config.XXXXXX")"
  {
    printf 'CONFIG_VERSION=1\n'
    printf 'NEXT_INSTANCE_NUMBER=%q\n' "$new_next"
    printf 'DISTRIBUTION_BASE_URL=%q\n' "$KAGEOS_DISTRIBUTION_BASE_URL"
  } > "$global_tmp"
  chmod 0600 "$global_tmp"
  mv "$global_tmp" "${KAGEOS_CONFIG_DIR%/}/config.env"
}

install_helper_cli() {
  local helper="/usr/local/bin/kageos"
  local manager_tmp

  manager_tmp="$(mktemp)"
  if [[ -n "$KAGEOS_MANAGER_SOURCE" ]]; then
    [[ -r "$KAGEOS_MANAGER_SOURCE" ]] || die "manager source not readable: $KAGEOS_MANAGER_SOURCE"
    cp "$KAGEOS_MANAGER_SOURCE" "$manager_tmp"
  else
    log "Downloading kageos instance manager"
    curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 -o "$manager_tmp" "$KAGEOS_MANAGER_URL"
  fi
  bash -n "$manager_tmp" || {
    rm -f "$manager_tmp"
    die "downloaded kageos instance manager is invalid"
  }
  install -m 0755 "$manager_tmp" "$helper"
  rm -f "$manager_tmp"
}
wait_for_start() {
  local deadline
  deadline=$((SECONDS + KAGEOS_START_TIMEOUT))

  log "Waiting for kageos first boot. This can take several minutes."
  while (( SECONDS < deadline )); do
    if "$ENGINE" logs "$KAGEOS_CONTAINER_NAME" 2>&1 | grep -qi "kageos started successfully"; then
      return 0
    fi
    if "$ENGINE" exec "$KAGEOS_CONTAINER_NAME" sh -lc 'test -s /var/lib/kageos/secrets/SYSTEM_USER_PASSWORD && curl --silent --show-error --fail --max-time 5 http://127.0.0.1:9090/health >/dev/null' >/dev/null 2>&1; then
      return 0
    fi
    if ! container_running; then
      "$ENGINE" logs "$KAGEOS_CONTAINER_NAME" 2>&1 | tail -n 160 >&2 || true
      die "kageos container stopped before startup completed"
    fi
    sleep 5
  done

  "$ENGINE" logs "$KAGEOS_CONTAINER_NAME" 2>&1 | tail -n 160 >&2 || true
  die "startup did not finish within ${KAGEOS_START_TIMEOUT}s. Run 'sudo kageos logs' to continue watching."
}

print_success_summary() {
  local password=""
  password="$(cat "${KAGEOS_DATA_DIR%/}/secrets/SYSTEM_USER_PASSWORD" 2>/dev/null || true)"

  cat <<EOF

kageos installed successfully

Instance: ${INSTANCE_ID}
URL:      ${BASE_URL}
Username: system
Password: ${password:-"(not ready yet; run sudo kageos password later)"}

Useful commands:
  sudo kageos status
  sudo kageos logs
  sudo kageos password
  sudo kageos restart
  sudo kageos update
  sudo kageos instance list

Data:
  ${KAGEOS_DATA_DIR}
EOF

  if host_forward_policy_may_block; then
    if [[ "$HOST_FORWARD_RULES_ADDED" == "1" ]]; then
      cat <<EOF

Network:
  Host FORWARD policy is ${HOST_FORWARD_POLICY}.
  Added scoped kageos port allow rule(s); no global FORWARD policy change was made.
  To persist current iptables rules on Ubuntu/Debian after confirming access:
    sudo apt-get install -y iptables-persistent netfilter-persistent
    sudo netfilter-persistent save
EOF
    else
      cat <<EOF

Network warning:
  Host FORWARD policy is ${HOST_FORWARD_POLICY}.
  If localhost works but the public URL times out, run:
    sudo iptables -P FORWARD ACCEPT
  To persist it on Ubuntu/Debian after confirming the fix:
    sudo apt-get install -y iptables-persistent netfilter-persistent
    sudo netfilter-persistent save
EOF
    fi
  fi
}

if [[ "${EUID:-$(id -u)}" -ne 0 && "$DRY_RUN" != "1" ]]; then
  if [[ "$USE_CN_MIRROR" == "1" || "$USE_CN_MIRROR" == "true" ]]; then
    die "run this installer with sudo, for example: curl -fsSL ${KAGEOS_DISTRIBUTION_BASE_URL}/install-prod.sh | sudo bash -s -- --cn"
  fi
  die "run this installer with sudo, for example: curl -fsSL ${KAGEOS_DISTRIBUTION_BASE_URL}/install-prod.sh | sudo bash"
fi

resolve_web_assets_credentials

if [[ "$DRY_RUN" != "1" && -r /etc/kageos-helper.env && ! -r "${KAGEOS_CONFIG_DIR%/}/config.env" ]]; then
  die "legacy installation detected at /etc/kageos-helper.env; migrate it before using the multi-instance installer"
fi

if [[ "$DRY_RUN" != "1" ]]; then
  acquire_install_lock
fi
resolve_instance_plan

printf 'kageos production one-command installer\n'
printf 'image:       %s\n' "$KAGEOS_IMAGE"
if [[ -n "$KAGEOS_IMAGE_FALLBACK" ]]; then
  printf 'fallback:    %s\n' "$KAGEOS_IMAGE_FALLBACK"
fi
cat <<EOF
instance:    ${INSTANCE_ID}
container:   ${KAGEOS_CONTAINER_NAME}
install dir: ${KAGEOS_INSTALL_DIR}
data dir:    ${KAGEOS_DATA_DIR}
base url:    ${BASE_URL}
tls mode:    ${TLS_MODE}
host port:   ${HTTP_PORT}
inner port:  ${CONTAINER_HTTP_PORT}
https port:  ${HTTPS_PORT}
timezone:    ${TIMEZONE}
cn mode:     ${USE_CN_MIRROR}
script source: ${KAGEOS_DISTRIBUTION_BASE_URL}
release source: ${KAGEOS_RELEASE_BASE_URL}
forward fix: ${KAGEOS_FIX_FORWARD_POLICY}
web assets:  ${WEB_ASSETS_MODE}
EOF
if [[ "$WEB_ASSETS_MODE" == "s3" ]]; then
  printf 'web bucket:  %s\n' "$WEB_S3_BUCKET"
  printf 'web CDN:     %s\n' "$WEB_CDN_URL"
  if [[ "$DRY_RUN" == "1" ]]; then
    printf 'web keys:    not checked in dry-run\n'
  else
    printf 'web keys:    configured (hidden)\n'
  fi
fi
if [[ -n "$KAGEOS_GOPROXY" ]]; then
  printf 'go proxy:    %s\n' "$KAGEOS_GOPROXY"
fi
if [[ -n "$KAGEOS_GOSUMDB" ]]; then
  printf 'go sumdb:    %s\n' "$KAGEOS_GOSUMDB"
fi
if [[ ( "$USE_CN_MIRROR" == "1" || "$USE_CN_MIRROR" == "true" ) && "$KAGEOS_CN_TARBALL" != "0" && "$KAGEOS_CN_TARBALL" != "false" ]]; then
  printf 'cn release:  %s (%s)\n' "$KAGEOS_RELEASE_BASE_URL" "$KAGEOS_RELEASE_VERSION"
fi

if [[ "$DRY_RUN" == "1" ]]; then
  log "Dry run complete. No host changes were made."
  exit 0
fi

ensure_basic_tools
select_engine
log "Container engine is ready: ${ENGINE}"
enable_engine_autostart
ensure_linger
remove_existing_container
ensure_ports_available
detect_host_forward_policy
ensure_host_forward_policy
ensure_tls_files
install_helper_cli
start_container
write_instance_state
wait_for_start
print_success_summary
