#!/usr/bin/env bash
# AiMote Server — one-click installer for macOS and Linux
#
# Two launch modes:
#   1. From a repo clone:     bash install.sh
#   2. From the internet:     curl -fsSL https://aimote.net/install.sh | bash
#
# Both end with an installed, runnable server at:
#   ~/.aimote/server/          (compiled JS + runtime deps)
#   ~/.aimote/bin/cloudflared  (auto-downloaded, only needed for aimote/cloudflare modes)
#   ~/.aimote/aimote           (launcher script; symlinked to /usr/local/bin if possible)
#   ~/.aimote/aimote-server    (legacy alias symlink; kept so old muscle memory and
#                               existing plists / systemd units keep working)

set -euo pipefail

# ── Pretty output ────────────────────────────────────────────────────────────
BOLD="\033[1m"; DIM="\033[2m"; CYAN="\033[36m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m"
info()    { echo -e "  ${CYAN}▸${RESET} $*"; }
success() { echo -e "  ${GREEN}✓${RESET} $*"; }
warn()    { echo -e "  ${YELLOW}⚠${RESET} $*"; }
die()     { echo -e "  ${RED}✗${RESET} $*" >&2; exit 1; }

# ── Paths ────────────────────────────────────────────────────────────────────
AIMOTE_DIR="$HOME/.aimote"
INSTALL_DIR="$AIMOTE_DIR/server"
BIN_DIR="$AIMOTE_DIR/bin"
LAUNCHER="$AIMOTE_DIR/aimote"
LEGACY_LAUNCHER="$AIMOTE_DIR/aimote-server"
SYMLINK="/usr/local/bin/aimote"
LEGACY_SYMLINK="/usr/local/bin/aimote-server"

# Tarball filename is part of our release contract (version.json references
# it, every existing install's auto-updater fetches it) — do NOT rename.
RELEASE_URL_TARBALL="https://aimote.net/aimote-server.tar.gz"

# ── Mirror base URLs (overridden below when China is detected) ────────────────
NODE_DIST_BASE="https://nodejs.org/dist"
NPM_REGISTRY="https://registry.npmjs.org"
GH_BASE="https://github.com"

echo ""
echo -e "  ${BOLD}${CYAN}╔══════════════════════════════════════════╗${RESET}"
echo -e "  ${BOLD}${CYAN}║     AiMote Server — Installer            ║${RESET}"
echo -e "  ${BOLD}${CYAN}╚══════════════════════════════════════════╝${RESET}"
echo ""

# ── Network detection (China mirror switching) ────────────────────────────────
# Probe the nodejs.org distribution endpoint with a 3-second timeout.
# Connections to nodejs.org / github.com / npmjs.org are routinely slow or
# unreliable from mainland China. When the probe fails we switch all download
# URLs to domestic mirrors: npmmirror.com for Node + npm, ghfast.top for GitHub.
detect_network() {
  info "Detecting network…"
  if curl -fsS --max-time 3 -o /dev/null "https://nodejs.org/dist/index.json" 2>/dev/null; then
    success "Network: global"
    return
  fi
  NODE_DIST_BASE="https://npmmirror.com/mirrors/node"
  NPM_REGISTRY="https://registry.npmmirror.com"
  GH_BASE="https://ghfast.top/https://github.com"
  export NPM_CONFIG_REGISTRY="$NPM_REGISTRY"
  warn "检测到国内网络，已自动切换镜像源 ↓"
  info "  Node  : $NODE_DIST_BASE"
  info "  npm   : $NPM_REGISTRY"
  info "  GitHub: $GH_BASE"
}
detect_network

# ── 1. Node.js + npm ─────────────────────────────────────────────────────────
# Strategy: prefer the system Node if it's >= 20. Otherwise download an
# official Node.js portable build into ~/.aimote/bin/node-portable/ and use
# that one. This keeps the install self-contained — no sudo, no apt, no
# polluting the user's PATH outside the launcher's scope.
mkdir -p "$BIN_DIR"

