#!/usr/bin/env bash
# add-node.sh — Register this machine as a 429 Inference node
# Usage: curl -sSL https://429inference.com/add-node.sh | bash
set -e

API="https://api.429inference.com"
CONFIG_DIR="$HOME/.config/429-node"
KEY_FILE="$CONFIG_DIR/node.key"
UNIT_DIR="$HOME/.config/systemd/user"
UNIT_FILE="$UNIT_DIR/429-node.service"

# ── colour helpers ───────────────────────────────────────────────────────────
BOLD=$(tput bold 2>/dev/null || true)
RESET=$(tput sgr0 2>/dev/null || true)
GREEN=$(tput setaf 2 2>/dev/null || true)
YELLOW=$(tput setaf 3 2>/dev/null || true)
RED=$(tput setaf 1 2>/dev/null || true)

info()  { echo "${BOLD}${GREEN}[+]${RESET} $*"; }
warn()  { echo "${BOLD}${YELLOW}[!]${RESET} $*"; }
error() { echo "${BOLD}${RED}[✗]${RESET} $*" >&2; }
die()   { error "$*"; exit 1; }

# ── pre-flight ───────────────────────────────────────────────────────────────
command -v curl  >/dev/null || die "curl is required"
command -v jq    >/dev/null || die "jq is required (apt: 'sudo apt install jq', mac: 'brew install jq')"

OS_NAME=$(uname -s)
ARCH=$(uname -m)
case "$OS_NAME" in
  Linux)  OS_KIND=linux ;;
  Darwin) OS_KIND=macos ;;
  *)      die "Unsupported OS: $OS_NAME (linux + macos only for now)" ;;
esac

# ── banner ───────────────────────────────────────────────────────────────────
echo ""
echo "${BOLD}${GREEN}"
cat <<'BANNER'
  ╔══════════════════════════════════════════════════════╗
  ║                                                      ║
  ║   ██╗  ██╗██████╗  █████╗     ██╗███╗   ██╗███╗   ██╗║
  ║   ██║  ██║╚════██╗██╔══██╗    ██║████╗  ██║████╗  ██║║
  ║   ███████║ █████╔╝╚██████║    ██║██╔██╗ ██║██╔██╗ ██║║
  ║   ╚════██║██╔═══╝  ╚═══██║    ██║██║╚██╗██║██║╚██╗██║║
  ║        ██║███████╗ █████╔╝    ██║██║ ╚████║██║ ╚████║║
  ║        ╚═╝╚══════╝ ╚════╝     ╚═╝╚═╝  ╚═══╝╚═╝  ╚═══╝║
  ║                                                      ║
  ║         an inference network for friends.            ║
  ║                                                      ║
  ╚══════════════════════════════════════════════════════╝
BANNER
echo "${RESET}"
echo "  ${BOLD}// friends don't let friends pay per token.${RESET}"
echo ""
echo "  This script will:"
echo "    1. detect your GPU and Ollama"
echo "    2. register this box as a node (pending admin approval)"
echo "    3. install a tiny systemd heartbeat"
echo "    4. mint you ${BOLD}${GREEN}1 × 429EC${RESET} per request you serve"
echo ""
echo "----------------------------------------"

# Already registered? Offer a clean reinstall instead of bailing.
if [[ -f "$KEY_FILE" ]] || [[ -f "$UNIT_FILE" ]]; then
  warn "This machine is already registered as a 429 node."
  echo "  key:  $KEY_FILE"
  echo "  unit: $UNIT_FILE"
  echo ""
  if [[ -r /dev/tty ]]; then
    printf "Delete and re-register from scratch? [y/N]: " > /dev/tty
    IFS= read -r REPLY < /dev/tty
  else
    REPLY="${REINSTALL:-n}"
  fi
  case "$REPLY" in
    y|Y|yes|YES)
      info "Tearing down old install…"
      if command -v systemctl >/dev/null 2>&1; then
        systemctl --user disable --now 429-node.service 2>/dev/null || true
        systemctl --user disable --now 429-tunnel.service 2>/dev/null || true
      fi
      # macOS LaunchAgent
      if [[ -f "$HOME/Library/LaunchAgents/com.429inference.node.plist" ]]; then
        launchctl unload "$HOME/Library/LaunchAgents/com.429inference.node.plist" 2>/dev/null || true
        rm -f "$HOME/Library/LaunchAgents/com.429inference.node.plist"
      fi
      # Kill any stray cloudflared we started without systemd (macOS).
      pkill -f 'cloudflared tunnel.*--url http://127.0.0.1:11434' 2>/dev/null || true
      # Kill stray heartbeat loops on macOS
      pkill -f "$CONFIG_DIR/heartbeat.sh" 2>/dev/null || true
      rm -f "$KEY_FILE" "$UNIT_FILE" "$UNIT_DIR/429-tunnel.service" \
            "$CONFIG_DIR/tunnel.url" "$CONFIG_DIR/tunnel.log" \
            "$CONFIG_DIR/heartbeat.sh" "$CONFIG_DIR/heartbeat.log"
      info "Old install removed. Continuing with fresh registration."
      ;;
    *)
      echo "Nothing to do. Re-run with REINSTALL=y to force."
      exit 0
      ;;
  esac
