#!/usr/bin/env bash
# Siklab Core installer — https://siklabcore.com
#
# The broker key below is not a licence and grants no entitlement: it lets this script pull
# the release while the source repo stays private. Siklab still runs a 3-day trial and then
# needs a purchased key. It is the same value already present in every installed copy.
: "${SIKLAB_BROKER_URL:=https://siklab-broker.siklab-bakunawa.workers.dev}"
: "${SIKLAB_BROKER_KEY:=9200646cf9ea0715627ed183e3f321fa86be36b07c54e144}"
export SIKLAB_BROKER_URL SIKLAB_BROKER_KEY
#===============================================================================
# Siklab Core — one-command installer
#
#   curl -fsSL <install-url>/install.sh | bash
#
# Installs everything in one shot: downloads the self-contained Siklab Core
# bundle (the API binary has Bun embedded — NO node/bun/git/build tools needed),
# drops it in ~/.siklab-core, starts it, and prints the link to open in your
# browser to finish setup (connect your LLM + set a password on first login).
#
# PRIVATE now / PUBLIC later — same script, no edits:
#   • While the release repo is PRIVATE, provide a GitHub token once:
#         SIKLAB_TOKEN=ghp_xxxx  bash install.sh
#     (or just have the `gh` CLI logged in — the script will use it).
#   • When the repo is made PUBLIC, the plain one-liner works with no token.
#
# Overridable env:
#   SIKLAB_REPO    owner/repo of the release      (default below)
#   SIKLAB_BRANCH  branch to install              (default: main)
#   SIKLAB_DEST    install dir                     (default: ~/.siklab-core)
#   SIKLAB_HOME    data dir                        (default: ~/.siklab)
#   SIKLAB_TOKEN   GitHub token (private repo)     (or GITHUB_TOKEN, or `gh`)
#   SIKLAB_NO_START=1   install but don't launch
#===============================================================================
set -euo pipefail

SIKLAB_REPO="${SIKLAB_REPO:-bakunawa-siklabcore/siklab-core-production}"
SIKLAB_BRANCH="${SIKLAB_BRANCH:-main}"
SIKLAB_DEST="${SIKLAB_DEST:-$HOME/.siklab-core}"
SIKLAB_HOME="${SIKLAB_HOME:-$HOME/.siklab}"

RED='\033[0;31m'; GRN='\033[0;32m'; YLW='\033[0;33m'; CYN='\033[0;36m'; B='\033[1m'; X='\033[0m'
say(){  printf "${CYN}▶${X} %s\n" "$1"; }
ok(){   printf "${GRN}✓${X} %s\n" "$1"; }
warn(){ printf "${YLW}!${X} %s\n" "$1"; }
die(){  printf "\n${RED}${B}ERROR:${X} %s\n" "$1" >&2; exit 1; }

printf "\n${B}  Siklab Core${X} — installer\n  ${CYN}%s${X}\n\n" "$SIKLAB_REPO"

#--- 1. preflight: PREREQUISITES (declared manifest) --------------------------
# Siklab Core dependency manifest (full doc: deploy/PREREQUISITES.md):
#
#   CORE — required to RUN the platform:
#     • curl, tar          (ship with macOS)
#     • macOS Darwin arm64/x64
#
#   BUILD TOOLCHAIN — required for the agents to BUILD/TEST software in workspaces
#   (Apolaki build + Sidapa QA gates). A minimal box has none of these:
#     • a JS runtime + package manager — Bun (recommended) OR Node.js >=18 + npm
#     • Playwright + Chromium browser  — for real-browser E2E QA
#
#   AI ENGINE — bring-your-own, required to USE the agents at all:
#     • a configured engine: a logged-in CLI (claude/codex/…) OR an API key,
#       set in Settings → LLM. Siklab ships NO engine credential.
#
# The installer HARD-REQUIRES the core deps, and CHECKS the build toolchain (reporting
# exactly what's missing). It never silently installs system packages. To auto-provision
# the build toolchain, re-run with:  SIKLAB_INSTALL_BUILD_DEPS=1

# core (hard requirements)
# Preinstalled on macOS; on a minimal Ubuntu/WSL image they may not be — say how to get them
# rather than just failing (that message is the whole difference between "broken" and "one apt away").
_apt_hint(){ [ -f /etc/debian_version ] && printf '  Install with:  sudo apt-get install -y %s\n' "$1"; }
command -v curl >/dev/null 2>&1 || { _apt_hint curl; die "curl is required."; }
command -v tar  >/dev/null 2>&1 || { _apt_hint tar;  die "tar is required."; }
command -v python3 >/dev/null 2>&1 || { warn "python3 not found — start.sh falls back to the default port."; _apt_hint python3; }