NODE_REQUIRED_MAJOR=20
NODE_PORTABLE_VER=20.19.0  # latest LTS at release time; bump in release.sh
PORTABLE_DIR="$BIN_DIR/node-portable"
USE_PORTABLE_NODE=0

check_system_node() {
  command -v node &>/dev/null || return 1
  local v
  v=$(node --version 2>/dev/null | sed 's/v//')
  local maj=${v%%.*}
  [ -n "$maj" ] && [ "$maj" -ge "$NODE_REQUIRED_MAJOR" ]
}

# Picks the official Node.js download URL for this OS/arch combo.
# Returns: "URL|TARBALL_NAME|SUBDIR" via stdout.
node_portable_url() {
  local os arch base="${NODE_DIST_BASE}/v${NODE_PORTABLE_VER}"
  os=$(uname -s); arch=$(uname -m)
  case "$os" in
    Darwin) os=darwin ;;
    Linux)  os=linux ;;
    *) return 1 ;;
  esac
  case "$arch" in
    arm64|aarch64) arch=arm64 ;;
    x86_64|amd64)  arch=x64 ;;
    armv7l|armhf)  arch=armv7l ;;
    *) return 1 ;;
  esac
  local stem="node-v${NODE_PORTABLE_VER}-${os}-${arch}"
  echo "${base}/${stem}.tar.gz|${stem}.tar.gz|${stem}"
}