fi

# ── interactive prompts ──────────────────────────────────────────────────────
# When run via `curl … | bash`, stdin is the pipe — `read` would slurp the
# next line of script source. Read from /dev/tty instead. Allow env-var
# overrides for true non-interactive use (CI, ansible, etc.).
if [[ -r /dev/tty ]]; then
  PROMPT_FD=/dev/tty
elif [[ -t 0 ]]; then
  PROMPT_FD=/dev/stdin
else
  PROMPT_FD=""
fi

ask() {
  # ask <varname> <prompt> [default]
  local __var="$1" __prompt="$2" __default="${3-}" __reply=""
  if [[ -n "${!__var:-}" ]]; then
    return 0  # already provided via env var
  fi
  if [[ -z "$PROMPT_FD" ]]; then
    [[ -n "$__default" ]] && { printf -v "$__var" '%s' "$__default"; return 0; }
    die "No TTY available and \$$__var not set. Re-run interactively or pass it as an env var."
  fi
  if [[ -n "$__default" ]]; then
    printf '%s' "$__prompt [$__default]: " > /dev/tty
  else
    printf '%s' "$__prompt: " > /dev/tty
  fi
  IFS= read -r __reply < "$PROMPT_FD"
  [[ -z "$__reply" && -n "$__default" ]] && __reply="$__default"
  printf -v "$__var" '%s' "$__reply"
}

ask NODE_NAME    "Node display name (e.g. 'rtx4080-desktop')"
[[ -z "$NODE_NAME" ]] && die "Name required"

ask OWNER_NAME   "Your name or alias"
ask OWNER_EMAIL  "Your email (admin will use this to send your API key)"
[[ -z "$OWNER_EMAIL" ]] && die "Email required"

# ── install ollama if missing ────────────────────────────────────────────────
if ! command -v ollama >/dev/null 2>&1; then
  info "Ollama not found — installing via official installer..."
  if [[ "$OS_KIND" == "linux" ]]; then
    curl -fsSL https://ollama.com/install.sh | sh || die "Ollama install failed"
  else
    die "Please install Ollama from https://ollama.com/download then re-run."
  fi
  info "Ollama installed."
fi

# ── make ollama bind 0.0.0.0 so the API server can reach it ──────────────────
if [[ "$OS_KIND" == "linux" ]] && command -v systemctl >/dev/null 2>&1; then
  if systemctl list-unit-files 2>/dev/null | grep -q '^ollama.service'; then
    OVERRIDE_DIR=/etc/systemd/system/ollama.service.d
    OVERRIDE=$OVERRIDE_DIR/429.conf
    if [[ ! -f "$OVERRIDE" ]] || ! grep -q 'OLLAMA_HOST=0.0.0.0' "$OVERRIDE" 2>/dev/null; then
      info "Binding ollama to 0.0.0.0:11434 so the API server can reach this node..."
      if sudo -n true 2>/dev/null || [[ $EUID -eq 0 ]]; then
        sudo mkdir -p "$OVERRIDE_DIR"
        # Also set OLLAMA_ORIGINS=* so requests proxied through a tunnel
        # (which arrive with a non-localhost Host header) aren't 403'd by
        # ollama's built-in CSRF protection.
        printf '[Service]\nEnvironment="OLLAMA_HOST=0.0.0.0:11434"\nEnvironment="OLLAMA_ORIGINS=*"\n' | sudo tee "$OVERRIDE" >/dev/null
        sudo systemctl daemon-reload
        sudo systemctl restart ollama
        info "Ollama restarted on 0.0.0.0:11434 with OLLAMA_ORIGINS=*."
      else
        warn "Need sudo to bind ollama to 0.0.0.0. Run manually:"
        echo "  sudo mkdir -p $OVERRIDE_DIR"
        echo "  printf '[Service]\\nEnvironment=\"OLLAMA_HOST=0.0.0.0:11434\"\\nEnvironment=\"OLLAMA_ORIGINS=*\"\\n' | sudo tee $OVERRIDE"
        echo "  sudo systemctl daemon-reload && sudo systemctl restart ollama"
      fi
    fi
  fi
