#!/usr/bin/env bash

set -euo pipefail

SCRIPT_NAME="$(basename "$0")"
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}"
LOG_DIR="$STATE_DIR/openclaw-mission-control-install"

PLATFORM=""
LINUX_DISTRO=""
PKG_MANAGER=""
PKG_UPDATED=0
DOCKER_USE_SUDO=0
INTERACTIVE=0

FORCE_MODE=""
FORCE_BACKEND_PORT=""
FORCE_FRONTEND_PORT=""
FORCE_PUBLIC_HOST=""
FORCE_API_URL=""
FORCE_TOKEN_MODE=""
FORCE_LOCAL_AUTH_TOKEN=""
FORCE_DB_MODE=""
FORCE_DATABASE_URL=""
FORCE_START_SERVICES=""
FORCE_INSTALL_SERVICE=""

if [[ -t 0 ]]; then
  INTERACTIVE=1
fi

info() {
  printf '[INFO] %s\n' "$*"
}

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

error() {
  printf '[ERROR] %s\n' "$*" >&2
}

die() {
  error "$*"
  exit 1
}

command_exists() {
  command -v "$1" >/dev/null 2>&1
}

usage() {
  cat <<EOF
Usage: $SCRIPT_NAME [options]

Options:
  --mode <docker|local>
  --backend-port <port>
  --frontend-port <port>
  --public-host <host>
  --api-url <url>
  --token-mode <generate|manual>
  --local-auth-token <token>      Required when --token-mode manual
  --db-mode <docker|external>     Local mode only
  --database-url <url>            Required when --db-mode external
  --start-services <yes|no>       Local mode only
  --install-service               Local mode only: install systemd user units for run at boot (Linux)
  -h, --help

If an option is omitted, the script prompts in interactive mode and uses defaults in non-interactive mode.
EOF
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mode)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --mode"
        fi
        FORCE_MODE="$2"
        shift 2
        ;;
      --backend-port)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --backend-port"
        fi
        FORCE_BACKEND_PORT="$2"
        shift 2
        ;;
      --frontend-port)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --frontend-port"
        fi
        FORCE_FRONTEND_PORT="$2"
        shift 2
        ;;
      --public-host)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --public-host"
        fi
        FORCE_PUBLIC_HOST="$2"
        shift 2
        ;;
      --api-url)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --api-url"
        fi
        FORCE_API_URL="$2"
        shift 2
        ;;
      --token-mode)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --token-mode"
        fi
        FORCE_TOKEN_MODE="$2"
        shift 2
        ;;
      --local-auth-token)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --local-auth-token"
        fi
        FORCE_LOCAL_AUTH_TOKEN="$2"
        shift 2
        ;;
      --db-mode)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --db-mode"
        fi
        FORCE_DB_MODE="$2"
        shift 2
        ;;
      --database-url)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --database-url"
        fi
        FORCE_DATABASE_URL="$2"
        shift 2
        ;;
      --start-services)
        if [[ $# -lt 2 || -z ${2:-} ]]; then
          usage
          die "Missing value for --start-services"
        fi
        FORCE_START_SERVICES="$2"
        shift 2
        ;;
      --install-service)
        FORCE_INSTALL_SERVICE="yes"
        shift
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "Unknown argument: $1"
        ;;
    esac
  done
}

is_one_of() {
  local value="$1"
  shift
  local option
  for option in "$@"; do
    if [[ "$value" == "$option" ]]; then
      return 0
    fi
  done
  return 1
}

as_root() {
  if [[ "$(id -u)" -eq 0 ]]; then
    "$@"
  else
    if ! command_exists sudo; then
      die "sudo is required to install system packages."
    fi
    sudo "$@"
  fi
}

install_command_hint() {
  local manager="$1"
  shift
  local -a packages
  packages=("$@")

  case "$manager" in
    apt)
      printf 'sudo apt-get update && sudo apt-get install -y %s' "${packages[*]}"
      ;;
    dnf)
      printf 'sudo dnf install -y %s' "${packages[*]}"
      ;;
    yum)
      printf 'sudo yum install -y %s' "${packages[*]}"
      ;;
    zypper)
      printf 'sudo zypper install -y %s' "${packages[*]}"
      ;;
    pacman)
      printf 'sudo pacman -Sy --noconfirm %s' "${packages[*]}"
      ;;
    brew)
      printf 'brew install %s' "${packages[*]}"
      ;;
    *)
      printf 'install packages manually: %s' "${packages[*]}"
      ;;
  esac
}