install_portable_node() {
  local spec url tarname subdir
  spec=$(node_portable_url) || { warn "Unsupported platform $(uname -s)/$(uname -m). Install Node ${NODE_REQUIRED_MAJOR}+ manually from https://nodejs.org and re-run."; return 1; }
  url=${spec%%|*}; rest=${spec#*|}
  tarname=${rest%%|*}; subdir=${rest#*|}

  info "Downloading Node.js v${NODE_PORTABLE_VER} (~30 MB, no sudo needed)…"
  local tmp; tmp=$(mktemp -t aimote-node.XXXXXX.tar.gz)
  if ! curl -fsSL -o "$tmp" "$url"; then
    rm -f "$tmp"
    die "Failed to download Node.js from $url. Check your network and re-run."
  fi
  rm -rf "$PORTABLE_DIR"
  mkdir -p "$PORTABLE_DIR"
  if ! tar -xzf "$tmp" -C "$PORTABLE_DIR" --strip-components=1; then
    rm -f "$tmp"
    die "Failed to extract Node.js archive."
  fi
  rm -f "$tmp"
  if [ ! -x "$PORTABLE_DIR/bin/node" ]; then
    die "Node.js extraction succeeded but bin/node is missing. Aborting."
  fi
  USE_PORTABLE_NODE=1
  # Make the portable Node + npm available for the rest of THIS script too.
  export PATH="$PORTABLE_DIR/bin:$PATH"
  success "Node.js $("$PORTABLE_DIR/bin/node" --version) installed at ${PORTABLE_DIR}"
}

info "Checking Node.js…"
if check_system_node; then
  success "Node.js $(node --version | sed 's/v//') (system)"
else
  if command -v node &>/dev/null; then
    warn "Node.js $(node --version | sed 's/v//') is too old (need ${NODE_REQUIRED_MAJOR}+) — falling back to a portable copy."
  else
    warn "Node.js not found on PATH — installing a portable copy (no sudo)."
  fi
  install_portable_node
fi
# Resolve the absolute path now (while our shell PATH is intact) so the
# launcher can use it directly — launchd's minimal environment won't have
# Homebrew / nvm / volta paths and would fail with "node: not found".
if [ "$USE_PORTABLE_NODE" -eq 1 ]; then
  NODE_BIN="$PORTABLE_DIR/bin/node"
else
  NODE_BIN="$(command -v node)"
fi

info "Checking npm…"
command -v npm &>/dev/null || die "npm missing even after Node install — that shouldn't happen. File a bug."
success "npm $(npm --version)"

# ── 2. Obtain the server source ──────────────────────────────────────────────
# Branch A: script lives in a repo clone → use the local `server/` dir
# Branch B: script was piped through curl → download the release tarball
# When piped through curl, BASH_SOURCE[0] isn't set. `set -u` makes that an
# error, so we defensively default to empty and fall through to download mode.
SOURCE_PATH="${BASH_SOURCE[0]:-}"
if [ -n "$SOURCE_PATH" ] && [ -f "$SOURCE_PATH" ]; then
  SCRIPT_DIR="$(cd "$(dirname "$SOURCE_PATH")" && pwd)"
else
  SCRIPT_DIR=""
fi
LOCAL_SERVER="${SCRIPT_DIR:+$SCRIPT_DIR/server}"

mkdir -p "$INSTALL_DIR" "$BIN_DIR"

if [ -n "$LOCAL_SERVER" ] && [ -f "$LOCAL_SERVER/package.json" ]; then
  info "Building from local repo clone at $LOCAL_SERVER…"
  (cd "$LOCAL_SERVER" && npm install --silent >/dev/null)
  (cd "$LOCAL_SERVER" && npm run build >/dev/null)
  cp -r "$LOCAL_SERVER/dist" "$INSTALL_DIR/"
  cp "$LOCAL_SERVER/package.json" "$INSTALL_DIR/"
  cp "$LOCAL_SERVER/package-lock.json" "$INSTALL_DIR/" 2>/dev/null || true
  success "Built from local source"
else
  info "Downloading latest server release…"
  TMP_TGZ="$(mktemp -t aimote-server.XXXXXX.tar.gz)"
  # Cache-buster: Cloudflare Pages defaults binary assets to a 4-hour
  # edge cache, so a fresh install in the minutes after a deploy can
  # otherwise pick up a stale tarball whose sha256 doesn't match the
  # current version.json. The query string forces a CDN miss and goes
  # straight to origin.
  if ! curl -fsSL -o "$TMP_TGZ" "${RELEASE_URL_TARBALL}?ts=$(date +%s)"; then
    die "Failed to download $RELEASE_URL_TARBALL. Check network / site availability."
  fi
  success "Downloaded $(ls -lh "$TMP_TGZ" | awk '{print $5}')"
  tar -xzf "$TMP_TGZ" -C "$INSTALL_DIR"
  rm -f "$TMP_TGZ"
  success "Extracted to $INSTALL_DIR"
fi

# ── 3. Install production deps ───────────────────────────────────────────────
info "Installing runtime dependencies (node-pty, ws, etc)…"
(cd "$INSTALL_DIR" && npm install --omit=dev --silent 2>/dev/null || npm install --production --silent)
success "Runtime dependencies installed"

# ── 3.5 Codex CLI ────────────────────────────────────────────────────────────
# Required for Codex sessions from mobile. On macOS the Codex Desktop app
# includes its own codex binary, so the server's resolver often finds one
# even without this step — but an explicit `codex` on PATH is the most
# reliable path across Linux and Mac both, and keeps behaviour consistent
# with the Windows installer (which *must* install via npm because the
# MS-Store codex.exe is sandboxed and unusable as a subprocess).
info "Checking Codex CLI…"
if command -v codex >/dev/null 2>&1; then
  codex_ver=$(codex --version 2>/dev/null | head -1 || true)
  success "Codex CLI: ${codex_ver:-found on PATH}"
else
  warn "Codex CLI not found on PATH — installing @openai/codex globally (~20-40 s)…"
  # `--no-audit` skips a slow roundtrip on constrained networks; `--silent`
  # keeps the terminal clean. Errors are still surfaced.
  if npm install -g @openai/codex --no-audit --silent 2>/dev/null; then
    if command -v codex >/dev/null 2>&1; then
      success "Codex CLI installed: $(codex --version 2>/dev/null | head -1 || echo 'installed')"
    else
      warn "npm completed but \`codex\` not on PATH — prefix may be unusual. Server resolver may still find it."
    fi
  else
    warn "Codex install failed. Retry later with:  npm install -g @openai/codex"
    info "  (You may need:  sudo npm install -g @openai/codex  depending on prefix perms.)"
  fi
fi

# ── 4. node-pty spawn-helper perm fix (darwin-arm64 bug) ─────────────────────
PTY_PREBUILDS="$INSTALL_DIR/node_modules/node-pty/prebuilds"
if [ -d "$PTY_PREBUILDS" ]; then
  find "$PTY_PREBUILDS" -type f \( -name 'spawn-helper' -o -name '*.node' \) \
    -exec chmod 0755 {} + 2>/dev/null || true
fi

# ── 5. Pre-download cloudflared (background, never blocks install) ───────────
# Default mode is `aimote` (unified discovery via provision.aimote.net — phone
# polls Worker, which serves trycloudflare URL while cloudflared is healthy
# and wss://relay.aimote.net otherwise). The phone never blocks on
# cloudflared, so neither should the install. Pre-fetching the binary is a
# cheap optimisation — without it the runtime's lazy ensureCloudflaredBinary()
# fires on the first tunnel launch attempt, which is the same outcome modulo
# a few seconds. Either way, the relay path works immediately.
install_cloudflared() {
  local OS ARCH URL ARCHIVE=0 TMP
  OS=$(uname -s)
  ARCH=$(uname -m)

  local CF_GH="${GH_BASE}/cloudflare/cloudflared/releases/latest/download"
  case "$OS-$ARCH" in
    Darwin-arm64)    URL="${CF_GH}/cloudflared-darwin-arm64.tgz";  ARCHIVE=1 ;;
    Darwin-x86_64)   URL="${CF_GH}/cloudflared-darwin-amd64.tgz";  ARCHIVE=1 ;;
    Linux-x86_64)    URL="${CF_GH}/cloudflared-linux-amd64" ;;
    Linux-aarch64|Linux-arm64) URL="${CF_GH}/cloudflared-linux-arm64" ;;
    Linux-armv7l|Linux-armhf)  URL="${CF_GH}/cloudflared-linux-arm" ;;
    *) warn "Unknown platform $OS-$ARCH — skipping cloudflared pre-install. Server will try at runtime."; return 0 ;;
  esac

  [ -x "$BIN_DIR/cloudflared" ] && { info "cloudflared already present"; return 0; }

  info "Downloading cloudflared ($OS/$ARCH)…"
  TMP="$(mktemp -t cloudflared.XXXXXX)"
  if ! curl -fsSL -o "$TMP" "$URL"; then
    warn "cloudflared download failed — server will retry at first launch."
    return 0
  fi

  if [ "$ARCHIVE" -eq 1 ]; then
    tar -xzf "$TMP" -C "$BIN_DIR" cloudflared
  else
    mv "$TMP" "$BIN_DIR/cloudflared"
  fi
  rm -f "$TMP" 2>/dev/null || true
  chmod 0755 "$BIN_DIR/cloudflared"
  success "cloudflared $("$BIN_DIR/cloudflared" --version 2>/dev/null | head -1 | awk '{print $3}')"
}

