#!/usr/bin/env bash
# bin/olp-connect — Lightweight client script to connect this machine to a remote
# OLP (Open LLM Proxy). Ported from OCP's `ocp-connect` per ADR 0010 § Phase 4
# D68-D70 charter; uses /health.anonymousKey when the remote operator opted in
# via `auth.advertise_anonymous_key: true` (ADR 0011).
#
# Authority:
#   - ADR 0010 (Phase 4 charter — D68 line: client-side IDE auto-config)
#   - ADR 0011 (anonymous-key deployment-context limits — trusted-LAN invariant)
#   - OCP `ocp-connect` v1.3.0 (prior-art reference)
#
# Why bash (not Node like `olp` CLI):
#   olp-connect MUST run on CLIENT machines that may not have a recent Node
#   installed (parents' laptops, work machines, Raspberry Pi). bash + curl +
#   python3 give maximum portability; this script does not import any OLP
#   Node modules.
#
# Dependencies: bash >=4, curl, python3 (for /health JSON parsing).
#
# Install:
#   curl -fsSL https://raw.githubusercontent.com/dtzp555-max/olp/main/bin/olp-connect -o olp-connect
#   chmod +x olp-connect
#
# Or via npm/npx (once `npm install -g olp` is run on a machine that has Node):
#   olp-connect <ip>
#
# Or run directly via curl-pipe:
#   curl -fsSL https://raw.githubusercontent.com/dtzp555-max/olp/main/bin/olp-connect | bash -s -- <host-ip>

set -euo pipefail

# D78 (G13): derive version from package.json instead of hardcoding (was
# stuck at "0.4.0-phase4" through v0.4.1/v0.4.2/v0.4.3 because no one
# updated it). Look up package.json next to the script if available;
# fall back to "unknown" when running curl-piped (no on-disk package.json).
_resolve_version() {
  local script_dir pkg
  # When curl-piped (`curl ... | bash`), BASH_SOURCE[0] is empty → dirname
  # yields "." → script_dir resolves to cwd. D78 reviewer P2-1 hardening:
  # require the suffix-strip to actually fire (script_dir ENDED with /bin),
  # otherwise we'd happily pick up an unrelated package.json from whatever
  # directory the user happens to be in when piping. Belt-and-braces.
  # ${BASH_SOURCE[0]:-} default-empty guards against `set -u` nounset error
  # when invoked via `curl ... | bash` (no source file → BASH_SOURCE unset).
  script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-}")" &>/dev/null && pwd)"
  if [[ "$script_dir" != */bin ]]; then
    echo "unknown"
    return
  fi
  pkg="${script_dir%/bin}/package.json"
  # D78 reviewer P2-2: pass $pkg via env var instead of -c interpolation
  # so paths with apostrophes / shell metacharacters can't break the
  # python invocation. Canonical layout is safe; this is defense-in-depth.
  if [[ -f "$pkg" ]] && command -v python3 >/dev/null 2>&1; then
    OLP_PKG_PATH="$pkg" python3 -c 'import json,os;print(json.load(open(os.environ["OLP_PKG_PATH"])).get("version","unknown"))' 2>/dev/null || echo "unknown"
  else
    echo "unknown"
  fi
}
OLP_CONNECT_VERSION="$(_resolve_version)"

show_version() {
  echo "olp-connect $OLP_CONNECT_VERSION"
}