elif [[ "$OS_KIND" == "macos" ]]; then
  # On macOS, Ollama runs as a GUI app launched via launchd. It reads env
  # vars set with `launchctl setenv`. Two are needed:
  #   OLLAMA_HOST=127.0.0.1:11434  — fine, that's the default
  #   OLLAMA_ORIGINS=*             — REQUIRED when using a tunnel, because
  #                                  ollama otherwise 403s any request whose
  #                                  Host header isn't localhost.
  CURRENT_ORIGINS=$(launchctl getenv OLLAMA_ORIGINS 2>/dev/null || true)
  if [[ "$CURRENT_ORIGINS" != "*" ]]; then
    info "Setting OLLAMA_ORIGINS=* so tunneled requests aren't blocked by ollama's host check..."
    launchctl setenv OLLAMA_ORIGINS "*" || warn "launchctl setenv failed — you may need to run this manually."
    # Restart ollama so the new env takes effect. We pkill rather than
    # `ollama serve` because the GUI app respawns automatically.
    if pgrep -x ollama >/dev/null 2>&1 || pgrep -f "Ollama.app" >/dev/null 2>&1; then
      info "Restarting ollama to pick up OLLAMA_ORIGINS..."
      osascript -e 'quit app "Ollama"' 2>/dev/null || true
      pkill -x ollama 2>/dev/null || true
      sleep 2
      # Try to relaunch
      if [[ -d "/Applications/Ollama.app" ]]; then
        open -ga Ollama 2>/dev/null || true
      else
        nohup ollama serve >/dev/null 2>&1 &
        disown || true
      fi
      # Wait for it to come back
      for _ in $(seq 1 20); do
        if curl -fsS --max-time 2 http://127.0.0.1:11434/api/version >/dev/null 2>&1; then
          info "Ollama is back up."
          break
        fi
        sleep 1
      done
    fi
  fi
fi

# ── pull the gpt-oss model ───────────────────────────────────────────────────
if command -v ollama >/dev/null 2>&1; then
  if ! ollama list 2>/dev/null | awk '{print $1}' | grep -q '^gpt-oss'; then
    info "Pulling gpt-oss model (this can take a while)..."
    ollama pull gpt-oss || warn "ollama pull gpt-oss failed — pull it manually later"
  else
    info "gpt-oss model already present."
  fi  # Pre-warm so the first real request is sub-second instead of ~20s cold.
  # keep_alive=24h tells ollama to keep the model resident in VRAM.
  info "Warming up gpt-oss in VRAM (keep_alive=24h)..."
  curl -fsS -X POST "http://127.0.0.1:11434/api/generate" \
    -H "Content-Type: application/json" \
    -d '{"model":"gpt-oss","prompt":"","keep_alive":"24h","stream":false}' \
    --max-time 120 >/dev/null 2>&1 \
    && info "Model loaded and pinned." \
    || warn "Warmup ping failed (ollama may still be downloading). It'll load on first request."
fi

# ── auto-resolve a reachable Ollama URL ──────────────────────────────────────
# We collect every plausible URL this machine could be reached at and ask the
# API server "can you reach this?" for each one. First success wins. The user
# only has to override if every candidate fails (NAT with no tunnel, etc.).
#
# Order matters — fastest path first:
#   1. tailscale (zero-config, encrypted, works through NAT)
#   2. cloudflare named tunnel (if `cloudflared` is configured for this box)
#   3. each LAN IPv4 (works when API server is on the same network)
#   4. public IPv4 (only works if the operator port-forwarded 11434)
declare -a CANDIDATES=()
add_candidate() {
  local u="$1"
  [[ -z "$u" ]] && return
  # de-dupe
  for existing in "${CANDIDATES[@]:-}"; do [[ "$existing" == "$u" ]] && return; done
  CANDIDATES+=("$u")
}

# 1. Tailscale
if command -v tailscale >/dev/null 2>&1; then
  TS_IP=$(tailscale ip -4 2>/dev/null | head -1 || true)
  [[ -n "$TS_IP" ]] && add_candidate "http://${TS_IP}:11434"
fi