# Default to `aimote` mode (matches setup.ts non-interactive default).
# Legacy `relay` collapses to `aimote` in setup.ts, so treat the env var the
# same way here. `local` mode is the only one that genuinely doesn't need
# cloudflared at all — skip the background fetch in that case.
_setup_mode="${AIMOTE_AUTO_SETUP_MODE:-aimote}"
if [ "$_setup_mode" = "local" ]; then
  info "LAN-only mode — cloudflared not needed, skipping download"
elif [ -x "$BIN_DIR/cloudflared" ]; then
  info "cloudflared already present"
else
  # Background fetch: install.sh never waits on a 30 MB GitHub download.
  # On CN networks where api.github.com release-asset CDN is slow / black-
  # holed, foreground install_cloudflared used to add minutes to the install
  # time, masking the fact that the rest of the install had already finished.
  # Now we kick it off and move on. Server's runtime ensureCloudflaredBinary()
  # is the safety net — it fetches if missing on first tunnel launch.
  info "Downloading cloudflared in background (install continues immediately)…"
  ( install_cloudflared > "$INSTALL_DIR/.cloudflared-install.log" 2>&1 || true ) &
  CFD_PID=$!
  # Don't `wait` — installation must return control to caller without blocking.
  info "  background pid: $CFD_PID — log: $INSTALL_DIR/.cloudflared-install.log"