# build toolchain (checked, not required to install — agents need it to build software)
JS_RUNTIME=""
command -v bun  >/dev/null 2>&1 && JS_RUNTIME="bun $(bun --version 2>/dev/null)"
[ -z "$JS_RUNTIME" ] && command -v node >/dev/null 2>&1 && JS_RUNTIME="node $(node --version 2>/dev/null)"
if [ -n "$JS_RUNTIME" ]; then
  ok "Core prerequisites present · build toolchain: JS runtime found ($JS_RUNTIME)"
elif [ "${SIKLAB_INSTALL_BUILD_DEPS:-1}" = "1" ]; then
  # Default now matches step 4b: the toolchain (JS runtime + Chromium) is provisioned unless the
  # operator explicitly opts out. This message and that step must agree — when they disagreed,
  # the install said "will be provisioned" only if you had already asked for it.
  ok "Core prerequisites present · build toolchain + browser will be provisioned automatically"
else
  ok "Core prerequisites present"
  warn "No JS runtime (Bun/Node), and provisioning is off (SIKLAB_INSTALL_BUILD_DEPS=0)."
  warn "Agent BUILD and browser-backed QA gates will be DISABLED."
  echo "       Let Siklab provision it:   bash install.sh          (the default)"
  echo "       …or install Bun yourself:  curl -fsSL https://bun.sh/install | bash"
fi

#--- 2. platform detect: pick the matching binary -----------------------------
OS=$(uname -s); ARCH=$(uname -m)
# WSL reports Linux but needs its own touches (no systemd by default, browser lives on Windows).
IS_WSL=0
if [ "$OS" = "Linux" ] && grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=1; fi
case "$OS-$ARCH" in
  Darwin-arm64)             BIN="siklab-api-darwin-arm64" ;;
  Darwin-x86_64)            BIN="siklab-api-darwin-x64"  ;;
  Darwin-*)                 die "Unrecognized Mac architecture: $ARCH" ;;
  # Linux names match start.sh's own mapping — keep the two in sync.
  Linux-x86_64)             BIN="siklab-api-linux-x64" ;;
  Linux-aarch64|Linux-arm64) BIN="siklab-api-linux-arm64" ;;
  Linux-*)                  die "Unsupported Linux architecture: $ARCH (x86_64 and aarch64 are supported)." ;;
  *) die "Siklab Core supports macOS and Linux (incl. WSL). Detected: $OS-$ARCH." ;;
esac
[ "$IS_WSL" = "1" ] && ok "Platform $OS/$ARCH (WSL) → $BIN" || ok "Platform $OS/$ARCH → $BIN"
if [ "$OS" = "Linux" ]; then
  # Honest, once, up front. The platform BUILDS fine on Linux; the browser-backed QA steps (mockup
  # preview, UAT, design critique) need Chromium's system libraries, which macOS never needs because
  # it drives system Chrome. Without them a build still produces the app but cannot verify it — and
  # silently-unverified is exactly the failure mode this product exists to prevent.
  warn "Linux/WSL support is BETA — builds work; browser-backed QA needs Chromium system libraries."
  # install-build-deps.sh now attempts this itself and prints an exact command if it needs root,
  # so do NOT pre-empt it here with an npx line. npx is not on a machine that has no Node, and
  # Siklab provisions Bun — the first beta tester had neither npm nor npx, which made the one
  # instruction he was given impossible to follow.
  [ -f /etc/debian_version ] && echo "       Siklab will install those for you; if it needs your password it will say so."
fi

#--- 3. resolve auth (private now, public later) ------------------------------
TOKEN="${SIKLAB_TOKEN:-${GITHUB_TOKEN:-}}"
if [ -z "$TOKEN" ] && command -v gh >/dev/null 2>&1; then
  TOKEN="$(gh auth token 2>/dev/null || true)"
  [ -n "$TOKEN" ] && say "Using GitHub token from the gh CLI"
fi

tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
TARBALL="$tmp/siklab.tgz"
API="https://api.github.com/repos/$SIKLAB_REPO/tarball/$SIKLAB_BRANCH"
PUBLIC="https://codeload.github.com/$SIKLAB_REPO/tar.gz/refs/heads/$SIKLAB_BRANCH"

BROKER="${SIKLAB_BROKER_URL:-}"
say "Downloading Siklab Core ($SIKLAB_BRANCH)…"
if [ -n "$BROKER" ]; then
  # TOKENLESS: pull the tarball through the Siklab broker (it holds the GitHub token).
  curl -fsSL -H "X-Siklab-Key: ${SIKLAB_BROKER_KEY:-}" "${BROKER%/}/tarball" -o "$TARBALL" \
    || die "Download from broker (${BROKER%/}/tarball) failed — is the broker up / key correct?"
elif [ -n "$TOKEN" ]; then
  curl -fsSL --location-trusted -H "Authorization: token $TOKEN" -H "Accept: application/vnd.github+json" \
    "$API" -o "$TARBALL" || die "Download failed (check the token has access to $SIKLAB_REPO)."
else
  curl -fsSL "$PUBLIC" -o "$TARBALL" 2>/dev/null \
    || die "Download failed. Private repo — set SIKLAB_BROKER_URL (recommended) or a token:\n      SIKLAB_BROKER_URL=https://…  bash install.sh\n      SIKLAB_TOKEN=ghp_xxx        bash install.sh   (or: gh auth login)"
fi
ok "Downloaded"

#--- 4. install (preserve data dir; idempotent re-runs = update) --------------
# Detect update vs fresh BEFORE we touch anything: an existing DB means the user
# already has an account + data, so this is an update (don't tell them to set a password).
IS_UPDATE=0; [ -f "$SIKLAB_HOME/data/siklab.db" ] && IS_UPDATE=1
say "Installing to $SIKLAB_DEST"
# The port whose process must be stopped before the swap. This was hardcoded to 3200, which is
# wrong for any install that is not on the default port: updating an install running on :3210
# killed whatever happened to own :3200 (a different environment entirely) and left the REAL
# instance running while its directory was renamed out from under it. Honour SIKLAB_PORT, which is
# already how start.sh and the launchd agents say which port this install owns.
EXISTING_PORT="${SIKLAB_PORT:-3200}"
# Atomic, rollback-safe update: extract + VERIFY the new bundle in a staging dir FIRST, then
# swap it in by rename. A corrupt or truncated download can NEVER leave a half-wiped, broken
# install — the current version keeps running until the new one is validated, and is restored
# if the swap fails. (The data dir $SIKLAB_HOME is never touched by any of this.)
STAGE="${SIKLAB_DEST}.new"; BACKUP="${SIKLAB_DEST}.old"
rm -rf "$STAGE" "$BACKUP"; mkdir -p "$STAGE"
# GitHub tarballs nest everything under one <owner>-<repo>-<sha>/ dir — strip it.
LC_ALL=C tar -xzf "$TARBALL" -C "$STAGE" --strip-components=1 \
  || { rm -rf "$STAGE"; die "Extract failed (corrupt/truncated download) — your current install was NOT touched."; }
# Binaries ship GZIPPED — GitHub hard-rejects any committed file over 100 MB, and the Linux targets
# are 100-107 MB raw (see deploy/build-release.sh). Decompress only the one this platform runs, then
# drop the other archives so the install dir does not carry ~100 MB of binaries for other machines.
if [ ! -f "$STAGE/$BIN" ] && [ -f "$STAGE/$BIN.gz" ]; then
  command -v gunzip >/dev/null 2>&1 || { rm -rf "$STAGE"; die "gunzip is required to unpack the binary — your current install was NOT touched."; }
  gunzip -c "$STAGE/$BIN.gz" > "$STAGE/$BIN" \
    || { rm -rf "$STAGE"; die "Could not decompress $BIN.gz (corrupt download) — your current install was NOT touched."; }
fi
rm -f "$STAGE"/siklab-api-*.gz 2>/dev/null || true
chmod +x "$STAGE/$BIN" "$STAGE/start.sh" 2>/dev/null || true
# We ship an ad-hoc-signed binary (no Apple Developer ID — deliberate, see deploy/build-release.sh).
# That runs fine on macOS INCLUDING Apple Silicon, which only requires *a* signature, not a trusted
# one. The single thing that would break it is the com.apple.quarantine attribute, which Gatekeeper
# honours by refusing anything not Developer-ID-signed + notarized. curl never sets that attribute,
# so the documented `curl | bash` path is unaffected — but a user who grabs the tarball in a BROWSER
# first, or pipes it through Mail/AirDrop, gets quarantined bytes and a "cannot be opened" wall.
# Strip it here so every delivery route behaves the same. No-op on Linux and on clean downloads.
if [ "$(uname -s)" = "Darwin" ] && command -v xattr >/dev/null 2>&1; then
  xattr -dr com.apple.quarantine "$STAGE" 2>/dev/null || true