# 2. Cloudflare tunnel — look for an active cloudflared config with a
#    hostname pointed at localhost:11434. Composable: any operator who runs
#    `cloudflared tunnel` and points a hostname at 127.0.0.1:11434 gets
#    auto-discovery for free.
if command -v cloudflared >/dev/null 2>&1; then
  for cf_cfg in "$HOME/.cloudflared/config.yml" "/etc/cloudflared/config.yml"; do
    if [[ -r "$cf_cfg" ]]; then
      # Pull the first hostname whose service points at localhost:11434.
      CF_HOST=$(awk '
        /^[[:space:]]*-?[[:space:]]*hostname:/ { h=$NF; gsub(/["\047]/,"",h) }
        /^[[:space:]]*service:.*(127\.0\.0\.1|localhost):11434/ { if (h) { print h; exit } }
      ' "$cf_cfg" 2>/dev/null || true)
      [[ -n "$CF_HOST" ]] && add_candidate "https://${CF_HOST}"
    fi
  done
fi

# 3. LAN IPv4 addresses (skip loopback + docker bridges).
if [[ "$OS_KIND" == "linux" ]] && command -v ip >/dev/null 2>&1; then
  while read -r lan_ip; do
    [[ -z "$lan_ip" || "$lan_ip" == 127.* ]] && continue
    add_candidate "http://${lan_ip}:11434"
  done < <(ip -4 -o addr show scope global 2>/dev/null \
    | awk '{print $4}' | cut -d/ -f1 \
    | grep -vE '^(172\.1[7-9]\.|172\.2[0-9]\.|172\.3[0-1]\.)')  # drop docker
elif [[ "$OS_KIND" == "macos" ]] && command -v ifconfig >/dev/null 2>&1; then
  while read -r lan_ip; do
    [[ -z "$lan_ip" || "$lan_ip" == 127.* ]] && continue
    add_candidate "http://${lan_ip}:11434"
  done < <(ifconfig 2>/dev/null | awk '/inet /{print $2}')
fi

# 4. Public IPv4 (only useful if 11434 is port-forwarded — but free to try).
PUBLIC_IP=$(curl -fsS --max-time 3 https://api.ipify.org 2>/dev/null \
            || curl -fsS --max-time 3 https://icanhazip.com 2>/dev/null \
            || true)
PUBLIC_IP=$(echo "${PUBLIC_IP:-}" | tr -d '[:space:]')
[[ -n "$PUBLIC_IP" ]] && add_candidate "http://${PUBLIC_IP}:11434"

# Probe every candidate from the API server's perspective. First reachable
# wins. The probe endpoint is read-only (GET /api/version with 5s timeout)
# and rejects loopback / cloud-metadata addresses on the API side.
probe_url() {
  local url="$1"
  curl -fsS --max-time 8 -X POST "$API/admin/workers/probe" \
    -H "Content-Type: application/json" \
    -d "$(jq -nc --arg url "$url" '{url:$url}')" 2>/dev/null \
    | jq -r '.ok // false' 2>/dev/null
}

OLLAMA_URL=""
if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
  warn "Couldn't enumerate any candidate URLs for this machine."
else
  info "Probing ${#CANDIDATES[@]} candidate URL(s) from the API server..."
  for cand in "${CANDIDATES[@]}"; do
    printf "    %s ... " "$cand"
    if [[ "$(probe_url "$cand")" == "true" ]]; then
      echo "${GREEN}reachable${RESET}"
      OLLAMA_URL="$cand"
      break
    else
      echo "${YELLOW}no${RESET}"
    fi
  done
fi

# If nothing reached the API server, automatically install + start a
# Cloudflare quick tunnel. Quick tunnels (`cloudflared tunnel --url ...`)
# need ZERO Cloudflare account, ZERO DNS config — they hand back a random
# https://*.trycloudflare.com URL that proxies straight to localhost:11434.
# This is the universal fallback that works behind any NAT.
TUNNEL_URL_FILE="$CONFIG_DIR/tunnel.url"
TUNNEL_LOG="$CONFIG_DIR/tunnel.log"
TUNNEL_UNIT="$UNIT_DIR/429-tunnel.service"
USING_TUNNEL=0

install_cloudflared() {
  command -v cloudflared >/dev/null 2>&1 && return 0
  info "Installing cloudflared (Cloudflare's tunnel client)..."
  case "$OS_KIND" in
    linux)
      local cf_arch
      case "$ARCH" in
        x86_64|amd64) cf_arch=amd64 ;;
        aarch64|arm64) cf_arch=arm64 ;;
        armv7l|armv6l) cf_arch=arm ;;
        *) error "Unsupported arch for cloudflared: $ARCH"; return 1 ;;
      esac
      mkdir -p "$HOME/.local/bin"
      curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cf_arch}" \
        -o "$HOME/.local/bin/cloudflared" || return 1
      chmod +x "$HOME/.local/bin/cloudflared"
      export PATH="$HOME/.local/bin:$PATH"
      ;;
    macos)
      if command -v brew >/dev/null 2>&1; then
        brew install cloudflared >/dev/null || return 1
      else
        local cf_arch
        case "$ARCH" in
          arm64) cf_arch=arm64 ;;
          x86_64) cf_arch=amd64 ;;
          *) error "Unsupported mac arch: $ARCH"; return 1 ;;
        esac
        mkdir -p "$HOME/.local/bin"
        curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cf_arch}.tgz" \
          | tar -xz -C "$HOME/.local/bin" cloudflared || return 1
        chmod +x "$HOME/.local/bin/cloudflared"
        export PATH="$HOME/.local/bin:$PATH"
      fi
      ;;
  esac
  command -v cloudflared >/dev/null 2>&1
}