fi

# ── 6. Launcher script ───────────────────────────────────────────────────────
# PATH order: portable Node (if installed) → AiMote bin (cloudflared) → system PATH
# `node` resolution: prefer portable, fall back to system. Either way `npm`
# uses the same Node, so node-pty's prebuilds stay matched.
cat > "$LAUNCHER" <<LAUNCHER_EOF
#!/usr/bin/env bash
# NODE_BIN is resolved at install time so launchd (which has a minimal PATH
# that excludes Homebrew / nvm / volta) can always find the right binary.
export PATH="$BIN_DIR:\$PATH"
exec "$NODE_BIN" "$INSTALL_DIR/dist/index.js" "\$@"
LAUNCHER_EOF
chmod +x "$LAUNCHER"
# Legacy alias — keeps `aimote-server …` working for users whose fingers
# or scripts remember the old name. Also keeps pre-existing launchd /
# systemd units that reference the old path happy during the transition.
ln -sf "$LAUNCHER" "$LEGACY_LAUNCHER"
success "Launcher installed at $LAUNCHER (legacy alias: $LEGACY_LAUNCHER)"

# ── 7. Make `aimote` available on PATH ──────────────────────────────────────
# We aggressively link into EVERY writable bin dir under both the new
# `aimote` name and the legacy `aimote-server` name. Then we always patch
# the user's shell rc so new terminals work even when none of the standard
# bin dirs are on PATH yet.
#
# Candidate bin dirs (in preference order):
#   1. /opt/homebrew/bin  — Apple Silicon brew; usually on PATH by default
#   2. /usr/local/bin     — Intel brew / Linux w/ brew; almost always on PATH
#   3. ~/.local/bin       — guaranteed-writable; we `mkdir -p` it first
#
# If none of those are on the current shell's PATH, we also rc-patch so a
# fresh terminal finds the command. The FINAL printed instructions always
# include the absolute path form (`~/.aimote/aimote`), which works RIGHT
# NOW in the current shell regardless of PATH state — that's the "fix" for
# new users who couldn't invoke `aimote-server` on macOS without first
# opening a new terminal.
install_launcher_on_path() {
  local -a linked_dirs=()

  # Best-effort: ~/.local may be root-owned on some systems (created by sudo tools).
  # The loop below checks writability before linking, so failure here is safe.
  mkdir -p "$HOME/.local/bin" 2>/dev/null || true

  for dir in /opt/homebrew/bin /usr/local/bin "$HOME/.local/bin"; do
    [ -d "$dir" ] || mkdir -p "$dir" 2>/dev/null || true
    if [ -w "$dir" ]; then
      ln -sf "$LAUNCHER" "$dir/aimote"          2>/dev/null || true
      ln -sf "$LAUNCHER" "$dir/aimote-server"   2>/dev/null || true  # legacy
      linked_dirs+=("$dir")
    fi
  done

  # If we still couldn't link anywhere (extremely unusual — /Users/$USER
  # is always writable), try passwordless sudo for /usr/local/bin.
  if [ ${#linked_dirs[@]} -eq 0 ] && sudo -n true 2>/dev/null; then
    sudo ln -sf "$LAUNCHER" "$SYMLINK" 2>/dev/null || true
    sudo ln -sf "$LAUNCHER" "$LEGACY_SYMLINK" 2>/dev/null || true
    linked_dirs+=("/usr/local/bin")
  fi

  # Is ANY of the dirs we linked into already on PATH?
  local on_path=""
  for dir in "${linked_dirs[@]}"; do
    if echo ":$PATH:" | grep -q ":$dir:"; then
      on_path="$dir"
      break
    fi
  done

  if [ -n "$on_path" ]; then
    success "\`aimote\` ready on PATH (linked via $on_path)"
  elif [ ${#linked_dirs[@]} -gt 0 ]; then
    success "\`aimote\` linked into: ${linked_dirs[*]}"
    info   "…but none are on your current shell PATH — patching shell rc too."
  else
    warn "Could not create any bin-dir symlinks — relying on shell rc + \$HOME/.aimote on PATH."
  fi

  # Always patch the rc so new terminals Just Work regardless of which
  # bin-dir succeeded. The block is idempotent via markers.
  local rcfile
  case "$(basename "${SHELL:-}")" in
    zsh)  rcfile="$HOME/.zshrc" ;;
    bash) rcfile="$HOME/.bashrc"
          [ -f "$HOME/.bash_profile" ] && rcfile="$HOME/.bash_profile" ;;
    fish) rcfile="$HOME/.config/fish/config.fish" ;;
    *)    rcfile="$HOME/.profile" ;;
  esac
  mkdir -p "$(dirname "$rcfile")"
  touch "$rcfile"
  if ! grep -q '# >>> aimote PATH >>>' "$rcfile"; then
    cat >> "$rcfile" <<'RC_EOF'

# >>> aimote PATH >>>
# Added by install.sh so `aimote` is runnable from any shell.
# Remove this block (markers included) to opt out.
export PATH="$HOME/.aimote:$HOME/.local/bin:$PATH"
# <<< aimote PATH <<<
RC_EOF
    success "Patched $rcfile so new shells find \`aimote\`"
  else
    info   "$rcfile already has the aimote PATH block"
  fi
  # Export so the final printed instructions below can reference $rcfile.
  export AIMOTE_RCFILE="$rcfile"
}
install_launcher_on_path