fi
[ -x "$STAGE/$BIN" ] || { rm -rf "$STAGE"; die "Downloaded bundle has no $BIN (nor $BIN.gz) — current install left intact."; }
# New bundle validated → stop the running instance and swap atomically, keeping the old version
# as a rollback until the swap succeeds.
# Stop whatever owns our port before the swap. lsof is standard on macOS but is NOT installed on a
# minimal Ubuntu/WSL image, where this silently did nothing and the swap raced a live process.
# Try lsof, then fuser (procps/psmisc), then ss+pkill — whichever exists.
if command -v lsof >/dev/null 2>&1; then
  lsof -ti:"$EXISTING_PORT" 2>/dev/null | xargs kill -9 2>/dev/null || true
elif command -v fuser >/dev/null 2>&1; then
  fuser -k -n tcp "$EXISTING_PORT" >/dev/null 2>&1 || true
else
  pkill -9 -f "$BIN" >/dev/null 2>&1 || true
fi
[ -e "$SIKLAB_DEST" ] && mv "$SIKLAB_DEST" "$BACKUP"
if mv "$STAGE" "$SIKLAB_DEST"; then
  rm -rf "$BACKUP"
else
  [ -e "$BACKUP" ] && mv "$BACKUP" "$SIKLAB_DEST"   # swap failed — restore previous version
  die "Install swap failed — restored the previous version (no data touched)."
fi
mkdir -p "$SIKLAB_HOME"
# 0700 — this directory holds everything private to the install: the credential encryption key
# (.env, which decrypts stored provider keys and connector tokens) and the database. It was created
# with the umask default (0755), so any local user could traverse in and read both. Locking the
# DIRECTORY is what makes the whole data dir safe, rather than chasing each file's mode.
#
# chmod, not `mkdir -m`: the mode argument only applies when a directory is CREATED, so on every
# update — where $SIKLAB_HOME already exists — it would silently do nothing. (Same trap as
# writeFileSync's `mode`, which is how the key came to be world-readable in the first place.)
chmod 700 "$SIKLAB_HOME" 2>/dev/null || warn "Could not tighten $SIKLAB_HOME to 0700 — check it is not world-readable."
ok "Installed (data dir: $SIKLAB_HOME, 0700)"

#--- 4a. manifest-driven prerequisite report (reads deploy/prerequisites.json) ---
# The declared dependency manifest (package.json-style) is the single source of truth.
# Read it here and check each declared dependency's `check` command, printing a status
# report. Requires python3 (a core optional dep); the inline core preflight above already
# guaranteed the hard requirements, so this is a richer report, not a gate.
MANIFEST="$SIKLAB_DEST/deploy/prerequisites.json"
if command -v python3 >/dev/null 2>&1 && [ -f "$MANIFEST" ]; then
  say "Verifying declared prerequisites (prerequisites.json)…"
  python3 - "$MANIFEST" <<'PY' || true
import json, sys, subprocess
m = json.load(open(sys.argv[1]))
def chk(cmd):
    try: return subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
    except Exception: return False
for group, g in m.get("prerequisites", {}).items():
    req = "required" if g.get("required") else "optional"
    print(f"    · {group} ({req})")
    deps = {**g.get("dependencies", {}), **g.get("optionalDependencies", {})}
    for name, d in deps.items():
        if isinstance(d, dict) and d.get("check"):
            print(f"        {'OK ' if chk(d['check']) else 'MISSING'}  {name} {d.get('version','')}")
    eng = g.get("engines", {})
    runtimes = [k for k in eng if k not in ("policy",)]
    if runtimes:
        found = [r for r in runtimes if chk(f"command -v {r}")]
        print(f"        {'OK ' if found else 'MISSING'}  runtime ({'/'.join(runtimes)}) {'→ '+','.join(found) if found else ''}")
PY
fi