start_tunnel() {
  : > "$TUNNEL_LOG"
  : > "$TUNNEL_URL_FILE"

  if command -v systemctl >/dev/null 2>&1 && [[ "$OS_KIND" == "linux" ]]; then
    mkdir -p "$UNIT_DIR"
    cat > "$TUNNEL_UNIT" << EOF
[Unit]
Description=429 Inference Cloudflare quick tunnel (ollama -> public https)
After=network-online.target

[Service]
Type=simple
ExecStart=$(command -v cloudflared) tunnel --no-autoupdate --url http://127.0.0.1:11434 --logfile $TUNNEL_LOG --loglevel info
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
EOF
    systemctl --user daemon-reload
    systemctl --user enable --now 429-tunnel.service >/dev/null 2>&1 || true
  else
    # macOS / no systemd: nohup it directly. The heartbeat will keep working
    # as long as the process is alive; cloudflared self-recovers connections.
    nohup "$(command -v cloudflared)" tunnel --no-autoupdate \
      --url http://127.0.0.1:11434 --logfile "$TUNNEL_LOG" --loglevel info \
      >/dev/null 2>&1 &
    disown || true
  fi

  # Wait up to 30s for the trycloudflare URL to appear in the log.
  local url=""
  for _ in $(seq 1 60); do
    sleep 0.5
    url=$(grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' "$TUNNEL_LOG" 2>/dev/null | head -1 || true)
    [[ -n "$url" ]] && break
  done
  if [[ -z "$url" ]]; then
    error "Cloudflare tunnel did not produce a URL in 30s. Log:"
    tail -20 "$TUNNEL_LOG" >&2 || true
    return 1
  fi
  echo "$url" > "$TUNNEL_URL_FILE"
  echo "$url"
}

if [[ -z "$OLLAMA_URL" ]]; then
  warn "No direct path reached the API server — falling back to Cloudflare quick tunnel."
  echo "  This installs cloudflared, opens a public https://*.trycloudflare.com URL"
  echo "  for ollama on this machine, and pins it as a systemd service."
  echo "  No Cloudflare account required."
  echo ""

  # Pre-flight: cloudflared will happily start a tunnel to a port nothing is
  # listening on, then the API probe will fail with HTTP 502 minutes later
  # and the user has no idea why. Catch it here.
  if ! curl -fsS --max-time 3 http://127.0.0.1:11434/api/version >/dev/null 2>&1; then
    error "Nothing is listening on 127.0.0.1:11434 — Ollama isn't running on this machine."
    if [[ "$OS_KIND" == "macos" ]]; then
      echo "  Open the Ollama app (or run 'ollama serve' in another terminal) and re-run this script."
    else
      echo "  Run 'sudo systemctl start ollama' (or 'ollama serve') and re-run this script."
    fi
    die "Aborting — can't tunnel to a port nothing is serving."
  fi

  if ! install_cloudflared; then
    die "Couldn't install cloudflared. Install it manually (https://github.com/cloudflare/cloudflared/releases) and re-run."
  fi

  # Verify ollama actually accepts non-localhost Host headers. Cloudflare
  # tunnels send the trycloudflare hostname through, and ollama 403s any
  # Host that isn't on its OLLAMA_ORIGINS allowlist. We test BEFORE bringing
  # up the tunnel — saves 2min of probing dead air on every misconfig.
  info "Verifying ollama allows tunneled requests (Host header check)..."
  ORIGIN_OK=0
  for attempt in 1 2; do
    LOCAL_PROBE_CODE=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 5 \
      -H "Host: example.trycloudflare.com" \
      http://127.0.0.1:11434/api/version 2>/dev/null || echo "000")
    if [[ "$LOCAL_PROBE_CODE" == "200" ]]; then
      ORIGIN_OK=1
      break
    fi
    if [[ "$attempt" == "1" ]]; then
      warn "ollama returned HTTP $LOCAL_PROBE_CODE for tunneled Host header — fixing OLLAMA_ORIGINS..."
      if [[ "$OS_KIND" == "macos" ]]; then
        launchctl setenv OLLAMA_ORIGINS "*" 2>/dev/null || true
        # Hard-quit Ollama: GUI quit, then kill any stray process. The
        # menubar app respawns on `open` and inherits the new launchctl env.
        osascript -e 'tell application "Ollama" to quit' 2>/dev/null || true
        sleep 1
        killall Ollama 2>/dev/null || true
        pkill -x ollama 2>/dev/null || true
        sleep 2
        if [[ -d "/Applications/Ollama.app" ]]; then
          open -ga Ollama 2>/dev/null || true
        else
          OLLAMA_ORIGINS='*' nohup ollama serve >/dev/null 2>&1 &
          disown 2>/dev/null || true
        fi
        info "Waiting for Ollama to come back up..."
        for _ in $(seq 1 30); do
          if curl -fsS --max-time 2 http://127.0.0.1:11434/api/version >/dev/null 2>&1; then break; fi
          sleep 1
        done
      fi
    fi
  done
  if [[ "$ORIGIN_OK" != "1" ]]; then
    error "Ollama is still rejecting tunneled requests (HTTP $LOCAL_PROBE_CODE)."
    echo "  This means OLLAMA_ORIGINS isn't set on the running ollama process."
    if [[ "$OS_KIND" == "macos" ]]; then
      echo ""
      echo "  ${BOLD}Manual fix on macOS:${RESET}"
      echo "    1. ${BOLD}launchctl setenv OLLAMA_ORIGINS '*'${RESET}"
      echo "    2. Right-click the Ollama menubar icon → ${BOLD}Quit Ollama${RESET}"
      echo "    3. Reopen Ollama.app from Applications"
      echo "    4. Re-run this script with: ${BOLD}REINSTALL=y bash -c \"\$(curl -fsSL https://429inference.com/add-node.sh)\"${RESET}"
      echo ""
      echo "  Verify with:  ${DIM}curl -H 'Host: x.trycloudflare.com' http://127.0.0.1:11434/api/version${RESET}"
      echo "  (should return JSON, not 403)"
    else
      echo ""
      echo "  ${BOLD}Manual fix on linux:${RESET}"
      echo "    sudo mkdir -p /etc/systemd/system/ollama.service.d"
      echo "    printf '[Service]\\nEnvironment=\"OLLAMA_ORIGINS=*\"\\n' | sudo tee /etc/systemd/system/ollama.service.d/origins.conf"
      echo "    sudo systemctl daemon-reload && sudo systemctl restart ollama"
    fi
    die "Aborting — fix OLLAMA_ORIGINS and re-run."
  fi
  info "ollama accepts tunneled Host headers. ${GREEN}OK${RESET}"

  info "Starting tunnel and waiting for public URL..."
  TUNNEL_URL=$(start_tunnel) || die "Tunnel failed to start. See $TUNNEL_LOG"
  info "Tunnel up: ${BOLD}${TUNNEL_URL}${RESET}"

  # Quick tunnels (*.trycloudflare.com) are registered with Cloudflare's edge
  # the moment cloudflared logs "Registered tunnel connection", but the
  # hostname can take 30–120s to actually resolve through the edge. Probe with
  # retries so we don't bail on healthy tunnels that just haven't propagated.
  printf "    probing from API server (this can take ~60s) "
  PROBE_OK=0
  for attempt in $(seq 1 24); do
    if [[ "$(probe_url "$TUNNEL_URL")" == "true" ]]; then
      PROBE_OK=1
      break
    fi
    printf "."
    sleep 5
  done
  echo ""
  if [[ "$PROBE_OK" == "1" ]]; then
    info "Tunnel reachable: ${GREEN}${TUNNEL_URL}${RESET}"
    OLLAMA_URL="$TUNNEL_URL"
    USING_TUNNEL=1
  else
    error "Tunnel URL $TUNNEL_URL came up locally but the API server still can't reach it after 2min."
    echo "  This usually means one of:"
    echo "    1. ollama is returning HTTP 403 — it blocks non-localhost Host headers by default."
    echo "       Fix on macOS:  launchctl setenv OLLAMA_ORIGINS '*'  then restart Ollama.app and re-run."
    echo "       Fix on linux:  add Environment=\"OLLAMA_ORIGINS=*\" to the ollama systemd unit."
    echo "    2. ollama isn't listening on 127.0.0.1:11434 — run 'ollama serve' (or open Ollama.app) and retry."
    echo "    3. Cloudflare's edge hasn't finished propagating the new hostname — wait a minute and re-run."
    echo "    4. cloudflared was blocked by a firewall (corporate / VPN). Check $TUNNEL_LOG."
    echo ""
    echo "  Quick check from this machine:"
    echo "    curl -i $TUNNEL_URL/api/version"
    echo ""
    echo "  Tunnel log tail:"
    tail -10 "$TUNNEL_LOG" 2>/dev/null | sed 's/^/    /'
    die "Aborting registration."
  fi
else
  info "Selected: ${BOLD}${OLLAMA_URL}${RESET}"
fi

# ── detect hardware ───────────────────────────────────────────────────────────
RAM_GB=0
GPU_MODEL=""
VRAM_GB=0

if command -v free >/dev/null 2>&1; then
  RAM_GB=$(free -g | awk '/^Mem:/{print $2}')
fi

if command -v nvidia-smi >/dev/null 2>&1; then
  GPU_MODEL=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || true)
  VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 | awk '{printf "%d", $1/1024}' || true)