# ── 8. Auto-start on login + start now ──────────────────────────────────────
# Without auto-start, the user has to manually run the launcher every reboot
# and after a fresh install they don't know what to do next. We register a
# LaunchAgent (macOS) or a systemd --user unit (Linux), then load it so the
# server starts immediately too.

LOG_FILE="$AIMOTE_DIR/server.log"

install_autostart_macos() {
  local plist="$HOME/Library/LaunchAgents/com.aimote.server.plist"
  mkdir -p "$(dirname "$plist")"
  cat > "$plist" <<PLIST_EOF
<?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>com.aimote.server</string>
  <key>ProgramArguments</key><array>
    <string>$LAUNCHER</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>$LOG_FILE</string>
  <key>StandardErrorPath</key><string>$LOG_FILE</string>
  <key>WorkingDirectory</key><string>$HOME</string>
  <key>EnvironmentVariables</key><dict>
    <key>HOME</key><string>$HOME</string>
    <key>PATH</key><string>$(dirname "$NODE_BIN"):$BIN_DIR:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
  </dict>
</dict>
</plist>
PLIST_EOF
  # Reload defensively (unload first if already loaded)
  launchctl unload "$plist" 2>/dev/null || true
  if launchctl load "$plist" 2>/dev/null; then
    success "Auto-start at login: enabled (launchd)"
    return 0
  fi
  warn "launchctl load failed — autostart disabled. Run $LAUNCHER manually."
  return 1
}

install_autostart_linux() {
  if ! command -v systemctl &>/dev/null; then
    warn "systemd not found — autostart skipped. Run $LAUNCHER manually."
    return 1
  fi
  local svc="$HOME/.config/systemd/user/aimote.service"
  mkdir -p "$(dirname "$svc")"
  cat > "$svc" <<SVC_EOF
[Unit]
Description=AiMote Server
After=network-online.target

[Service]
Type=simple
ExecStart=$LAUNCHER
Restart=on-failure
RestartSec=10
StandardOutput=append:$LOG_FILE
StandardError=append:$LOG_FILE

[Install]
WantedBy=default.target
SVC_EOF
  systemctl --user daemon-reload 2>/dev/null || true
  if systemctl --user enable --now aimote.service 2>/dev/null; then
    success "Auto-start at login: enabled (systemd --user)"
    info "For boot-time start without login, run: sudo loginctl enable-linger \$USER"
    return 0
  fi
  warn "systemctl --user enable failed — autostart disabled. Run $LAUNCHER manually."
  return 1
}