#--- 4b. build toolchain provision (DEFAULT ON; opt out with SIKLAB_INSTALL_BUILD_DEPS=0) ----
# This was OPT-IN, and that was wrong. Siklab's whole claim is that it VERIFIES what it builds —
# Sidapa's QA, the mockup preview, the design critique and UAT all drive a real browser. An
# install without one still builds apps and can no longer check any of them, which is precisely
# the failure this product exists to prevent.
#
# It stayed invisible because macOS falls back to system Chrome, which most developer machines
# have. On a machine without Chrome, every browser-backed step failed separately at the moment it
# was needed and the owner just saw "Sidapa can't test", over and over, with no way to learn why.
#
# Still DETACHED: the Chromium download is ~150MB and must never block the install from finishing.
# The server starts immediately, reports at boot whether a browser is present, and the toolchain
# lands in the background. Log: $SIKLAB_DEST/build-deps.log.
if [ "${SIKLAB_INSTALL_BUILD_DEPS:-1}" = "1" ]; then
  if [ -f "$SIKLAB_DEST/deploy/install-build-deps.sh" ]; then
    say "Pulling build toolchain + browser in background (log: $SIKLAB_DEST/build-deps.log)…"
    ( nohup bash "$SIKLAB_DEST/deploy/install-build-deps.sh" "$SIKLAB_DEST" >"$SIKLAB_DEST/build-deps.log" 2>&1 & ) </dev/null
    ok "Toolchain pull started — Siklab is usable now; browser-backed QA turns on when it lands"
  else
    warn "install-build-deps.sh missing from release — browser-backed QA will be DISABLED."
    # Name a runtime that is actually on the machine. npx assumes Node, which Siklab never
    # installs — it provisions Bun. Falling back to bunx keeps this runnable on a box with no npm.
    if command -v bunx >/dev/null 2>&1;   then warn "Fix once with:  bunx playwright install chromium"
    elif command -v npx >/dev/null 2>&1;  then warn "Fix once with:  npx playwright install chromium"
    else warn "Fix once by installing Bun, then:  bunx playwright install chromium"
         warn "  curl -fsSL https://bun.sh/install | bash"
    fi
  fi
else
  warn "Skipping toolchain (SIKLAB_INSTALL_BUILD_DEPS=0) — browser-backed QA will be DISABLED."
  warn "Apps will still build, but nothing can verify them in a browser."
fi

#--- 5. launch + wait for health ---------------------------------------------
if [ "${SIKLAB_NO_START:-0}" = "1" ]; then
  ok "Skipping launch (SIKLAB_NO_START=1).  Start later: $SIKLAB_DEST/start.sh"
  exit 0
fi

say "Starting Siklab Core…"
LOG="$SIKLAB_DEST/siklab.log"

# Prefer a LaunchAgent: launchd owns the process, so the installer returns
# immediately (no held-open terminal/SSH channel) AND the app auto-starts on
# login and respawns on crash. Falls back to a detached nohup if there's no
# GUI (Aqua) session to bootstrap into.
UID_NUM="$(id -u)"
LABEL="com.siklab.core"
LA_DIR="$HOME/Library/LaunchAgents"
PLIST="$LA_DIR/$LABEL.plist"
start_via_nohup(){
  ( cd "$SIKLAB_DEST" && SIKLAB_HOME="$SIKLAB_HOME" nohup ./start.sh >"$LOG" 2>&1 </dev/null & )
  disown 2>/dev/null || true
}
if command -v launchctl >/dev/null 2>&1; then
  mkdir -p "$LA_DIR"
  cat > "$PLIST" <<PL
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>$LABEL</string>
  <key>ProgramArguments</key>
  <array><string>$SIKLAB_DEST/start.sh</string></array>
  <key>WorkingDirectory</key><string>$SIKLAB_DEST</string>
  <key>EnvironmentVariables</key>
  <dict><key>SIKLAB_HOME</key><string>$SIKLAB_HOME</string><key>PATH</key><string>$HOME/.bun/bin:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string><key>SIKLAB_BROKER_URL</key><string>${SIKLAB_BROKER_URL:-}</string><key>SIKLAB_BROKER_KEY</key><string>${SIKLAB_BROKER_KEY:-}</string></dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>$LOG</string>
  <key>StandardErrorPath</key><string>$LOG</string>