detect_platform() {
  local uname_s
  uname_s="$(uname -s)"
  if [[ "$uname_s" == "Darwin" ]]; then
    PLATFORM="darwin"
    PKG_MANAGER="brew"
    if ! command_exists brew; then
      die "Homebrew is required on macOS. Install from https://brew.sh, then re-run this script."
    fi
    info "Detected platform: darwin (macOS), package manager: Homebrew"
    return
  fi

  if [[ "$uname_s" != "Linux" ]]; then
    die "Unsupported platform: $uname_s. Linux and macOS (Darwin) are supported."
  fi

  PLATFORM="linux"
  if [[ ! -r /etc/os-release ]]; then
    die "Cannot detect Linux distribution (/etc/os-release missing)."
  fi

  # shellcheck disable=SC1091
  . /etc/os-release
  LINUX_DISTRO="${ID:-unknown}"
  # ID_LIKE is available via /etc/os-release when we need it for future detection heuristics.

  if command_exists apt-get; then
    PKG_MANAGER="apt"
  elif command_exists dnf; then
    PKG_MANAGER="dnf"
  elif command_exists yum; then
    PKG_MANAGER="yum"
  elif command_exists zypper; then
    PKG_MANAGER="zypper"
  elif command_exists pacman; then
    PKG_MANAGER="pacman"
  else
    die "Unsupported Linux distribution: $LINUX_DISTRO. No supported package manager detected (expected apt/dnf/yum/zypper/pacman)."
  fi

  if [[ "$PKG_MANAGER" != "apt" ]]; then
    warn "Detected distro '$LINUX_DISTRO' with package manager '$PKG_MANAGER'. This installer currently provides Debian/Ubuntu as stable path; other distros are scaffolded with actionable guidance."
  fi

  info "Detected Linux distro: $LINUX_DISTRO (package manager: $PKG_MANAGER)"
}

install_packages() {
  local -a packages
  packages=("$@")

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

  case "$PKG_MANAGER" in
    apt)
      if [[ "$PKG_UPDATED" -eq 0 ]]; then
        as_root apt-get update
        PKG_UPDATED=1
      fi
      as_root apt-get install -y "${packages[@]}"
      ;;
    brew)
      brew install "${packages[@]}"
      ;;
    dnf|yum|zypper|pacman)
      die "Automatic package install is not implemented yet for '$PKG_MANAGER'. Run: $(install_command_hint "$PKG_MANAGER" "${packages[@]}")"
      ;;
    *)
      die "Unknown package manager '$PKG_MANAGER'. Install manually: ${packages[*]}"
      ;;
  esac
}

prompt_with_default() {
  local prompt="$1"
  local default_value="$2"
  local input=""

  if [[ "$INTERACTIVE" -eq 0 ]]; then
    printf '%s\n' "$default_value"
    return
  fi

  read -r -p "$prompt [$default_value]: " input
  input="${input:-$default_value}"
  printf '%s\n' "$input"
}

prompt_choice() {
  local prompt="$1"
  local default_value="$2"
  shift 2
  local -a options
  local input=""
  local option=""
  options=("$@")

  if [[ "$INTERACTIVE" -eq 0 ]]; then
    printf '%s\n' "$default_value"
    return
  fi

  while true; do
    read -r -p "$prompt [$(IFS='/'; echo "${options[*]}")] (default: $default_value): " input
    input="${input:-$default_value}"
    for option in "${options[@]}"; do
      if [[ "$input" == "$option" ]]; then
        printf '%s\n' "$input"
        return
      fi
    done
    warn "Invalid choice: $input"
  done
}

prompt_secret() {
  local prompt="$1"
  local input=""

  if [[ "$INTERACTIVE" -eq 0 ]]; then
    printf '\n'
    return
  fi

  read -r -s -p "$prompt: " input
  printf '\n' >&2
  printf '%s\n' "$input"
}