elif command -v rocm-smi >/dev/null 2>&1; then
  GPU_MODEL=$(rocm-smi --showproductname 2>/dev/null | awk -F': ' '/GPU/{print $2}' | head -1 || true)
elif [[ "$OS_NAME" == "Darwin" ]]; then
  GPU_MODEL=$(system_profiler SPDisplaysDataType 2>/dev/null | awk -F': ' '/Chipset Model/{print $2}' | head -1 || true)
fi

info "Detected: OS=$OS_NAME ARCH=$ARCH RAM=${RAM_GB}GB GPU='${GPU_MODEL}' VRAM=${VRAM_GB}GB"
echo ""

# ── register with API ─────────────────────────────────────────────────────────
info "Registering node with 429 Inference..."

PAYLOAD=$(jq -n \
  --arg name        "$NODE_NAME" \
  --arg ownerName   "$OWNER_NAME" \
  --arg ownerEmail  "$OWNER_EMAIL" \
  --arg ollamaUrl   "$OLLAMA_URL" \
  --arg gpuModel    "$GPU_MODEL" \
  --argjson vramGb  "${VRAM_GB:-0}" \
  --arg os          "$OS_NAME" \
  --arg arch        "$ARCH" \
  --argjson ramGb   "${RAM_GB:-0}" \
  '{name:$name, ownerName:$ownerName, ownerEmail:$ownerEmail,
    ollamaUrl:$ollamaUrl, gpuModel:$gpuModel, vramGb:$vramGb,
    os:$os, arch:$arch, ramGb:$ramGb}')