</dict>
</plist>
PL
  plutil -lint "$PLIST" >/dev/null 2>&1 || warn "plist lint warning (continuing)"
  launchctl bootout "gui/$UID_NUM/$LABEL" 2>/dev/null || true
  if launchctl bootstrap "gui/$UID_NUM" "$PLIST" 2>/dev/null; then
    ok "Installed as a background service (auto-starts on login, restarts on crash)"
  else
    warn "Could not register launchd service (no GUI session?) — starting directly"
    rm -f "$PLIST"
    start_via_nohup
  fi
elif systemctl --user show-environment >/dev/null 2>&1; then
  # Linux equivalent of the LaunchAgent. Capability-checked, not just `command -v systemctl`:
  # WSL ships the binary but `--user` fails unless systemd is enabled, and a blind call there
  # would leave the install with no running server at all.
  SD_DIR="$HOME/.config/systemd/user"; mkdir -p "$SD_DIR"
  cat > "$SD_DIR/siklab-core.service" <<SD
[Unit]
Description=Siklab Core
After=network-online.target

[Service]
Type=simple
WorkingDirectory=$SIKLAB_DEST
ExecStart=$SIKLAB_DEST/start.sh
Environment=SIKLAB_HOME=$SIKLAB_HOME
Environment=PATH=$HOME/.bun/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
Environment=SIKLAB_BROKER_URL=${SIKLAB_BROKER_URL:-}
Environment=SIKLAB_BROKER_KEY=${SIKLAB_BROKER_KEY:-}
Restart=always
RestartSec=3
StandardOutput=append:$LOG
StandardError=append:$LOG

[Install]
WantedBy=default.target
SD
  systemctl --user daemon-reload >/dev/null 2>&1 || true
  if systemctl --user enable --now siklab-core.service >/dev/null 2>&1; then
    ok "Installed as a systemd user service (auto-starts on login, restarts on crash)"
    # Without lingering the service stops when the last session closes — tell them, don't sudo for them.
    loginctl show-user "$(id -un)" 2>/dev/null | grep -q "Linger=yes" \
      || echo "       Keep it running after logout:  sudo loginctl enable-linger $(id -un)"
  else
    warn "systemd user service didn't start — running directly instead"
    start_via_nohup
  fi
else
  # No launchd, no usable systemd (typical WSL): a detached process is the honest best effort.
  start_via_nohup
  [ "$IS_WSL" = "1" ] && echo "       WSL has no systemd by default — Siklab won't auto-restart. Re-run start.sh after a reboot."
fi

#--- 5b. self-update timer (PULL model: the install checks + pulls its own updates) ---
# OFF BY DEFAULT. It used to be on, so an install updated ITSELF in the background and restarted
# mid-session — the owner would be working, the server would swap under them, and the next click
# landed on a login screen with no explanation. Their words: "if updating is this invasive, at
# least have the user do the update. not a surprising shit like this."
#
# An update here is not a silent patch: it stops the server, swaps the install directory, and
# restarts. That is disruptive enough to be the owner's call, made when they are ready. The app
# still CHECKS and tells them an update is available (Settings -> Updates); pulling it is a button.
#
# Opt back in with SIKLAB_AUTOUPDATE=1 for the old unattended behaviour.
if command -v launchctl >/dev/null 2>&1 && [ "${SIKLAB_AUTOUPDATE:-0}" = "1" ] && [ -f "$SIKLAB_DEST/deploy/update.sh" ]; then
  ULABEL="com.siklab.updater"; UPLIST="$LA_DIR/$ULABEL.plist"
  cat > "$UPLIST" <<PL
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>$ULABEL</string>
  <key>ProgramArguments</key><array><string>/bin/bash</string><string>$SIKLAB_DEST/deploy/update.sh</string></array>
  <key>EnvironmentVariables</key><dict><key>SIKLAB_DEST</key><string>$SIKLAB_DEST</string><key>SIKLAB_HOME</key><string>$SIKLAB_HOME</string><key>SIKLAB_BROKER_URL</key><string>${SIKLAB_BROKER_URL:-}</string><key>SIKLAB_BROKER_KEY</key><string>${SIKLAB_BROKER_KEY:-}</string></dict>
  <key>StartInterval</key><integer>21600</integer>
  <key>RunAtLoad</key><true/>