show_help() {
  cat <<'EOF'
olp-connect — Connect this machine to a remote OLP (Open LLM Proxy)

Configures OPENAI_BASE_URL + OPENAI_API_KEY in your shell rc file (and macOS
launchctl env / Linux systemd user env), then detects installed IDEs (Cline,
Continue.dev, Cursor, Aider, OpenClaw, Claude Code) and prints / writes the
provider-specific configuration each needs.

Usage:
  olp-connect <host-ip> [options]
  olp-connect --help
  olp-connect --version

Options:
  --port PORT       Port OLP listens on (default: 4567 — OLP v0.4.0+ default;
                    set 3456 if connecting to a pre-D60 OLP install)
  --key API_KEY     OLP API key (from `olp-keys keygen` on the server). When
                    omitted, the script reads /health.anonymousKey (if the
                    server opted in via auth.advertise_anonymous_key=true) or
                    prompts interactively.
  --no-system-env   Skip macOS launchctl setenv / Linux systemd env writes;
                    only update shell rc files.
  --dry-run         Print everything the script would do without modifying
                    any file or setting any env var.
  --version         Print version and exit
  --help, -h        Show this help

Examples:
  olp-connect 192.168.1.10
  olp-connect 192.168.1.10 --port 8080
  olp-connect 192.168.1.10 --key olp_AbcDef1234...
  olp-connect 100.64.0.5 --dry-run

Requires:
  bash, curl, python3 (for /health JSON parsing)

Exit codes:
  0   success
  1   bad arguments / unknown flag / missing required value
  2   connectivity or auth failure / smoke test failure

Authority: ADR 0010 § Phase 4 D68-D70; ADR 0011 (anonymous-key trusted-LAN
invariant — when --key is auto-resolved from /health.anonymousKey, this
deployment MUST be on a trusted LAN).
EOF
}

# ── Globals populated by main() ─────────────────────────────────────────────

DRY_RUN=false
NO_SYSTEM_ENV=false

# ── Logging helpers ─────────────────────────────────────────────────────────

log_info()  { echo "  $*"; }
log_step()  { echo "  → $*"; }
log_ok()    { echo "  ✓ $*"; }
log_warn()  { echo "  ⚠ $*"; }
log_err()   { echo "  ✗ $*" >&2; }

# Echo a state change (a write / env-set) before executing — operator can
# Ctrl-C if something looks wrong. Returns 0 always.
log_change() { echo "  • $*"; }

# ── IDE detection + configuration ───────────────────────────────────────────