confirm() {
  local prompt="$1"
  local default="${2:-y}"
  local input=""

  if [[ "$INTERACTIVE" -eq 0 ]]; then
    [[ "$default" == "y" ]]
    return
  fi

  if [[ "$default" == "y" ]]; then
    read -r -p "$prompt [Y/n]: " input
    input="${input:-y}"
  else
    read -r -p "$prompt [y/N]: " input
    input="${input:-n}"
  fi

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

is_valid_port() {
  local value="$1"
  [[ "$value" =~ ^[0-9]+$ ]] || return 1
  ((value >= 1 && value <= 65535))
}

generate_token() {
  if command_exists openssl; then
    openssl rand -hex 32
  else
    tr -dc 'A-Za-z0-9' </dev/urandom | head -c 64
    printf '\n'
  fi
}

# Portable relative path: print path of $1 relative to $2 (both absolute).
relative_to() {
  local target="$1"
  local base="$2"
  local rel
  if [[ -z "$base" || -z "$target" ]]; then
    printf '%s' "$target"
    return
  fi
  base="${base%/}"
  target="${target%/}"
  if [[ "$target" == "$base" ]]; then
    printf ''
    return
  fi
  rel="${target#$base/}"
  if [[ "$rel" != "$target" ]]; then
    printf '%s' "$rel"
  else
    printf '%s' "$target"
  fi
}

ensure_file_from_example() {
  local target_file="$1"
  local example_file="$2"
  local display_path

  if [[ -f "$target_file" ]]; then
    return
  fi

  if [[ ! -f "$example_file" ]]; then
    die "Missing example file: $example_file"
  fi

  cp "$example_file" "$target_file"
  if command_exists realpath && realpath --relative-to="$REPO_ROOT" "$target_file" >/dev/null 2>&1; then
    display_path="$(realpath --relative-to="$REPO_ROOT" "$target_file" 2>/dev/null)"
  else
    display_path="$(relative_to "$(cd -- "$(dirname -- "$target_file")" && pwd -P)/$(basename "$target_file")" "$REPO_ROOT")"
  fi
  info "Created $display_path"
}

upsert_env_value() {
  local file="$1"
  local key="$2"
  local value="$3"
  local tmp_file

  tmp_file="$(mktemp)"
  awk -v k="$key" -v v="$value" '
    BEGIN { done = 0 }
    $0 ~ ("^" k "=") {
      print k "=" v
      done = 1
      next
    }
    { print }
    END {
      if (!done) {
        print k "=" v
      }
    }
  ' "$file" >"$tmp_file"
  mv "$tmp_file" "$file"
}

ensure_command_with_packages() {
  local cmd="$1"
  shift
  local -a packages
  packages=("$@")

  if command_exists "$cmd"; then
    return
  fi

  info "Command '$cmd' is missing."
  if ! confirm "Install required package(s) for '$cmd' now?" "y"; then
    die "Cannot continue without '$cmd'."
  fi

  install_packages "${packages[@]}"

  if ! command_exists "$cmd"; then
    die "Failed to install '$cmd'."
  fi
}

ensure_uv() {
  if command_exists uv; then
    return
  fi

  info "uv is not installed."
  if ! confirm "Install uv using the official installer?" "y"; then
    die "Cannot continue without uv for local deployment."
  fi

  curl -LsSf https://astral.sh/uv/install.sh | sh
  export PATH="$HOME/.local/bin:$PATH"

  if ! command_exists uv; then
    die "uv was installed but is not available in PATH."
  fi
}

ensure_nodejs() {
  local node_major="0"
  local node_version=""

  if command_exists node; then
    node_version="$(node -v || true)"
    node_major="${node_version#v}"
    node_major="${node_major%%.*}"
    if [[ "$node_major" =~ ^[0-9]+$ ]] && ((node_major >= 22)) && command_exists npm; then
      return
    fi
  fi

  info "Node.js >= 22 is required for local deployment."
  if ! confirm "Install or upgrade Node.js now?" "y"; then
    die "Cannot continue without Node.js >= 22."
  fi

  if [[ "$PLATFORM" == "darwin" ]]; then
    brew upgrade node@22 2>/dev/null || brew install node@22
    if [[ -d "$(brew --prefix node@22 2>/dev/null)/bin" ]]; then
      export PATH="$(brew --prefix node@22)/bin:$PATH"
    fi
    if ! command_exists node || ! command_exists npm; then
      die 'Node.js/npm installation failed. Ensure Homebrew bin is in PATH (e.g. eval "$(brew shellenv)").'
    fi
    hash -r || true
    node_version="$(node -v || true)"
    node_major="${node_version#v}"
    node_major="${node_major%%.*}"
    if [[ ! "$node_major" =~ ^[0-9]+$ ]] || ((node_major < 22)); then
      die "Detected Node.js $node_version. Node.js >= 22 is required. Install with: brew install node@22 and ensure $(brew --prefix node@22 2>/dev/null || echo '/opt/homebrew/opt/node@22')/bin is in PATH."
    fi
    return
  fi

  if [[ "$PKG_MANAGER" != "apt" ]]; then
    die "Node.js auto-install is currently implemented for apt-based distros and macOS only. Install Node.js >= 22 manually, then rerun installer. Suggested command: $(install_command_hint "$PKG_MANAGER" nodejs npm)"
  fi

  install_packages ca-certificates curl gnupg
  curl -fsSL https://deb.nodesource.com/setup_22.x | as_root bash -
  install_packages nodejs

  if ! command_exists node || ! command_exists npm; then
    die "Node.js/npm installation failed."
  fi

  # Refresh command lookup + PATH after install (CI runners often have an older Node in PATH).
  hash -r || true
  if [[ -x /usr/bin/node ]]; then
    export PATH="/usr/bin:$PATH"
  fi

  node_version="$(node -v || true)"
  node_major="${node_version#v}"
  node_major="${node_major%%.*}"
  if [[ ! "$node_major" =~ ^[0-9]+$ ]] || ((node_major < 22)); then
    die "Detected Node.js $node_version. Node.js >= 22 is required."
  fi
}

ensure_docker() {
  if command_exists docker && docker compose version >/dev/null 2>&1; then
    return
  fi

  if [[ "$PLATFORM" == "darwin" ]]; then
    die "Docker and Docker Compose v2 are required on macOS. Install Docker Desktop from https://www.docker.com/products/docker-desktop/, start it, then re-run this script."
  fi

  info "Docker and Docker Compose v2 are required."
  if ! confirm "Install Docker tooling now?" "y"; then
    die "Cannot continue without Docker."
  fi

  install_packages docker.io
  if ! install_packages docker-compose-plugin; then
    warn "docker-compose-plugin unavailable; trying docker-compose package."
    install_packages docker-compose
  fi

  if command_exists systemctl; then
    as_root systemctl enable --now docker || warn "Could not enable/start docker service automatically."
  fi

  if ! command_exists docker; then
    die "Docker installation failed."
  fi

  if ! docker compose version >/dev/null 2>&1; then
    die "Docker Compose v2 is unavailable ('docker compose')."
  fi
}

docker_compose() {
  local tmp_file
  local rc=0

  if [[ "$DOCKER_USE_SUDO" -eq 1 ]]; then
    as_root docker compose "$@"
    return
  fi

  tmp_file="$(mktemp)"
  if docker compose "$@" 2> >(tee "$tmp_file" >&2); then
    rm -f "$tmp_file"
    return
  fi
  rc=$?

  if [[ "$(id -u)" -ne 0 ]] && command_exists sudo; then
    if grep -Eqi 'permission denied|docker.sock|cannot connect to the docker daemon' "$tmp_file"; then
      warn "Docker permission issue detected, retrying with sudo."
      DOCKER_USE_SUDO=1
      rm -f "$tmp_file"
      as_root docker compose "$@"
      return
    fi
  fi

  rm -f "$tmp_file"
  return "$rc"
}

wait_for_http() {
  local url="$1"
  local label="$2"
  local timeout_seconds="${3:-120}"
  local i

  for ((i = 1; i <= timeout_seconds; i++)); do
    if curl -fsS "$url" >/dev/null 2>&1; then
      info "$label is reachable at $url"
      return 0
    fi
    sleep 1
  done

  warn "Timed out waiting for $label at $url"
  return 1
}

start_local_services() {
  local backend_port="$1"
  local frontend_port="$2"

  mkdir -p "$LOG_DIR"

  info "Starting backend in background..."
  (
    cd "$REPO_ROOT/backend"
    nohup uv run uvicorn app.main:app --host 0.0.0.0 --port "$backend_port" >"$LOG_DIR/backend.log" 2>&1 &
    echo $! >"$LOG_DIR/backend.pid"
  )

  info "Starting frontend in background..."
  (
    cd "$REPO_ROOT/frontend"
    nohup npm run start -- --hostname 0.0.0.0 --port "$frontend_port" >"$LOG_DIR/frontend.log" 2>&1 &
    echo $! >"$LOG_DIR/frontend.pid"
  )
}

install_systemd_services() {
  local backend_port="$1"
  local frontend_port="$2"
  local systemd_user_dir
  systemd_user_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
  local units_dir="$REPO_ROOT/docs/deployment/systemd"

  if [[ "$REPO_ROOT" == *" "* ]]; then
    warn "REPO_ROOT must not contain spaces (systemd unit paths do not support it): $REPO_ROOT"
    return 1
  fi
  if [[ "$PLATFORM" != "linux" ]]; then
    info "Skipping systemd install (not Linux). For macOS run-at-boot see docs/deployment/README.md (launchd)."
    return 0
  fi
  if [[ ! -d "$units_dir" ]]; then
    warn "Systemd units dir not found: $units_dir"
    return 1
  fi
  for name in openclaw-mission-control-backend openclaw-mission-control-frontend openclaw-mission-control-rq-worker; do
    if [[ ! -f "$units_dir/$name.service" ]]; then
      warn "Unit file not found: $units_dir/$name.service"
      return 1
    fi
  done

  mkdir -p "$systemd_user_dir"
  for name in openclaw-mission-control-backend openclaw-mission-control-frontend openclaw-mission-control-rq-worker; do
    sed -e "s|REPO_ROOT|$REPO_ROOT|g" \
        -e "s|BACKEND_PORT|$backend_port|g" \
        -e "s|FRONTEND_PORT|$frontend_port|g" \
        "$units_dir/$name.service" > "$systemd_user_dir/$name.service"
    info "Installed $systemd_user_dir/$name.service"
  done
  if command_exists systemctl; then
    systemctl --user daemon-reload
    systemctl --user enable openclaw-mission-control-backend openclaw-mission-control-frontend openclaw-mission-control-rq-worker
    info "Systemd user units enabled. Start with: systemctl --user start openclaw-mission-control-backend openclaw-mission-control-frontend openclaw-mission-control-rq-worker"
  else
    warn "systemctl not found; units were copied but not enabled."
  fi
}

ensure_repo_layout() {
  [[ -f "$REPO_ROOT/Makefile" ]] || die "Run $SCRIPT_NAME from repository root."
  [[ -f "$REPO_ROOT/compose.yml" ]] || die "Missing compose.yml in repository root."
}

main() {
  local deployment_mode
  local public_host
  local backend_port
  local frontend_port
  local next_public_api_url
  local token_mode
  local local_auth_token
  local db_mode="docker"
  local database_url=""
  local start_services="yes"

  cd "$REPO_ROOT"
  ensure_repo_layout
  parse_args "$@"

  detect_platform
  if [[ "$PLATFORM" == "darwin" ]]; then
    info "Platform detected: darwin (macOS)"
  else
    info "Platform detected: linux ($LINUX_DISTRO)"
  fi

  if [[ -n "$FORCE_MODE" ]]; then
    deployment_mode="$FORCE_MODE"
  else
    deployment_mode="$(prompt_choice "Deployment mode" "docker" "docker" "local")"
  fi
  if ! is_one_of "$deployment_mode" "docker" "local"; then
    die "Invalid deployment mode: $deployment_mode (expected docker|local)"
  fi

  while true; do
    if [[ -n "$FORCE_BACKEND_PORT" ]]; then
      backend_port="$FORCE_BACKEND_PORT"
    else
      backend_port="$(prompt_with_default "Backend port" "8000")"
    fi
    is_valid_port "$backend_port" && break
    warn "Invalid backend port: $backend_port"
    FORCE_BACKEND_PORT=""
  done

  while true; do
    if [[ -n "$FORCE_FRONTEND_PORT" ]]; then
      frontend_port="$FORCE_FRONTEND_PORT"
    else
      frontend_port="$(prompt_with_default "Frontend port" "3000")"
    fi
    is_valid_port "$frontend_port" && break
    warn "Invalid frontend port: $frontend_port"
    FORCE_FRONTEND_PORT=""
  done

  if [[ -n "$FORCE_PUBLIC_HOST" ]]; then
    public_host="$FORCE_PUBLIC_HOST"
  else
    public_host="$(prompt_with_default "Public host/IP for browser access" "localhost")"
  fi
  if [[ -n "$FORCE_API_URL" ]]; then
    next_public_api_url="$FORCE_API_URL"
  else
    next_public_api_url="$(prompt_with_default "Public API URL used by frontend" "http://$public_host:$backend_port")"
  fi

  if [[ -n "$FORCE_TOKEN_MODE" ]]; then
    token_mode="$FORCE_TOKEN_MODE"
  else
    token_mode="$(prompt_choice "LOCAL_AUTH_TOKEN" "generate" "generate" "manual")"
  fi
  if ! is_one_of "$token_mode" "generate" "manual"; then
    die "Invalid token mode: $token_mode (expected generate|manual)"
  fi
  if [[ "$token_mode" == "manual" ]]; then
    if [[ -n "$FORCE_LOCAL_AUTH_TOKEN" ]]; then
      local_auth_token="$FORCE_LOCAL_AUTH_TOKEN"
    else
      local_auth_token="$(prompt_secret "Enter LOCAL_AUTH_TOKEN (min 50 chars)")"
    fi
    if [[ "${#local_auth_token}" -lt 50 ]]; then
      die "LOCAL_AUTH_TOKEN must be at least 50 characters."
    fi
  else
    local_auth_token="$(generate_token)"
    info "Generated LOCAL_AUTH_TOKEN."
  fi

  if [[ "$deployment_mode" == "local" ]]; then
    if [[ -n "$FORCE_DB_MODE" ]]; then
      db_mode="$FORCE_DB_MODE"
    else
      db_mode="$(prompt_choice "Database source for local deployment" "docker" "docker" "external")"
    fi
    if ! is_one_of "$db_mode" "docker" "external"; then
      die "Invalid db mode: $db_mode (expected docker|external)"
    fi
    if [[ "$db_mode" == "external" ]]; then
      if [[ -n "$FORCE_DATABASE_URL" ]]; then
        database_url="$FORCE_DATABASE_URL"
      else
        database_url="$(prompt_with_default "External DATABASE_URL" "postgresql+psycopg://postgres:postgres@localhost:5432/mission_control")"
      fi
    fi
    if [[ -n "$FORCE_START_SERVICES" ]]; then
      start_services="$FORCE_START_SERVICES"
    else
      start_services="$(prompt_choice "Start backend/frontend processes automatically after bootstrap" "yes" "yes" "no")"
    fi
    if ! is_one_of "$start_services" "yes" "no"; then
      die "Invalid start-services value: $start_services (expected yes|no)"
    fi
  fi

  ensure_command_with_packages curl curl
  ensure_command_with_packages git git
  ensure_command_with_packages make make
  ensure_command_with_packages openssl openssl

  if [[ "$deployment_mode" == "docker" || "$db_mode" == "docker" ]]; then
    ensure_docker
  fi

  if [[ "$deployment_mode" == "local" ]]; then
    ensure_uv
    ensure_nodejs
    info "Ensuring Python 3.12 is available through uv..."
    uv python install 3.12
  fi

  ensure_file_from_example "$REPO_ROOT/.env" "$REPO_ROOT/.env.example"
  upsert_env_value "$REPO_ROOT/.env" "BACKEND_PORT" "$backend_port"
  upsert_env_value "$REPO_ROOT/.env" "FRONTEND_PORT" "$frontend_port"
  upsert_env_value "$REPO_ROOT/.env" "AUTH_MODE" "local"
  upsert_env_value "$REPO_ROOT/.env" "LOCAL_AUTH_TOKEN" "$local_auth_token"
  upsert_env_value "$REPO_ROOT/.env" "NEXT_PUBLIC_API_URL" "$next_public_api_url"
  upsert_env_value "$REPO_ROOT/.env" "BASE_URL" "http://$public_host:$backend_port"
  upsert_env_value "$REPO_ROOT/.env" "CORS_ORIGINS" "http://$public_host:$frontend_port"

  if [[ "$deployment_mode" == "docker" ]]; then
    ensure_file_from_example "$REPO_ROOT/backend/.env" "$REPO_ROOT/backend/.env.example"

    upsert_env_value "$REPO_ROOT/.env" "DB_AUTO_MIGRATE" "true"

    info "Starting production-like Docker stack..."
    docker_compose -f compose.yml --env-file .env up -d --build

    wait_for_http "http://127.0.0.1:$backend_port/healthz" "Backend" 180 || true
    wait_for_http "http://127.0.0.1:$frontend_port" "Frontend" 180 || true

    cat <<SUMMARY

Bootstrap complete (Docker mode).

Access URLs:
- Frontend: http://$public_host:$frontend_port
- Backend:  http://$public_host:$backend_port/healthz

Auth:
- AUTH_MODE=local
- LOCAL_AUTH_TOKEN=$local_auth_token

Stop stack:
  docker compose -f compose.yml --env-file .env down
SUMMARY
    return
  fi

  ensure_file_from_example "$REPO_ROOT/backend/.env" "$REPO_ROOT/backend/.env.example"
  ensure_file_from_example "$REPO_ROOT/frontend/.env" "$REPO_ROOT/frontend/.env.example"

  if [[ "$db_mode" == "docker" ]]; then
    upsert_env_value "$REPO_ROOT/.env" "POSTGRES_DB" "mission_control"
    upsert_env_value "$REPO_ROOT/.env" "POSTGRES_USER" "postgres"
    upsert_env_value "$REPO_ROOT/.env" "POSTGRES_PASSWORD" "postgres"
    upsert_env_value "$REPO_ROOT/.env" "POSTGRES_PORT" "5432"

    database_url="postgresql+psycopg://postgres:postgres@localhost:5432/mission_control"

    info "Starting PostgreSQL via Docker..."
    docker_compose -f compose.yml --env-file .env up -d db
  fi

  upsert_env_value "$REPO_ROOT/backend/.env" "ENVIRONMENT" "prod"
  upsert_env_value "$REPO_ROOT/backend/.env" "DATABASE_URL" "$database_url"
  upsert_env_value "$REPO_ROOT/backend/.env" "AUTH_MODE" "local"
  upsert_env_value "$REPO_ROOT/backend/.env" "LOCAL_AUTH_TOKEN" "$local_auth_token"
  upsert_env_value "$REPO_ROOT/backend/.env" "CORS_ORIGINS" "http://$public_host:$frontend_port"
  upsert_env_value "$REPO_ROOT/backend/.env" "BASE_URL" "http://$public_host:$backend_port"
  upsert_env_value "$REPO_ROOT/backend/.env" "DB_AUTO_MIGRATE" "false"

  upsert_env_value "$REPO_ROOT/frontend/.env" "NEXT_PUBLIC_API_URL" "$next_public_api_url"
  upsert_env_value "$REPO_ROOT/frontend/.env" "NEXT_PUBLIC_AUTH_MODE" "local"

  info "Installing backend/frontend dependencies..."
  make setup

  info "Applying database migrations..."
  make backend-migrate

  info "Building frontend production bundle..."
  make frontend-build

  if [[ "$start_services" == "yes" ]]; then
    start_local_services "$backend_port" "$frontend_port"
    wait_for_http "http://127.0.0.1:$backend_port/healthz" "Backend" 120 || true
    wait_for_http "http://127.0.0.1:$frontend_port" "Frontend" 120 || true
  fi

  if [[ -n "$FORCE_INSTALL_SERVICE" ]]; then
    if ! install_systemd_services "$backend_port" "$frontend_port"; then
      warn "Systemd service install failed; see errors above."
      die "Cannot continue when --install-service was requested and install failed."
    fi
    if [[ "$PLATFORM" == "linux" ]]; then
      info "Run at boot: systemd user units were installed and enabled. Start with: systemctl --user start openclaw-mission-control-backend openclaw-mission-control-frontend openclaw-mission-control-rq-worker"
    fi
  fi

  cat <<SUMMARY

Bootstrap complete (Local mode).

Access URLs:
- Frontend: http://$public_host:$frontend_port
- Backend:  http://$public_host:$backend_port/healthz

Auth:
- AUTH_MODE=local
- LOCAL_AUTH_TOKEN=$local_auth_token

If services were started by this script, logs are under:
- $LOG_DIR/backend.log
- $LOG_DIR/frontend.log

Stop local background services:
  kill "\$(cat $LOG_DIR/backend.pid)" "\$(cat $LOG_DIR/frontend.pid)"
SUMMARY
}

main "$@"
