#!/bin/bash

# omarchy:summary=Run OpenClaw's setup wizard the way Omarchy needs it: in the terminal, installing the gateway as a user service, and returning when it is done.

# Not bare `openclaw onboard`: as of 2026.9.1 that is the guided flow, which
# ends by running a foreground gateway and handing off to a browser tab, and
# never returns. --install-daemon keeps it to the classic wizard (minimal
# prompts with --flow quickstart) that installs the gateway service, and
# --skip-ui drops its closing Control UI/TUI prompt since whoever called this
# opens one next.
#
# That classic wizard has one wrinkle: with --skip-ui it prints "Onboarding
# complete" and then never exits. Upstream only calls exit(0) when it launched
# the TUI, and the model sign-in leaves an open socket that keeps the process
# alive otherwise. So this script watches for the gateway to answer and then
# stops the wizard. That is safe because every prompt in the quickstart flow
# runs before the gateway service is installed and reachable: by the time the
# dashboard answers, only the closing notes are left.

set -uo pipefail

wizard=(openclaw onboard --flow quickstart --install-daemon --skip-ui)
config=$HOME/.openclaw/openclaw.json
settle_seconds=${OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS:-3}
# Counted from the moment the config exists, i.e. once the wizard has applied
# setup; the prompts before that take as long as the user takes.
gateway_timeout=${OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT:-180}

gateway_answers() {
  timeout 10 openclaw dashboard --json 2>/dev/null | jq -e '.ok == true' >/dev/null 2>&1
}

# A gateway already answering means OpenClaw is set up, and this run must not
# read its own success off state that predates it: the wizard's repair pass
# would be stopped mid-prompt the moment the watcher looked. Nothing to do.
if [[ -f $config ]] && gateway_answers; then
  echo "OpenClaw is already set up and its gateway is running." >&2
  echo "To change providers or settings, run: openclaw onboard --classic" >&2
  exit 0
fi

# Marks when this run began, so a config left behind by an earlier, incomplete
# setup is not mistaken for this run having applied its own: the gateway
# deadline below must not start ticking while the user is still at prompts.
started=$(mktemp)
trap 'rm -f "$started"' EXIT

# Backgrounded so this script can watch it, but with the terminal kept as its
# stdin (bash would otherwise hand a background job /dev/null). Job control is
# off in a script, so it stays in the terminal's foreground process group and
# reads from it freely. Ctrl-C does not reach it directly, though: bash starts
# async children with SIGINT ignored when job control is off, so the INT trap
# below is what turns Ctrl-C into the wizard's exit.
"${wizard[@]}" <&0 &
wizard_pid=$!

stop_wizard() {
  kill -TERM "$wizard_pid" 2>/dev/null || true
}
# Any signal at this script, whether Ctrl-C from the terminal or a kill aimed
# at its pid alone, takes the wizard down with it rather than leaving it
# running unwatched.
trap 'stop_wizard' INT TERM HUP

# Whether this run has applied setup: the config exists and is not older than
# the run itself.
config_applied() {
  [[ -f $config && ! $started -nt $config ]]
}

# Whether this run's gateway is up: setup applied, and the process answering
# on the gateway's port is the main process of the service the wizard
# installs. Not the dashboard alone: with no config on disk `openclaw
# dashboard --json` still probes the default loopback port, so a gateway left
# behind by something else (an earlier guided onboarding's foreground
# gateway, say) would read as this run's success while the user is still at
# the first prompt. And not the unit being active either: its Type=simple
# counts it active from the fork, before it has found the port taken by such
# an orphan, and the app would then open on the orphan rather than the
# service this run installed.
gateway_ready() {
  config_applied || return 1
  local json port listener main_pid
  json=$(timeout 10 openclaw dashboard --json 2>/dev/null) || return 1
  jq -e '.ok == true' <<<"$json" >/dev/null 2>&1 || return 1
  port=$(jq -r '.port // empty' <<<"$json" 2>/dev/null)
  [[ -n $port ]] || return 1
  listener=$(ss -ltnpH "sport = :$port" 2>/dev/null | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
  main_pid=$(systemctl --user show -p MainPID --value openclaw-gateway.service 2>/dev/null)
  [[ -n $listener && -n $main_pid && $main_pid != 0 && $listener == "$main_pid" ]]
}

stopped=false
timed_out=false
config_seen_at=
while kill -0 "$wizard_pid" 2>/dev/null; do
  sleep 2
  if gateway_ready; then
    # Let the outro finish printing, then end the process the wizard leaves
    # running.
    sleep "$settle_seconds"
    stop_wizard
    stopped=true
    break
  fi
  config_applied || continue
  : "${config_seen_at:=$SECONDS}"
  if (( SECONDS - config_seen_at >= gateway_timeout )); then
    # Setup was applied but the service never came up (port taken, unit
    # failing, ...): the wizard would sit in its never-exiting state forever,
    # and so would whoever is waiting on this script.
    stop_wizard
    timed_out=true
    break
  fi
done

wait "$wizard_pid"
rc=$?

if [[ $timed_out == true ]]; then
  echo "OpenClaw's gateway did not come up within ${gateway_timeout}s of setup finishing." >&2
  echo "Check it with: openclaw gateway status" >&2
  exit 1
fi
# A wizard stopped here after the gateway came up did its job. One that
# exited on its own, including a user who chose "Skip for now" (no config,
# non-zero), keeps its own exit code.
[[ $stopped == true ]] && rc=0
exit "$rc"