info "Setting up auto-start…"
: > "$LOG_FILE"  # truncate so we can grep without reading old runs
case "$(uname -s)" in
  Darwin) install_autostart_macos ;;
  Linux)  install_autostart_linux ;;
  *)      warn "Auto-start not supported on $(uname -s). Run $LAUNCHER manually." ;;
esac

# ── 9. Wait for server to be reachable + show connect details ───────────────
info "Waiting for server to come online (this can take 10–30 s on first run while the tunnel is provisioned)…"
DOMAIN_LINE=""
for i in $(seq 1 60); do
  if [ -s "$LOG_FILE" ] && grep -q "Ready. Waiting for connections" "$LOG_FILE" 2>/dev/null; then
    DOMAIN_LINE=$(grep -oE 'aimote://connect[^ ]*' "$LOG_FILE" | tail -1)
    break
  fi
  sleep 1
done

QR_URL=$(grep -oE 'http://127\.0\.0\.1:[0-9]+' "$LOG_FILE" 2>/dev/null | tail -1 || true)
PIN_VAL=$(grep -oE 'PIN: *\\?[a-zA-Z0-9]+' "$LOG_FILE" 2>/dev/null | tail -1 | grep -oE '[a-zA-Z0-9]+$' || true)

# ── 9b. Belt-and-suspenders update sweep ─────────────────────────────────────
# Even with cache-busting on the tarball download above, there's still a
# small race between us building the release and Pages publishing the new
# version.json. If the tarball we just installed isn't at the newest
# commit the manifest advertises, fetch + apply once so the user lands
# on the truly current version.
update_sweep() {
  local manifest cur new
  manifest=$(curl -fsSL --max-time 8 "https://aimote.net/version.json?ts=$(date +%s)" 2>/dev/null) || return 0
  new=$(printf '%s' "$manifest" | grep -oE '"commit"[^,]*' | head -1 | grep -oE '[0-9a-f]{6,40}' | head -1)
  cur=$(grep -oE '"commit"[^,]*' "$INSTALL_DIR/RELEASE_INFO.json" 2>/dev/null | head -1 | grep -oE '[0-9a-f]{6,40}' | head -1)
  if [ -z "$new" ] || [ "$new" = "$cur" ]; then
    return 0  # already current, or manifest unreadable — silent
  fi
  info "Manifest advertises commit ${new}; we just installed ${cur:-unknown}. Pulling…"
  local tmp; tmp=$(mktemp -t aimote-sweep.XXXXXX.tar.gz)
  if curl -fsSL --max-time 60 -o "$tmp" "https://aimote.net/aimote-server.tar.gz?ts=$(date +%s)"; then
    tar -xzf "$tmp" -C "$INSTALL_DIR" 2>/dev/null && success "Refreshed install to commit ${new}"
    rm -f "$tmp"
    # Restart the service so the new code starts running.
    case "$(uname -s)" in
      Darwin) launchctl kickstart -k "gui/$(id -u)/com.aimote.server" 2>/dev/null || true ;;
      Linux)  systemctl --user restart aimote.service 2>/dev/null || true ;;
    esac
  else
    rm -f "$tmp"
    warn "Could not fetch tarball for the update sweep — will be picked up by the next 6 h auto-update cycle."
  fi
}
update_sweep

echo ""
echo -e "  ${BOLD}${GREEN}Installation complete!${RESET}"
echo ""

if [ -n "$DOMAIN_LINE" ]; then
  echo -e "  ${BOLD}Server is running. Open the AiMote app and scan the QR (or connect manually):${RESET}"
  echo ""
  echo -e "  ${CYAN}  ${DOMAIN_LINE}${RESET}"
  [ -n "$QR_URL" ] && echo -e "  ${BOLD}QR page:${RESET}    ${CYAN}${QR_URL}${RESET}"
  # Open the QR page in the user's default browser for convenience
  if [ -n "$QR_URL" ]; then
    case "$(uname -s)" in
      Darwin) (open "$QR_URL" 2>/dev/null &) ;;
      Linux)  command -v xdg-open >/dev/null && (xdg-open "$QR_URL" 2>/dev/null &) ;;
    esac
  fi