RESPONSE=$(curl -sSf -X POST "$API/admin/workers/register" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD" 2>&1) || die "Registration failed: $RESPONSE"

NODE_KEY=$(echo "$RESPONSE" | jq -r '.nodeKey // empty')
[[ -z "$NODE_KEY" ]] && die "No nodeKey in response: $RESPONSE"

# ── persist key ───────────────────────────────────────────────────────────────
mkdir -p "$CONFIG_DIR"
chmod 700 "$CONFIG_DIR"
echo "$NODE_KEY" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
info "Node key saved to $KEY_FILE"

# ── write heartbeat systemd unit ──────────────────────────────────────────────
mkdir -p "$UNIT_DIR"

# If we're using a Cloudflare quick tunnel, the URL changes every time
# cloudflared restarts. The heartbeat re-scrapes it from the tunnel log
# each tick so the API always sees the live URL without a re-register.
cat > "$UNIT_FILE" << EOF
[Unit]
Description=429 Inference node heartbeat
After=network-online.target

[Service]
Type=simple
Environment=API_URL=$API
Environment=KEY_FILE=$KEY_FILE
Environment=STATIC_OLLAMA_URL=$OLLAMA_URL
Environment=TUNNEL_LOG=$TUNNEL_LOG
ExecStart=/bin/bash -c 'while true; do \
  if [ -s "\$TUNNEL_LOG" ]; then \
    URL=\$(grep -Eo "https://[a-z0-9-]+\\.trycloudflare\\.com" "\$TUNNEL_LOG" | tail -1); \
  fi; \
  URL=\${URL:-\$STATIC_OLLAMA_URL}; \
  curl -sf -X POST "\$API_URL/admin/workers/heartbeat" \
    -H "X-Node-Key: \$(cat \$KEY_FILE)" \
    -H "Content-Type: application/json" \
    -d "{\\"ollamaUrl\\":\\"\$URL\\"}" >/dev/null 2>&1 || true; \
  sleep 20; \
done'
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
EOF

# ── enable & start ────────────────────────────────────────────────────────────
if command -v systemctl >/dev/null 2>&1; then
  systemctl --user daemon-reload
  systemctl --user enable --now 429-node.service
  info "Heartbeat service enabled (systemctl --user status 429-node.service)"
elif [[ "$OS_KIND" == "macos" ]]; then
  # Install a launchd LaunchAgent so the heartbeat survives logout / reboot.
  LAUNCH_AGENT_DIR="$HOME/Library/LaunchAgents"
  LAUNCH_AGENT="$LAUNCH_AGENT_DIR/com.429inference.node.plist"
  HEARTBEAT_SCRIPT="$CONFIG_DIR/heartbeat.sh"
  HEARTBEAT_LOG="$CONFIG_DIR/heartbeat.log"
  mkdir -p "$LAUNCH_AGENT_DIR"
  cat > "$HEARTBEAT_SCRIPT" << EOF
#!/bin/bash
API_URL="$API"
KEY_FILE="$KEY_FILE"
STATIC_OLLAMA_URL="$OLLAMA_URL"
TUNNEL_LOG="$TUNNEL_LOG"
while true; do
  URL=""
  if [ -s "\$TUNNEL_LOG" ]; then
    URL=\$(grep -Eo "https://[a-z0-9-]+\.trycloudflare\.com" "\$TUNNEL_LOG" | tail -1)
  fi
  URL=\${URL:-\$STATIC_OLLAMA_URL}
  curl -sf -X POST "\$API_URL/admin/workers/heartbeat" \\
    -H "X-Node-Key: \$(cat \$KEY_FILE)" \\
    -H "Content-Type: application/json" \\
    -d "{\"ollamaUrl\":\"\$URL\"}" >/dev/null 2>&1 || true
  sleep 20
done
EOF
  chmod +x "$HEARTBEAT_SCRIPT"
  cat > "$LAUNCH_AGENT" << 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.429inference.node</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>$HEARTBEAT_SCRIPT</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>$HEARTBEAT_LOG</string>
  <key>StandardErrorPath</key><string>$HEARTBEAT_LOG</string>
</dict>
</plist>
EOF
  launchctl unload "$LAUNCH_AGENT" 2>/dev/null || true
  launchctl load "$LAUNCH_AGENT" 2>/dev/null || true
  info "Heartbeat LaunchAgent installed (launchctl list | grep 429inference)"
else
  warn "neither systemd nor launchd detected — start the heartbeat manually:"
  echo "  while true; do curl -sf -X POST $API/admin/workers/heartbeat -H 'X-Node-Key: \$(cat $KEY_FILE)' -d '{}' -H 'Content-Type: application/json'; sleep 20; done &"
fi

# ── install the `429` CLI ────────────────────────────────────────────────────
# Prefer /usr/local/bin (always on PATH on macOS + linux). If sudo isn't
# available, fall back to ~/.local/bin and add that to PATH for both
# bash and zsh (zsh is macOS default).
CLI_TMP="$(mktemp)"
if curl -fsSL "https://429inference.com/429-cli.sh" -o "$CLI_TMP" 2>/dev/null; then
  chmod +x "$CLI_TMP"

  CLI_INSTALLED=""
  # 1. Try /usr/local/bin via sudo (no-prompt)
  if [[ -w /usr/local/bin ]]; then
    cp "$CLI_TMP" /usr/local/bin/429 && CLI_INSTALLED="/usr/local/bin/429"
  elif sudo -n true 2>/dev/null; then
    sudo cp "$CLI_TMP" /usr/local/bin/429 && sudo chmod +x /usr/local/bin/429 \
      && CLI_INSTALLED="/usr/local/bin/429"
  fi
  # 2. Try /opt/homebrew/bin (apple silicon brew default — already on PATH)
  if [[ -z "$CLI_INSTALLED" && -w /opt/homebrew/bin ]]; then
    cp "$CLI_TMP" /opt/homebrew/bin/429 && CLI_INSTALLED="/opt/homebrew/bin/429"
  fi
  # 3. Fallback: ~/.local/bin and update shell rc files
  if [[ -z "$CLI_INSTALLED" ]]; then
    BIN_DIR="$HOME/.local/bin"
    mkdir -p "$BIN_DIR"
    cp "$CLI_TMP" "$BIN_DIR/429" && chmod +x "$BIN_DIR/429" \
      && CLI_INSTALLED="$BIN_DIR/429"

    PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
    # Cover bash, zsh (macOS default), and the POSIX profile.
    for RC in "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.profile"; do
      [[ -f "$RC" ]] || continue
      if ! grep -qF "$PATH_LINE" "$RC"; then
        printf '\n# 429 inference CLI\n%s\n' "$PATH_LINE" >> "$RC"
        info "Added ~/.local/bin to PATH in ${RC#$HOME/}"
      fi
    done
    # Also create the rc files if missing (zsh is default on macOS).
    if [[ "$OS_KIND" == "macos" && ! -f "$HOME/.zshrc" ]]; then
      printf '# 429 inference CLI\n%s\n' "$PATH_LINE" > "$HOME/.zshrc"
      info "Created ~/.zshrc with ~/.local/bin on PATH."
    fi
    case ":$PATH:" in
      *":$BIN_DIR:"*) ;;
      *) warn "Open a new terminal (or run: 'source ~/.zshrc' on macOS, 'source ~/.bashrc' on linux) to use the '429' command." ;;
    esac
  fi
  rm -f "$CLI_TMP"
  if [[ -n "$CLI_INSTALLED" ]]; then
    info "Installed CLI: $CLI_INSTALLED  (try '429 help' or '429 update')"
  fi
else
  rm -f "$CLI_TMP"
  warn "Could not install 429 CLI (network issue?). Skipping."
fi

echo ""
echo "${BOLD}${GREEN}Node registered!${RESET}"
echo "  Name:      $NODE_NAME"
echo "  Owner:     $OWNER_NAME <$OWNER_EMAIL>"
echo "  Status:    pending (awaiting admin approval)"
echo ""
echo "The admin will be notified at admin@429inference.com."
echo "Once approved, you will receive an API key at $OWNER_EMAIL."
echo ""
echo "Next:"
echo "  ${BOLD}429 status${RESET}    — check the heartbeat"
echo "  ${BOLD}429 bal${RESET}       — your 429EC balance"
echo "  ${BOLD}429 history${RESET}   — recent requests served"
echo "  ${BOLD}429 help${RESET}      — full command list"
echo ""