# Truncate long keys for display (avoid leaking via screenshot / screen share).
key_display() {
  local k="$1"
  if [[ -z "$k" ]]; then
    echo "(none — anonymous; most IDEs require a non-empty API Key)"
  elif [[ ${#k} -gt 16 ]]; then
    echo "${k:0:8}...${k: -4}"
  else
    echo "$k"
  fi
}

# D74 P1-2: validate OLP API key format. Per ADR 0007 § 3, tokens are
# `olp_` + 32 random bytes base64url-encoded (43 chars, no padding). This
# regex pins the on-the-wire shape so a malformed or hostile `--key` /
# server-advertised `anonymousKey` never gets persisted into a shell rc.
# Returns 0 on valid, 1 on invalid (with diagnostic to stderr).
validate_olp_token() {
  local k="$1" source="$2"
  if [[ ! "$k" =~ ^olp_[A-Za-z0-9_-]{43}$ ]]; then
    log_err "Rejected $source: token format does not match ^olp_[A-Za-z0-9_-]{43}$ (ADR 0007 § 3)."
    log_err "  Got ${#k}-char value starting with '$(echo "$k" | cut -c1-8)...'"
    log_err "  Expected: olp_ followed by 43 base64url chars. Run 'npx olp-keys list' on the server"
    log_err "  to confirm the key format, or have the operator regenerate with 'npx olp-keys keygen'."
    return 1
  fi
  return 0
}

# D74 P1-2: POSIX shell-quote a value before interpolating into a shell rc
# write. Wraps in single quotes + escapes embedded single quotes per:
#   foo'bar  →  'foo'\''bar'
# Even with the validator above, this is defense-in-depth: any non-token
# string that slips through (e.g., environment.d KEY=VALUE writes) MUST be
# safe to source. Same helper pattern as lib/doctor.mjs _shellQuote.
shell_quote() {
  local s="$1"
  # Escape any single quotes: ' → '\''
  printf "'%s'" "${s//\'/\'\\\'\'}"
}

# Detect Claude Code and print warn-only message. Per ADR 0010 § Out of
# Phase 4 scope, OLP does NOT ship /v1/messages and CC is not a supported
# client. The user is steered toward Cline + OLP.
detect_claude_code() {
  if command -v claude &>/dev/null; then
    log_info ""
    log_info "Detected: Claude Code (`command -v claude`)"
    log_warn "Claude Code is NOT supported as an OLP client (OLP does not ship"
    log_warn "  /v1/messages — see ADR 0010 § Out-of-Phase-4-scope)."
    log_warn "  Recommended alternative: install Cline (VSCode extension) + OLP."
    log_warn "  Cline uses OpenAI-shape /v1/chat/completions which OLP DOES serve."
  fi
}

# Detect Cline VSCode extension and print manual-configure snippet.
# Cline cannot be auto-configured via env vars — user must paste into VSCode
# settings UI. We surface the values for them.
detect_cline() {
  local base_url="$1" key="$2"
  local exts=""
  if command -v code &>/dev/null; then
    exts=$(code --list-extensions 2>/dev/null || true)
  fi
  if [[ -z "$exts" && -d "$HOME/.vscode/extensions" ]]; then
    exts=$(ls "$HOME/.vscode/extensions/" 2>/dev/null || true)
  fi

  if echo "$exts" | grep -qiE 'cline|saoudrizwan\.claude-dev'; then
    log_info ""
    log_info "Detected: Cline (VSCode extension)"
    log_info "  Cline must be configured via the VSCode settings UI."
    log_info "  Open VSCode → Cline panel → Settings → API Provider:"
    log_info "    API Provider:  \"OpenAI Compatible\""
    log_info "    Base URL:      $base_url/v1"
    log_info "    API Key:       $(key_display "$key")"
    log_info "    Model ID:      claude-sonnet-4-5 (or any model from /v1/models)"
  fi
}

# Detect Continue.dev and write a `models:` entry to ~/.continue/config.yaml
# (idempotent — checks if an entry with the same name exists first).
detect_continue() {
  local base_url="$1" key="$2"
  local exts=""
  if command -v code &>/dev/null; then
    exts=$(code --list-extensions 2>/dev/null || true)
  fi
  local config_yaml="$HOME/.continue/config.yaml"
  local config_json="$HOME/.continue/config.json"

  local found=false
  if echo "$exts" | grep -qi 'continue\.continue'; then found=true; fi
  if [[ -f "$config_yaml" || -f "$config_json" ]]; then found=true; fi

  if ! $found; then return 0; fi

  log_info ""
  log_info "Detected: Continue.dev"
  log_info "  Configuration snippet for ~/.continue/config.yaml:"
  log_info "    models:"
  log_info "      - name: OLP Sonnet"
  log_info "        provider: openai"
  log_info "        model: claude-sonnet-4-5"
  log_info "        apiBase: $base_url/v1"
  log_info "        apiKey: $(key_display "$key")"
  log_info "  Note: Continue.dev autoreload-on-save is fragile; restart VSCode if"
  log_info "        the new model doesn't appear in the model selector."
}

# Detect Cursor and print manual snippet + known-fragility warning.
detect_cursor() {
  local base_url="$1" key="$2"
  local found=false
  if command -v cursor &>/dev/null; then found=true; fi
  if [[ -d "$HOME/.cursor" ]]; then found=true; fi
  if [[ -d "/Applications/Cursor.app" ]]; then found=true; fi

  if ! $found; then return 0; fi

  log_info ""
  log_info "Detected: Cursor"
  log_info "  Cmd+Shift+P → 'Cursor Settings' → Models:"
  log_info "    OpenAI API Key:           $(key_display "$key")"
  log_info "    Override OpenAI Base URL: $base_url/v1"
  log_info "    Custom OpenAI Models:     claude-sonnet-4-5,claude-opus-4-1"
  log_warn "  Cursor's base-URL handling is known-fragile (issue #7128 et al);"
  log_warn "  if requests fail with 'malformed request', try removing then"
  log_warn "  re-adding the model in the Cursor models list."
}

# Detect Aider and write OPENAI_API_BASE / OPENAI_API_KEY to rc files.
# Aider reads these env vars at startup — already handled by the rc-file
# block in main(). We just announce detection here.
detect_aider() {
  if command -v aider &>/dev/null; then
    log_info ""
    log_info "Detected: Aider (`command -v aider`)"
    log_info "  Aider reads OPENAI_API_BASE + OPENAI_API_KEY from env."
    log_info "  These are already being written to your shell rc — open a fresh"
    log_info "  shell and run: aider --model openai/claude-sonnet-4-5"
  fi
}

# Detect OpenClaw. Phase 4 D71-D73 shipped olp-plugin/ as the OpenClaw
# gateway plugin for /olp Telegram + Discord slash commands. Point users
# at the install path.
detect_openclaw() {
  if command -v openclaw &>/dev/null || [[ -f "$HOME/.openclaw/openclaw.json" ]]; then
    log_info ""
    log_info "Detected: OpenClaw"
    log_info "  OLP ships an OpenClaw gateway plugin for /olp Telegram + Discord"
    log_info "  slash commands (status / usage / cache / models / providers /"
    log_info "  chain show / health / doctor). Read-only by design — no chat-side"
    log_info "  mutations."
    log_info ""
    log_info "  Install the plugin (one-time, on the host running OpenClaw):"
    log_info "    git clone https://github.com/dtzp555-max/olp.git /tmp/olp-repo"
    log_info "    openclaw plugins install /tmp/olp-repo/olp-plugin"
    log_info "    # OR symlink: ln -sf /tmp/olp-repo/olp-plugin ~/.openclaw/extensions/olp"
    log_info ""
    log_info "  Then edit ~/.openclaw/openclaw.json to set the plugin apiKey to a"
    log_info "  dedicated OLP key (NOT your owner key — create one via olp-keys"
    log_info "  keygen --name <bot-name>). Restart OpenClaw gateway."
    log_info ""
    log_info "  See docs/integrations/openclaw.md for full instructions."
  fi
}

# ── rc-file helpers ─────────────────────────────────────────────────────────

# Identify which shell rc files to write to. Returns paths on stdout, one per line.
detect_rc_files() {
  local is_mac=false
  [[ "$(uname)" == "Darwin" ]] && is_mac=true
  if [[ "${SHELL:-}" == */fish ]]; then
    log_warn "fish shell detected; writing to ~/.bashrc — add to fish config manually." >&2
    echo "$HOME/.bashrc"
    return
  fi
  if $is_mac; then
    # macOS Catalina+ default shell is zsh
    [[ -f "$HOME/.bashrc" ]] && echo "$HOME/.bashrc"
    [[ -f "$HOME/.zshrc" ]] || { $DRY_RUN || touch "$HOME/.zshrc"; }
    echo "$HOME/.zshrc"
  else
    [[ -f "$HOME/.bashrc" || "${SHELL:-}" == */bash ]] && echo "$HOME/.bashrc"
    [[ -f "$HOME/.zshrc" || "${SHELL:-}" == */zsh ]] && echo "$HOME/.zshrc"
  fi
}

# Remove any previously-written OLP block from an rc file (idempotent).
# The block is bracketed by:
#   # OLP LAN (added by olp-connect) ... # /OLP LAN
strip_olp_block() {
  local rc_file="$1"
  [[ -f "$rc_file" ]] || return 0
  if $DRY_RUN; then
    if grep -qF '# OLP LAN (added by olp-connect)' "$rc_file" 2>/dev/null; then
      log_change "[dry-run] would strip existing OLP block from $rc_file"
    fi
    return 0
  fi
  python3 - "$rc_file" <<'PYEOF'
import sys
path = sys.argv[1]
try:
    with open(path) as f:
        lines = f.readlines()
except OSError:
    sys.exit(0)
out = []
skip = False
for line in lines:
    s = line.rstrip('\n')
    if s == '# OLP LAN (added by olp-connect)':
        skip = True
        continue
    if skip and s == '# /OLP LAN':
        skip = False
        continue
    if skip:
        continue
    out.append(line)
with open(path, 'w') as f:
    f.writelines(out)
PYEOF
}

# Append a new OLP block to an rc file.
append_olp_block() {
  local rc_file="$1" base_url="$2" key="$3"
  if $DRY_RUN; then
    log_change "[dry-run] would append OLP block to $rc_file:"
    log_change "    # OLP LAN (added by olp-connect)"
    log_change "    export OPENAI_BASE_URL=$(shell_quote "$base_url/v1")"
    [[ -n "$key" ]] && log_change "    export OPENAI_API_KEY=$(shell_quote "$(key_display "$key")")"
    log_change "    # /OLP LAN"
    return 0
  fi
  # D74 P1-2: shell-quote values before writing to rc files. Defense-in-depth
  # alongside validate_olp_token — even if a future code path bypasses the
  # validator, the rc file remains safe to source.
  {
    echo ""
    echo "# OLP LAN (added by olp-connect)"
    echo "export OPENAI_BASE_URL=$(shell_quote "$base_url/v1")"
    if [[ -n "$key" ]]; then
      echo "export OPENAI_API_KEY=$(shell_quote "$key")"
    fi
    echo "# /OLP LAN"
  } >> "$rc_file"
}

# ── System-level env (macOS launchctl / Linux systemd user) ────────────────

set_system_env() {
  local base_url="$1" key="$2"
  if $NO_SYSTEM_ENV; then
    log_info "Skipping system-level env (--no-system-env)"
    return 0
  fi
  if [[ "$(uname)" == "Darwin" ]]; then
    if $DRY_RUN; then
      log_change "[dry-run] would launchctl setenv OPENAI_BASE_URL=$base_url/v1"
      [[ -n "$key" ]] && log_change "[dry-run] would launchctl setenv OPENAI_API_KEY=$(key_display "$key")"
      return 0
    fi
    launchctl setenv OPENAI_BASE_URL "$base_url/v1" 2>/dev/null || log_warn "launchctl setenv OPENAI_BASE_URL failed"
    if [[ -n "$key" ]]; then
      launchctl setenv OPENAI_API_KEY "$key" 2>/dev/null || log_warn "launchctl setenv OPENAI_API_KEY failed"
    fi
    log_ok "launchctl setenv applied (visible to GUI apps + daemons)"
    log_info "  Note: launchctl env vars reset on reboot. Re-run olp-connect after restart"
    log_info "        or add the script to Login Items."
  else
    local env_dir="$HOME/.config/environment.d"
    if $DRY_RUN; then
      log_change "[dry-run] would write $env_dir/olp.conf"
      return 0
    fi
    mkdir -p "$env_dir" 2>/dev/null
    # D74 P1-2: systemd environment.d format is KEY=VALUE per line. While
    # systemd does its own parsing (no shell sourcing), reject embedded
    # newlines defensively — validate_olp_token already enforces the
    # restricted charset for the API key, so this is belt-and-braces.
    if [[ "$base_url" == *$'\n'* || "$key" == *$'\n'* ]]; then
      log_err "Refusing to write environment.d entry: value contains newline."
      return 2
    fi
    {
      echo "OPENAI_BASE_URL=$base_url/v1"
      if [[ -n "$key" ]]; then
        echo "OPENAI_API_KEY=$key"
      fi
    } > "$env_dir/olp.conf"
    log_ok "Wrote $env_dir/olp.conf (applies to systemd user services after re-login)"
  fi
}

# ── Main ────────────────────────────────────────────────────────────────────

main() {
  local host="" port=4567 key=""

  # Parse args (POSIX-style; --flag value AND --flag=value both accepted)
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --port)    port="${2:?--port requires a value}"; shift 2 ;;
      --port=*)  port="${1#*=}"; shift ;;
      --key)     key="${2:?--key requires a value}"
                 [[ -z "$key" ]] && { log_err "--key cannot be empty (omit --key for zero-config / auto-discovery)"; exit 1; }
                 # D74 P1-2: reject malformed --key before it ever reaches an rc write.
                 validate_olp_token "$key" "--key flag" || exit 1
                 shift 2 ;;
      --key=*)   key="${1#*=}"
                 [[ -z "$key" ]] && { log_err "--key cannot be empty (omit --key for zero-config / auto-discovery)"; exit 1; }
                 validate_olp_token "$key" "--key flag" || exit 1
                 shift ;;
      --no-system-env) NO_SYSTEM_ENV=true; shift ;;
      --dry-run) DRY_RUN=true; shift ;;
      --version) show_version; exit 0 ;;
      --help|-h) show_help; exit 0 ;;
      --*)       log_err "Unknown option: $1"; show_help >&2; exit 1 ;;
      *)         host="$1"; shift ;;
    esac
  done

  if [[ -z "$host" ]]; then
    log_err "host IP is required."
    echo "" >&2
    show_help >&2
    exit 1
  fi

  if ! [[ "$host" =~ ^[a-zA-Z0-9._-]+$ ]]; then
    log_err "invalid host '$host'"
    exit 1
  fi

  # Dependency check
  for cmd in curl python3; do
    if ! command -v "$cmd" &>/dev/null; then
      log_err "'$cmd' is required but not found in PATH."
      [[ "$cmd" == "python3" ]] && log_err "  python3 is used for /health + /v1/models JSON parsing."
      exit 1
    fi
  done

  local base_url="http://$host:$port"

  echo "olp-connect v$OLP_CONNECT_VERSION"
  echo "─────────────────────────────────────"
  log_info "Remote: $base_url"
  $DRY_RUN && log_info "Mode:   DRY RUN (no files will be modified, no env vars will be set)"
  echo ""

  # Step 1: connectivity probe. We capture status separately from body so we
  # can distinguish "TCP/HTTP unreachable" from "reached but 401" (the latter
  # is a known surface when the server has auth.allow_anonymous=false AND
  # auth.advertise_anonymous_key=false — user MUST provide --key).
  log_step "Probing /health..."
  local probe_body probe_status
  probe_body=$(curl -s --max-time 5 -o /tmp/olp-connect-health.$$ -w "%{http_code}" "$base_url/health" 2>/dev/null || echo "000")
  probe_status="$probe_body"
  if [[ -f /tmp/olp-connect-health.$$ ]]; then
    probe_body=$(cat /tmp/olp-connect-health.$$ 2>/dev/null || echo "")
    rm -f /tmp/olp-connect-health.$$
  fi
  if [[ "$probe_status" == "000" ]]; then
    log_err "Cannot reach $base_url/health (connection refused / timeout / DNS)"
    log_err "  Ensure OLP is running on $host and bound to 0.0.0.0 (LAN mode)."
    log_err "  Default port changed 3456 → 4567 at OLP v0.4.0; pass --port 3456 for older installs."
    exit 2
  fi
  if [[ "$probe_status" == "401" ]]; then
    log_warn "Server reachable but /health returned 401."
    log_warn "  Either the operator has not enabled auth.advertise_anonymous_key, or"
    log_warn "  the server requires auth (auth.allow_anonymous=false)."
    if [[ -z "$key" ]]; then
      log_err "  Pass --key olp_... to continue, or ask the operator to advertise an anonymous key (see ADR 0011)."
      exit 2
    fi
    # If user supplied --key, we proceed without /health body (auth-required mode).
    log_info "  Proceeding with the --key you supplied; skipping /health.anonymousKey discovery."
    local health_json=""
    local remote_version="?"
  else
    if [[ "$probe_status" != "200" ]]; then
      log_err "/health returned HTTP $probe_status (expected 200 or 401)."
      exit 2
    fi
    local health_json="$probe_body"
    local remote_version
    remote_version=$(echo "$health_json" | python3 -c "import sys,json
try: print(json.loads(sys.stdin.read()).get('version','?'))
except: print('?')" 2>/dev/null || echo "?")
    log_ok "Connected — OLP v$remote_version"
  fi

  # Step 2: auth resolution
  if [[ -z "$key" ]]; then
    # Try /health.anonymousKey first (D69 / ADR 0011 opt-in).
    local anon_key
    anon_key=$(echo "$health_json" | python3 -c "import sys,json
try:
    d = json.loads(sys.stdin.read())
    k = d.get('anonymousKey')
    print(k if isinstance(k, str) and k else '')
except: print('')" 2>/dev/null || echo "")
    if [[ -n "$anon_key" ]]; then
      # D74 P1-2: validate server-advertised token shape before consuming.
      # A hostile or misconfigured server could otherwise inject arbitrary
      # strings into the user's rc file via the `anonymousKey` field.
      if ! validate_olp_token "$anon_key" "/health.anonymousKey from ${host}:${port}"; then
        log_err "Refusing to consume malformed advertised key. Use --key explicitly or contact the OLP operator."
        exit 2
      fi
      key="$anon_key"
      log_ok "Using server-advertised anonymous key: $(key_display "$key")"
      log_info "  (set by remote via auth.advertise_anonymous_key=true; see ADR 0011 for"
      log_info "   the trusted-LAN-only invariant — this assumes you and the remote are"
      log_info "   on the same trusted network)"
    else
      # No advertised key; prompt interactively (skip in dry-run for non-TTY safety)
      if $DRY_RUN; then
        log_info "[dry-run] would prompt for API key here (no --key + no anonymousKey)"
        key="<prompted-at-runtime>"
      else
        echo ""
        log_info "Remote does not advertise an anonymous key."
        log_info "Ask the OLP operator to run on the server:  olp-keys keygen --name <your-label>"
        printf "  Enter OLP API key: "
        { read -rs key </dev/tty; } 2>/dev/null || key=""
        echo
        if [[ -z "$key" ]]; then
          log_err "No key provided and the remote did not advertise an anonymous key."
          log_err "  Re-run with: olp-connect $host --key olp_..."
          exit 2
        fi
        # D74 P1-2: also validate the interactively-prompted key.
        validate_olp_token "$key" "interactive prompt" || exit 1
      fi
    fi
  fi

  # Step 3: smoke test /v1/models
  log_step "Smoke-testing /v1/models..."
  if $DRY_RUN && [[ "$key" == "<prompted-at-runtime>" ]]; then
    log_info "[dry-run] skipping smoke test (no real key)"
  else
    local models_out models_ok=0
    if [[ -n "$key" ]]; then
      models_out=$(curl -sf --max-time 10 \
        -H "Authorization: Bearer $key" \
        "$base_url/v1/models" 2>/dev/null) && models_ok=1
    else
      models_out=$(curl -sf --max-time 10 "$base_url/v1/models" 2>/dev/null) && models_ok=1
    fi
    if [[ $models_ok -eq 0 ]]; then
      log_err "/v1/models request failed — key may be invalid, revoked, or not allowed for any provider."
      exit 2
    fi
    local model_count
    model_count=$(echo "$models_out" | python3 -c "import sys,json
try: print(len(json.loads(sys.stdin.read()).get('data', [])))
except: print('?')" 2>/dev/null || echo "?")
    log_ok "/v1/models OK — $model_count models available"
  fi
  echo ""

  # Step 4: write shell rc files
  log_step "Writing shell rc files..."
  local rc_files=()
  while IFS= read -r line; do
    [[ -n "$line" ]] && rc_files+=("$line")
  done < <(detect_rc_files)

  if [[ ${#rc_files[@]} -eq 0 ]]; then
    log_warn "No shell rc files detected; falling back to ~/.bashrc"
    rc_files=("$HOME/.bashrc")
  fi

  for rc_file in "${rc_files[@]}"; do
    log_change "stripping old OLP block from $(basename "$rc_file") (idempotent)"
    strip_olp_block "$rc_file"
    log_change "appending new OLP block to $(basename "$rc_file")"
    append_olp_block "$rc_file" "$base_url" "$key"
  done
  log_ok "Shell rc files updated:"
  for rc_file in "${rc_files[@]}"; do
    log_info "  $rc_file"
  done
  echo ""

  # Step 5: system-level env (macOS launchctl / Linux systemd)
  log_step "Setting system-level env..."
  set_system_env "$base_url" "$key"
  echo ""

  # Step 6: IDE detection + per-IDE config
  log_step "Detecting installed IDEs..."
  detect_claude_code
  detect_cline    "$base_url" "$key"
  detect_continue "$base_url" "$key"
  detect_cursor   "$base_url" "$key"
  detect_aider
  detect_openclaw
  echo ""

  # Step 7: final summary
  log_step "Done."
  log_info "OLP base URL:   $base_url/v1"
  log_info "OLP API key:    $(key_display "$key")"
  log_info ""
  log_info "Test it: open a fresh shell, run:"
  log_info "  curl -sf -H \"Authorization: Bearer \$OPENAI_API_KEY\" $base_url/v1/models | python3 -m json.tool | head -20"
  log_info ""
  log_info "Reload your current shell to apply env changes:"
  for rc_file in "${rc_files[@]}"; do
    log_info "  source $rc_file"
  done
}

main "$@"