</dict></plist>
PL
  plutil -lint "$UPLIST" >/dev/null 2>&1 || warn "updater plist lint warning (continuing)"
  launchctl bootout "gui/$UID_NUM/$ULABEL" 2>/dev/null || true
  launchctl bootstrap "gui/$UID_NUM" "$UPLIST" 2>/dev/null \
    && ok "Automatic updates enabled (you asked for it with SIKLAB_AUTOUPDATE=1)" \
    || warn "Could not register auto-update timer (no GUI session?)"
else
  # ACTIVELY REMOVE a previously-registered timer. Flipping the default to opt-in does nothing for
  # the machines that already have com.siklab.updater loaded from an earlier install — they would
  # keep updating themselves in the background, which is the exact surprise this change exists to
  # stop. Running the installer once now disarms it.
  if command -v launchctl >/dev/null 2>&1; then
    ULABEL="com.siklab.updater"
    if launchctl print "gui/$UID_NUM/$ULABEL" >/dev/null 2>&1; then
      launchctl bootout "gui/$UID_NUM/$ULABEL" 2>/dev/null || true
      rm -f "$LA_DIR/$ULABEL.plist" 2>/dev/null || true
      ok "Turned OFF background auto-updates — you decide when to update (Settings -> Updates)"
    fi
  fi
fi

# Use the port this install actually runs on — NOT a hardcoded 3200. This one line drove three
# separate annoyances for anyone on a different port: the health loop below polled :3200 and sat
# there for 30s before reporting a failure while the server was up and fine; the success message
# printed the wrong address; and the browser auto-opened to a dead one, so the first thing a user
# saw after a clean install was a connection error. Same bug the port-kill step above already had
# (see EXISTING_PORT) — it was fixed there and missed here.
URL="http://localhost:${EXISTING_PORT}"
for i in $(seq 1 30); do
  curl -fsS -m2 "$URL/health" >/dev/null 2>&1 && { HEALTHY=1; break; }
  sleep 1
done

echo
if [ "${HEALTHY:-0}" = "1" ]; then
  ok "Siklab Core is running"
  # After an UPDATE, send the browser to a page that explains what just happened. Landing on a
  # bare login screen after the server restarted under you reads as "set up again" — which is how
  # a preserved install with a valid session got mistaken for a wiped one.
  OPEN_URL="$URL"
  if [ "$IS_UPDATE" = "1" ]; then
    # Wait for AUTH to be readable, not just /health. The server answers /health before the DB
    # migration finishes, and until auth_setup_state is readable /api/auth/status returns
    # "uninitialized" — so opening the browser on the health signal alone raced the boot and the
    # owner landed on a setup-looking screen on a fully configured install. Poll for a real state.
    for _i in $(seq 1 20); do
      curl -fsS -m2 "$URL/api/auth/status" 2>/dev/null | grep -q '"state":"\(active\|bootstrap_pending\|recovery_pending\)"' && break
      sleep 1
    done
    NEWVER=$(curl -fsS -m3 "$URL/api/health" 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
    OPEN_URL="$URL/start?updated=${NEWVER:-1}"
  fi
  if [ "$IS_UPDATE" = "1" ]; then
    printf "\n  ${B}Updated — your data is preserved.${X}\n  ${CYN}${B}%s${X}\n\n  Log in with your existing password. (Nothing to set up again.)\n\n" "$URL"
  else
    printf "\n  ${B}Open your browser to finish setup:${X}\n  ${CYN}${B}%s${X}\n\n  Just set a password — that's it. (The first-run key is handled automatically.)\n\n" "$URL"
  fi
  # Open the browser on whatever this is. macOS: open. Linux desktop: xdg-open. WSL: hand it to
  # Windows (WSL2 forwards localhost, so the Windows browser reaches the server fine).
  if [ "$IS_WSL" = "1" ]; then
    { command -v wslview >/dev/null 2>&1 && wslview "$OPEN_URL"; } >/dev/null 2>&1 \
      || { command -v explorer.exe >/dev/null 2>&1 && explorer.exe "$OPEN_URL"; } >/dev/null 2>&1 || true
  elif command -v open >/dev/null 2>&1; then
    open "$OPEN_URL" >/dev/null 2>&1 || true
  elif command -v xdg-open >/dev/null 2>&1; then
    xdg-open "$OPEN_URL" >/dev/null 2>&1 || true
  fi
else
  warn "Started, but it isn't answering on $URL yet — give it a few seconds."
  printf "  Open: ${CYN}%s${X}\n  Logs: %s\n\n" "$URL" "$LOG"
fi