else
  warn "Server didn't print a 'Ready' line within 60s — check ${LOG_FILE} for errors."
fi

echo ""
echo -e "  ${BOLD}Useful commands${RESET} — copy-paste directly into ${BOLD}this${RESET} terminal:"
echo ""
# Lead with the absolute-path form because it works right now regardless
# of whether the shell has picked up the new PATH. After the user opens a
# new terminal (or sources their rc file), the plain `aimote …` form
# below works identically.
echo -e "    ${CYAN}~/.aimote/aimote status${RESET}     ${BOLD}# show PID, port, PIN, QR URL${RESET}"
echo -e "    ${CYAN}~/.aimote/aimote qr${RESET}         ${BOLD}# reopen the QR page${RESET}"
echo -e "    ${CYAN}~/.aimote/aimote logs${RESET}       ${BOLD}# tail the server log${RESET}"
echo -e "    ${CYAN}~/.aimote/aimote update${RESET}     ${BOLD}# pull the newest release${RESET}"
echo -e "    ${CYAN}~/.aimote/aimote help${RESET}       ${BOLD}# full command list${RESET}"
echo ""
echo -e "  ${BOLD}In a new terminal${RESET} (or after: ${CYAN}source ${AIMOTE_RCFILE:-$HOME/.zshrc}${RESET}) the short form works:"
echo -e "    ${CYAN}aimote status${RESET}   ${CYAN}aimote qr${RESET}   ${CYAN}aimote logs${RESET}   ${CYAN}aimote help${RESET}"
echo ""
case "$(uname -s)" in
  Darwin)
    echo -e "  ${BOLD}Service management:${RESET}"
    echo -e "    Stop:       ${CYAN}launchctl unload ~/Library/LaunchAgents/com.aimote.server.plist${RESET}"
    echo -e "    Disable:    ${CYAN}rm ~/Library/LaunchAgents/com.aimote.server.plist${RESET}"
    ;;
  Linux)
    echo -e "  ${BOLD}Service management:${RESET}"
    echo -e "    Stop:       ${CYAN}systemctl --user stop aimote${RESET}"
    echo -e "    Disable:    ${CYAN}systemctl --user disable aimote${RESET}"
    ;;
esac
echo -e "    Log file:   ${CYAN}$LOG_FILE${RESET}"
echo ""

# ── Claude auth reminder ─────────────────────────────────────────────────────
# The server runs under launchd/systemd and never inherits the shell env that
# Claude Desktop writes CLAUDE_CODE_OAUTH_TOKEN into. Without a stashed copy
# every spawned `claude -p` session starts with "Not logged in · run /login".
# If the user has already done this (upgrade/reinstall) stay silent; only
# print the banner on a fresh install where the file is absent.
if [ ! -f "$AIMOTE_DIR/claude-auth.env" ]; then
  echo ""
  echo -e "  ${BOLD}${YELLOW}┌─────────────────────────────────────────────────────────┐${RESET}"
  echo -e "  ${BOLD}${YELLOW}│  One more step if you use Claude Code sessions          │${RESET}"
  echo -e "  ${BOLD}${YELLOW}└─────────────────────────────────────────────────────────┘${RESET}"
  echo ""
  echo -e "  The server runs in the background and cannot inherit the"
  echo -e "  Claude auth token from your shell. Without it, every Claude"
  echo -e "  session will show ${RED}\"Not logged in · Please run /login\"${RESET}."
  echo ""
  echo -e "  Run this ${BOLD}once${RESET} now to stash the token:"
  echo ""
  echo -e "    ${CYAN}~/.aimote/aimote claude-auth${RESET}"
  echo ""
  echo -e "  ${DIM}(Requires Claude Desktop to be installed and logged in.${RESET}"
  echo -e "  ${DIM} After that, the token refreshes automatically.)${RESET}"
  echo ""
fi
