#!/usr/bin/env bash
# =============================================================================
# Tokenwise Connect — one-line installer (v1)
#
#   curl -fsSL https://tokenwisehq.com/connect.sh | bash
#
# Routes every LLM call your local AI tools make (Claude Code, Claude CLI,
# Codex CLI, Cursor) through Tokenwise's edge proxy so you can observe + save
# 30%+ on LLM spend. The installer does NOT read, display, or transmit your
# existing provider keys — pass-through means your IDE auth stays in your IDE.
#
# What it does:
#   1. Pairs this device to your Tokenwise workspace via device-code flow.
#   2. Detects installed AI IDEs / CLIs.
#   3. Patches each one's config to point its BASE_URL at the Tokenwise proxy.
#   4. Leaves all original config files backed up as `<file>.tokenwise.bak`.
#
# Source     : https://tokenwisehq.com/connect.sh
# Uninstall  : curl -fsSL https://tokenwisehq.com/disconnect.sh | bash
# Project    : https://tokenwisehq.com
# Build date : 2026-05-27
# License    : MIT — see https://opensource.org/licenses/MIT
#
# Flags:
#   --yes              Skip all interactive [Y/n] prompts (good for CI).
#   --dry-run          Print every action that WOULD be taken; touch nothing.
#   --api-base=URL     Override the Tokenwise API base (default
#                      https://tokenwisehq.com; env var TW_API_BASE wins).
#   --help, -h         Show this help text and exit.
#
# Idempotency: re-running the script is safe. Patches are wrapped in
#   `# tokenwise-connect: START <ide>` / `# tokenwise-connect: END <ide>`
# markers so contents are replaced rather than appended a second time.
# =============================================================================

set -euo pipefail

# ─── Constants ────────────────────────────────────────────────────────────────
SCRIPT_VERSION="1"
DEFAULT_API_BASE="https://tokenwisehq.com"
PROXY_BASE="https://proxy.tokenwisehq.com"
TW_DIR="$HOME/.tokenwise"
TOKEN_FILE="$TW_DIR/route-token"

# ─── Pretty output (only when stdout is a TTY) ────────────────────────────────
if [ -t 1 ]; then
  BOLD=$'\033[1m'
  DIM=$'\033[2m'
  GREEN=$'\033[32m'
  YELLOW=$'\033[33m'
  RED=$'\033[31m'
  CYAN=$'\033[36m'
  RESET=$'\033[0m'
else
  BOLD=""
  DIM=""
  GREEN=""
  YELLOW=""
  RED=""
  CYAN=""
  RESET=""
fi

log()  { printf "%s\n" "$1"; }
info() { printf "%s%s%s\n" "$CYAN" "$1" "$RESET"; }
ok()   { printf "%s✓%s %s\n" "$GREEN" "$RESET" "$1"; }
warn() { printf "%s!%s %s\n" "$YELLOW" "$RESET" "$1" >&2; }
err()  { printf "%serror:%s %s\n" "$RED" "$RESET" "$1" >&2; }
skip() { printf "%s⊘%s %s\n" "$DIM" "$RESET" "$1"; }

# ─── Help ─────────────────────────────────────────────────────────────────────
#
# Heredoc rather than self-parse so `--help` still works when the script is
# piped via `curl ... | bash -s -- --help` (in which case $0 is "bash" and
# `sed "$0"` would crash).
show_help() {
  cat <<'HELPEOF'
Tokenwise Connect — one-line installer (v1)

  curl -fsSL https://tokenwisehq.com/connect.sh | bash

Routes every LLM call your local AI tools make (Claude Code, Claude CLI,
Codex CLI, Cursor) through Tokenwise's edge proxy so you can observe + save
30%+ on LLM spend. The installer does NOT read, display, or transmit your
existing provider keys — pass-through means your IDE auth stays in your IDE.

What it does:
  1. Pairs this device to your Tokenwise workspace via device-code flow.
  2. Detects installed AI IDEs / CLIs.
  3. Patches each one's config to point its BASE_URL at the Tokenwise proxy.
  4. Leaves all original config files backed up as `<file>.tokenwise.bak`.

Flags:
  --yes              Skip all interactive [Y/n] prompts (good for CI).
  --dry-run          Print every action that WOULD be taken; touch nothing.
  --api-base=URL     Override the Tokenwise API base (default
                     https://tokenwisehq.com; env var TW_API_BASE wins).
  --help, -h         Show this help text and exit.

Idempotency: re-running the script is safe. Patches are wrapped in
  # tokenwise-connect: START <ide> / # tokenwise-connect: END <ide>
markers so contents are replaced rather than appended a second time.

Uninstall: curl -fsSL https://tokenwisehq.com/disconnect.sh | bash
Source:    https://tokenwisehq.com/connect.sh
Project:   https://tokenwisehq.com
HELPEOF
}

# ─── Flag parsing ─────────────────────────────────────────────────────────────
YES=0
DRY_RUN=0
CLI_API_BASE=""

for arg in "$@"; do
  case "$arg" in
    --yes|-y) YES=1 ;;
    --dry-run|-n) DRY_RUN=1 ;;
    --api-base=*) CLI_API_BASE="${arg#--api-base=}" ;;
    --help|-h)
      show_help
      exit 0
      ;;
    *)
      err "unknown flag: $arg"
      printf >&2 "Run with --help for usage.\n"
      exit 2
      ;;
  esac
done

# TW_API_BASE env wins; then --api-base=; then default.
TW_API_BASE="${TW_API_BASE:-${CLI_API_BASE:-$DEFAULT_API_BASE}}"

# Strip trailing slash so we can concat paths cleanly.
TW_API_BASE="${TW_API_BASE%/}"

# ─── Trap / cleanup ───────────────────────────────────────────────────────────
TMP=""
cleanup() {
  if [ -n "${TMP:-}" ] && [ -d "$TMP" ]; then
    rm -rf "$TMP"
  fi
}
on_error() {
  local code=$?
  err "aborted (exit code $code). No further changes were made."
  exit "$code"
}
trap cleanup EXIT
trap on_error ERR INT TERM

TMP="$(mktemp -d 2>/dev/null || mktemp -d -t 'tw-connect')"

# ─── Banner + SHA256 self-fingerprint ─────────────────────────────────────────
sha_self() {
  # $0 may be /dev/stdin when piped from curl; only fingerprint a real file.
  local src="${BASH_SOURCE[0]:-$0}"
  if [ -f "$src" ] && [ -r "$src" ]; then
    if command -v sha256sum >/dev/null 2>&1; then
      sha256sum "$src" | awk '{print $1}'
      return
    fi
    if command -v shasum >/dev/null 2>&1; then
      shasum -a 256 "$src" | awk '{print $1}'
      return
    fi
  fi
  printf "(unavailable — script was streamed from stdin)"
}

printf "%s┌─ Tokenwise Connect installer v%s ─┐%s\n" "$BOLD" "$SCRIPT_VERSION" "$RESET"
printf "%s└─ %s%s\n" "$DIM" "$TW_API_BASE" "$RESET"
printf "  sha256: %s\n\n" "$(sha_self)"

if [ "$DRY_RUN" -eq 1 ]; then
  warn "DRY RUN — no files will be modified."
fi

# ─── Step 1 — verify prerequisites ────────────────────────────────────────────
missing=""
add_missing() {
  if [ -n "$missing" ]; then
    missing="$missing, $1"
  else
    missing="$1"
  fi
}

if ! command -v curl >/dev/null 2>&1; then add_missing "curl"; fi

JSON_TOOL=""
if command -v jq >/dev/null 2>&1; then
  JSON_TOOL="jq"
elif command -v python3 >/dev/null 2>&1; then
  JSON_TOOL="python3"
  warn "jq not found — falling back to python3 for JSON parsing."
else
  add_missing "jq (or python3)"
fi

if [ -n "$missing" ]; then
  err "missing required tool(s): $missing"
  printf >&2 "\nInstall with one of:\n"
  printf >&2 "  macOS (Homebrew) : brew install %s\n" "${missing//,/}"
  printf >&2 "  Debian / Ubuntu  : sudo apt-get install -y %s\n" "${missing//,/}"
  printf >&2 "  Fedora / RHEL    : sudo dnf install -y %s\n" "${missing//,/}"
  printf >&2 "  Arch / Manjaro   : sudo pacman -S --needed %s\n" "${missing//,/}"
  exit 1
fi

# ─── JSON helpers (jq preferred, python3 fallback) ────────────────────────────
json_get() {
  # json_get <json-string> <key>
  local data="$1" key="$2"
  if [ "$JSON_TOOL" = "jq" ]; then
    printf "%s" "$data" | jq -r --arg k "$key" '.[$k] // empty'
  else
    printf "%s" "$data" | python3 -c '
import json, sys
try:
  d = json.load(sys.stdin)
  v = d.get(sys.argv[1])
  if v is None:
    pass
  else:
    print(v, end="")
except Exception:
  pass
' "$key"
  fi
}

json_set_string() {
  # json_set_string <file> <key> <value>  → writes new JSON to file (in-place)
  local file="$1" key="$2" value="$3"
  if [ "$JSON_TOOL" = "jq" ]; then
    local tmp="$file.tw-tmp"
    jq --arg k "$key" --arg v "$value" '. + {($k): $v}' "$file" > "$tmp"
    mv "$tmp" "$file"
  else
    python3 - "$file" "$key" "$value" <<'PYEOF'
import json, sys
file, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
try:
  with open(file, "r") as f:
    data = json.load(f)
except Exception:
  data = {}
if not isinstance(data, dict):
  raise SystemExit("settings.json root is not an object; refusing to patch.")
data[key] = val
with open(file, "w") as f:
  json.dump(data, f, indent=2)
  f.write("\n")
PYEOF
  fi
}

# Build a JSON object from key/value pairs. Used to safely encode the
# device-code payloads so a hostname containing a quote, backslash, or
# control character can't break the JSON.
#   json_object key1 value1 key2 value2 ...
json_object() {
  if [ "$JSON_TOOL" = "jq" ]; then
    local args=() filter='{}' i=0
    while [ "$#" -ge 2 ]; do
      args+=(--arg "k$i" "$1" --arg "v$i" "$2")
      if [ "$i" -eq 0 ]; then
        filter="{(\$k$i): \$v$i}"
      else
        filter="$filter + {(\$k$i): \$v$i}"
      fi
      i=$((i + 1))
      shift 2
    done
    # -c => compact, single-line output for nicer logging.
    jq -nc "${args[@]}" "$filter"
  else
    python3 - "$@" <<'PYEOF'
import json, sys
args = sys.argv[1:]
out = {}
for i in range(0, len(args), 2):
    out[args[i]] = args[i + 1]
sys.stdout.write(json.dumps(out))
PYEOF
  fi
}

# ─── OS + arch detection ──────────────────────────────────────────────────────
OS_KERNEL="$(uname -s)"
OS_ARCH="$(uname -m)"
IS_WSL=0
case "$OS_KERNEL" in
  Darwin) OS_LABEL="macOS $OS_ARCH" ;;
  Linux)
    if [ -r /proc/sys/kernel/osrelease ] && grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; then
      IS_WSL=1
      OS_LABEL="Linux $OS_ARCH (WSL)"
    else
      OS_LABEL="Linux $OS_ARCH"
    fi
    ;;
  *) OS_LABEL="$OS_KERNEL $OS_ARCH" ;;
esac
info "Detected: $OS_LABEL"
printf "\n"

# ─── Confirm helper ───────────────────────────────────────────────────────────
confirm() {
  # confirm "<prompt text>"  → returns 0 if yes, 1 if no
  local prompt="$1"
  if [ "$YES" -eq 1 ]; then
    printf "  %s [Y/n] %s(auto-yes)%s\n" "$prompt" "$DIM" "$RESET"
    return 0
  fi
  local answer
  # When piped via curl|bash, stdin (fd 0) is the script body — we can't
  # read user input from it. But the terminal itself is usually still
  # reachable via /dev/tty. Try that first.
  if [ ! -t 0 ] && [ -r /dev/tty ] && [ -w /dev/tty ]; then
    printf "  %s [Y/n] " "$prompt" > /dev/tty
    read -r answer < /dev/tty
  elif [ -t 0 ]; then
    printf "  %s [Y/n] " "$prompt"
    read -r answer
  else
    # No terminal at all (CI / non-interactive container). Default yes.
    printf "  %s [Y/n] %s(no TTY — default yes)%s\n" "$prompt" "$DIM" "$RESET"
    return 0
  fi
  case "$answer" in
    ""|y|Y|yes|YES|Yes) return 0 ;;
    *) return 1 ;;
  esac
}

# ─── Curl with strict TLS ─────────────────────────────────────────────────────
secure_curl() {
  # Wrap curl with strict-TLS flags. All callers go through this.
  #
  # The TW_INSECURE_LOCALHOST escape hatch exists ONLY for local smoke tests
  # against a 127.0.0.1 mock server. It is intentionally undocumented in the
  # public --help so it can't be social-engineered into someone's run.
  if [ "${TW_INSECURE_LOCALHOST:-0}" = "1" ] && [[ "$TW_API_BASE" == http://127.0.0.1:* || "$TW_API_BASE" == http://localhost:* ]]; then
    curl --fail --silent --show-error "$@"
  else
    curl --fail --proto =https --tlsv1.2 --silent --show-error "$@"
  fi
}

# ─── Step 2 — device-code pairing ─────────────────────────────────────────────
HOSTNAME_VAL="$(hostname 2>/dev/null || echo 'unknown-host')"
DEVICE_OS="${OS_KERNEL}-${OS_ARCH}"

# Build payload via json_object so a hostname containing quotes/backslashes
# can't produce invalid JSON.
start_payload="$(json_object deviceName "$HOSTNAME_VAL" deviceOs "$DEVICE_OS")"

ROUTE_TOKEN=""
WORKSPACE_NAME=""
DEVICE_NAME=""

if [ "$DRY_RUN" -eq 1 ]; then
  info "[dry-run] POST $TW_API_BASE/api/v1/device-code/start"
  log "          payload: $start_payload"
  log "          would poll /api/v1/device-code/poll until approved."
  ROUTE_TOKEN="tw_route_DRYRUN0000000000000000"
  WORKSPACE_NAME="<dry-run workspace>"
  DEVICE_NAME="$HOSTNAME_VAL"
else
  info "Requesting a one-time device code from Tokenwise..."
  start_resp="$(secure_curl \
    -X POST \
    -H 'Content-Type: application/json' \
    -H "User-Agent: tokenwise-connect/$SCRIPT_VERSION" \
    --data "$start_payload" \
    "$TW_API_BASE/api/v1/device-code/start")"

  USER_CODE="$(json_get "$start_resp" "code")"
  DEVICE_CODE="$(json_get "$start_resp" "deviceCode")"
  VERIFICATION_URL="$(json_get "$start_resp" "verificationUrl")"
  EXPIRES_IN="$(json_get "$start_resp" "expiresIn")"
  POLL_INTERVAL="$(json_get "$start_resp" "pollIntervalSeconds")"

  if [ -z "$DEVICE_CODE" ] || [ -z "$USER_CODE" ] || [ -z "$VERIFICATION_URL" ]; then
    err "device-code/start did not return the expected fields."
    err "response: $start_resp"
    exit 1
  fi

  # Sensible numeric defaults if backend omits them.
  case "$POLL_INTERVAL" in ''|*[!0-9]*) POLL_INTERVAL=2 ;; esac
  case "$EXPIRES_IN"    in ''|*[!0-9]*) EXPIRES_IN=600 ;; esac

  printf "\n"
  printf "  %sOpen this URL in your browser to authorize:%s\n" "$BOLD" "$RESET"
  printf "  %s%s%s\n\n" "$CYAN" "$VERIFICATION_URL" "$RESET"
  printf "  Your one-time code:  %s%s%s\n" "$BOLD" "$USER_CODE" "$RESET"
  printf "  (expires in %ss)\n\n" "$EXPIRES_IN"

  # Try to auto-open the browser. Failure is non-fatal. We background-detach
  # the launch so a hanging xdg-open/wslview can never wedge the installer.
  open_url() {
    local url="$1"
    if [ "$IS_WSL" -eq 1 ] && command -v wslview >/dev/null 2>&1; then
      ( wslview "$url" >/dev/null 2>&1 & ) || true
    elif [ "$OS_KERNEL" = "Darwin" ] && command -v open >/dev/null 2>&1; then
      ( open "$url" >/dev/null 2>&1 & ) || true
    elif command -v xdg-open >/dev/null 2>&1; then
      ( xdg-open "$url" >/dev/null 2>&1 & ) || true
    fi
  }
  if [ "${TW_NO_OPEN:-0}" != "1" ]; then
    open_url "$VERIFICATION_URL"
  fi

  poll_payload="$(json_object deviceCode "$DEVICE_CODE")"
  start_time=$(date +%s)
  backoff="$POLL_INTERVAL"

  info "Waiting for approval..."
  while :; do
    now=$(date +%s)
    if [ "$((now - start_time))" -ge "$EXPIRES_IN" ]; then
      err "Device code expired before approval. Re-run the installer."
      exit 1
    fi

    # We want to inspect non-2xx (e.g. 429) bodies, so don't use --fail here.
    if [ "${TW_INSECURE_LOCALHOST:-0}" = "1" ] && [[ "$TW_API_BASE" == http://127.0.0.1:* || "$TW_API_BASE" == http://localhost:* ]]; then
      poll_tls_flags=(--silent --show-error)
    else
      poll_tls_flags=(--proto =https --tlsv1.2 --silent --show-error)
    fi
    http_code="$(curl "${poll_tls_flags[@]}" \
      -o "$TMP/poll.body" -w '%{http_code}' \
      -X POST \
      -H 'Content-Type: application/json' \
      -H "User-Agent: tokenwise-connect/$SCRIPT_VERSION" \
      --data "$poll_payload" \
      "$TW_API_BASE/api/v1/device-code/poll" || true)"

    poll_body="$(cat "$TMP/poll.body" 2>/dev/null || true)"

    case "$http_code" in
      200)
        status="$(json_get "$poll_body" "status")"
        case "$status" in
          approved)
            ROUTE_TOKEN="$(json_get "$poll_body" "routeToken")"
            WORKSPACE_NAME="$(json_get "$poll_body" "workspaceName")"
            DEVICE_NAME="$(json_get "$poll_body" "deviceName")"
            if [ -z "$ROUTE_TOKEN" ]; then
              err "approved response did not include a routeToken."
              exit 1
            fi
            break
            ;;
          pending|"")
            sleep "$backoff"
            ;;
          expired)
            err "Device code expired. Re-run the installer to start over."
            exit 1
            ;;
          denied)
            err "Authorization was denied in the browser. Aborting."
            exit 1
            ;;
          *)
            warn "Unknown poll status \"$status\". Retrying."
            sleep "$backoff"
            ;;
        esac
        ;;
      429)
        # Rate-limited: exponential backoff up to 30s.
        backoff=$(( backoff * 2 ))
        if [ "$backoff" -gt 30 ]; then backoff=30; fi
        warn "Rate-limited by Tokenwise — backing off to ${backoff}s."
        sleep "$backoff"
        ;;
      4*|5*)
        err "Polling failed (HTTP $http_code)."
        err "body: $poll_body"
        exit 1
        ;;
      *)
        warn "Unexpected response (HTTP $http_code). Retrying."
        sleep "$backoff"
        ;;
    esac
  done

  ok "Paired device \"$DEVICE_NAME\" → workspace \"$WORKSPACE_NAME\""
fi

# ─── Step 3 — persist route token (mode 0600) ─────────────────────────────────
if [ "$DRY_RUN" -eq 1 ]; then
  info "[dry-run] Would write route token to $TOKEN_FILE (mode 0600)."
else
  # Refuse to write through symlinks — a pre-planted `$HOME/.tokenwise`
  # symlink would otherwise let an attacker redirect the token write and
  # `chmod 700` the target directory.
  if [ -L "$TW_DIR" ]; then
    err "$TW_DIR exists as a symlink; refusing to write through it."
    err "Remove the symlink and re-run the installer."
    exit 1
  fi
  if [ -L "$TOKEN_FILE" ]; then
    err "$TOKEN_FILE exists as a symlink; refusing to write through it."
    exit 1
  fi
  mkdir -p "$TW_DIR"
  chmod 700 "$TW_DIR"
  # Write atomically via a tmp file so the mode is set before any reader can
  # see the contents.
  umask_old=$(umask)
  umask 077
  : > "$TOKEN_FILE.tmp"
  printf "%s\n" "$ROUTE_TOKEN" > "$TOKEN_FILE.tmp"
  chmod 600 "$TOKEN_FILE.tmp"
  mv "$TOKEN_FILE.tmp" "$TOKEN_FILE"
  umask "$umask_old"

  # Verify the final mode is exactly 600 (or 0600 — some BSD `stat` builds
  # emit the 4-digit form). If chmod was silently dropped (e.g. on some FUSE
  # filesystems), refuse to continue.
  actual_mode="$(stat -c '%a' "$TOKEN_FILE" 2>/dev/null || stat -f '%Lp' "$TOKEN_FILE" 2>/dev/null || echo '???')"
  case "$actual_mode" in
    600|0600) ;;
    *)
      err "route-token file mode is $actual_mode (expected 600). Refusing to continue."
      err "Please check the filesystem at $TW_DIR for ACL or mount issues."
      exit 1
      ;;
  esac
  ok "Saved route token to $TOKEN_FILE (mode 0600)"
fi

printf "\n"

# Convenience: short alias used in proxy URLs below.
TW_ROUTE="$ROUTE_TOKEN"

# ─── Marker helpers (idempotency) ─────────────────────────────────────────────
marker_start() { printf "# tokenwise-connect: START %s" "$1"; }
marker_end()   { printf "# tokenwise-connect: END %s"   "$1"; }

# replace_or_append_block FILE TAG BLOCK_CONTENT
#   - If a marked block for TAG already exists in FILE, replace its body.
#   - Otherwise append a fresh marked block.
#   - Creates FILE if it doesn't exist.
#   - Honours DRY_RUN; backs up the file as <file>.tokenwise.bak (one backup only).
replace_or_append_block() {
  local file="$1" tag="$2" body="$3"
  local start end
  start="$(marker_start "$tag")"
  end="$(marker_end "$tag")"

  local new_block
  new_block="$(printf '%s\n%s\n%s\n' "$start" "$body" "$end")"

  if [ "$DRY_RUN" -eq 1 ]; then
    info "[dry-run] Would patch $file with block \"$tag\":"
    printf "%s%s%s\n" "$DIM" "$new_block" "$RESET" | sed 's/^/  | /'
    return 0
  fi

  if [ -f "$file" ]; then
    # One-shot backup; don't overwrite a real previous backup.
    if [ ! -f "$file.tokenwise.bak" ]; then
      cp -p "$file" "$file.tokenwise.bak"
    fi

    if grep -Fq "$start" "$file" && grep -Fq "$end" "$file"; then
      # Replace existing block in-place using awk (POSIX-safe).
      local tmpfile
      tmpfile="$(mktemp "${file}.XXXXXX")"
      awk -v s="$start" -v e="$end" -v body="$body" '
        BEGIN { inblock=0 }
        $0 == s {
          print s
          print body
          print e
          inblock=1
          next
        }
        $0 == e {
          if (inblock) { inblock=0; next }
        }
        {
          if (!inblock) print
        }
      ' "$file" > "$tmpfile"
      mv "$tmpfile" "$file"
      ok "  Updated block \"$tag\" in $file"
      return 0
    fi
  else
    mkdir -p "$(dirname "$file")"
    : > "$file"
  fi

  # Fresh append.
  {
    printf "\n"
    printf "%s\n" "$new_block"
  } >> "$file"
  ok "  Added block \"$tag\" to $file"
}

# ─── Step 4 — IDE detection + patching ────────────────────────────────────────
info "Scanning for installed AI IDEs / CLIs..."
printf "\n"

CONFIGURED=()
SKIPPED=()

# ─── 4a. Resolve shell rc target ──────────────────────────────────────────────
detect_shell_rc() {
  local s="${SHELL:-/bin/sh}"
  case "$s" in
    *zsh*)  printf "%s/.zshrc"  "$HOME" ;;
    *bash*) printf "%s/.bashrc" "$HOME" ;;
    *)
      # Fall back to whichever rc file already exists, else .profile.
      if [ -f "$HOME/.zshrc" ]; then
        printf "%s/.zshrc" "$HOME"
      elif [ -f "$HOME/.bashrc" ]; then
        printf "%s/.bashrc" "$HOME"
      else
        printf "%s/.profile" "$HOME"
      fi
      ;;
  esac
}
SHELL_RC="$(detect_shell_rc)"

# ─── 4b. Claude Code (IDE) + Claude CLI ───────────────────────────────────────
# Detects:
#   - Claude Code IDE (Electron app)             → ~/Library/Application Support/Claude  /  ~/.config/Claude
#   - Claude Code CLI (the `claude` binary)      → command -v claude  /  ~/.claude
#   - Legacy Anthropic CLI (`ant` binary)        → command -v ant, with Apache Ant disambig
# All three speak the same Anthropic API shape, so one ANTHROPIC_BASE_URL
# env var in shell rc covers them.
claude_code_present=0
if [ "$OS_KERNEL" = "Darwin" ] && [ -d "$HOME/Library/Application Support/Claude" ]; then
  claude_code_present=1
elif [ -d "$HOME/.config/Claude" ]; then
  claude_code_present=1
fi

claude_cli_present=0
# Modern Claude Code CLI binary is `claude` (npm install -g @anthropic-ai/claude-code).
# It creates ~/.claude/ on first run. Either signal is enough.
if command -v claude >/dev/null 2>&1; then
  claude_cli_present=1
elif [ -d "$HOME/.claude" ]; then
  claude_cli_present=1
fi
# `ant` is the LEGACY Anthropic CLI binary, but it ALSO collides with Apache Ant
# (the Java build tool, widely installed via brew/apt). Disambiguate by
# probing `ant --version`: the Apache Ant build tool prints "Apache Ant",
# while the Anthropic CLI does not.
if [ "$claude_cli_present" -eq 0 ] && command -v ant >/dev/null 2>&1; then
  ant_version_output="$(ant --version 2>&1 | head -n1 || true)"
  case "$ant_version_output" in
    *"Apache Ant"*) : ;;   # build tool, not Anthropic CLI — skip
    *) claude_cli_present=1 ;;
  esac
fi

if [ "$claude_code_present" -eq 1 ] || [ "$claude_cli_present" -eq 1 ]; then
  detected_what=""
  if [ "$claude_code_present" -eq 1 ]; then detected_what="Claude Code"; fi
  if [ "$claude_cli_present" -eq 1 ]; then
    if [ -n "$detected_what" ]; then
      detected_what="$detected_what + Claude CLI"
    else
      detected_what="Claude CLI"
    fi
  fi

  printf "%s%s detected%s — will set ANTHROPIC_BASE_URL in %s.\n" \
    "$BOLD" "$detected_what" "$RESET" "$SHELL_RC"

  if confirm "Patch $SHELL_RC for $detected_what?"; then
    block_body="export ANTHROPIC_BASE_URL=\"$PROXY_BASE/r/$TW_ROUTE/anthropic\""
    replace_or_append_block "$SHELL_RC" "anthropic" "$block_body"
    if [ "$claude_code_present" -eq 1 ]; then CONFIGURED+=("Claude Code (shell rc)"); fi
    if [ "$claude_cli_present"  -eq 1 ]; then CONFIGURED+=("Claude CLI (shell rc)"); fi
  else
    if [ "$claude_code_present" -eq 1 ]; then SKIPPED+=("Claude Code (user declined)"); fi
    if [ "$claude_cli_present"  -eq 1 ]; then SKIPPED+=("Claude CLI (user declined)"); fi
  fi
else
  SKIPPED+=("Claude Code / Claude CLI (not installed)")
fi

printf "\n"

# ─── 4c. Codex CLI ────────────────────────────────────────────────────────────
codex_present=0
codex_cfg="$HOME/.codex/config.toml"
if command -v codex >/dev/null 2>&1; then codex_present=1; fi
if [ -f "$codex_cfg" ]; then codex_present=1; fi

if [ "$codex_present" -eq 1 ]; then
  printf "%sCodex CLI detected%s — will configure %s.\n" "$BOLD" "$RESET" "$codex_cfg"

  if confirm "Patch $codex_cfg for Codex CLI?"; then
    if [ "$DRY_RUN" -eq 1 ]; then
      info "[dry-run] Would add/update [model_providers.tokenwise] in $codex_cfg"
      info "          base_url = \"$PROXY_BASE/r/$TW_ROUTE/openai/v1\""
      info "          model_provider = \"tokenwise\""
      CONFIGURED+=("Codex CLI (config.toml)")
    else
      mkdir -p "$(dirname "$codex_cfg")"
      [ -f "$codex_cfg" ] || : > "$codex_cfg"
      if [ ! -f "$codex_cfg.tokenwise.bak" ]; then
        cp -p "$codex_cfg" "$codex_cfg.tokenwise.bak"
      fi
      new_base="$PROXY_BASE/r/$TW_ROUTE/openai/v1"

      # Idempotent rewrite via python3 (always available — we required it
      # earlier as the jq fallback). We rewrite only the [model_providers.tokenwise]
      # block and the top-level model_provider key, leaving every other
      # provider block and key untouched.
      python3 - "$codex_cfg" "$new_base" <<'PYEOF'
import re, sys

path, new_base = sys.argv[1], sys.argv[2]
with open(path, "r") as f:
    text = f.read()

provider_block = (
    "[model_providers.tokenwise]\n"
    'name = "Tokenwise"\n'
    f'base_url = "{new_base}"\n'
    # Codex removed Chat Completions support in Feb 2026 — third-party
    # providers must use the Responses API now. base_url already ends in
    # /openai/v1, so Codex calls .../openai/v1/responses (forwarded verbatim).
    'wire_api = "responses"\n'
)

# Replace existing [model_providers.tokenwise] block (up to next [section] or EOF).
# NOTE: the table name is `model_providers`, not `providers` — the regex must
# match what we WRITE above, or re-runs never find the prior block and append a
# duplicate (which makes Codex reject the config on a duplicate-table error).
pattern = re.compile(
    r'(?ms)^\[model_providers\.tokenwise\]\s*\n(?:(?!^\[).*\n?)*'
)
if pattern.search(text):
    text = pattern.sub(provider_block, text)
else:
    if text and not text.endswith("\n"):
        text += "\n"
    if text:
        text += "\n"
    text += provider_block

# Set or replace top-level model_provider = "tokenwise" (top of file only —
# only match lines outside any [section]).
lines = text.splitlines(keepends=True)
out, in_section, set_done = [], False, False
for line in lines:
    stripped = line.lstrip()
    if stripped.startswith("["):
        in_section = True
    if (not in_section) and re.match(r'^\s*model_provider\s*=', line):
        out.append('model_provider = "tokenwise"\n')
        set_done = True
        continue
    out.append(line)

text = "".join(out)
if not set_done:
    text = 'model_provider = "tokenwise"\n' + text

with open(path, "w") as f:
    f.write(text)
PYEOF
      ok "  Patched $codex_cfg"
      CONFIGURED+=("Codex CLI (config.toml)")
    fi
  else
    SKIPPED+=("Codex CLI (user declined)")
  fi
else
  SKIPPED+=("Codex CLI (not installed)")
fi

printf "\n"

# ─── 4d. Cursor ───────────────────────────────────────────────────────────────
cursor_cfg=""
if [ "$OS_KERNEL" = "Darwin" ]; then
  cursor_cfg="$HOME/Library/Application Support/Cursor/User/settings.json"
elif [ -f "$HOME/.config/Cursor/User/settings.json" ]; then
  cursor_cfg="$HOME/.config/Cursor/User/settings.json"
fi

if [ -n "$cursor_cfg" ] && [ -f "$cursor_cfg" ]; then
  printf "%sCursor detected%s — will set openAIApiBase in %s.\n" "$BOLD" "$RESET" "$cursor_cfg"
  warn "Cursor's BYO-model traffic will route through Tokenwise; the bundled Cursor Pro models stay on Cursor's own auth and remain unobserved."

  if confirm "Patch $cursor_cfg for Cursor?"; then
    new_base="$PROXY_BASE/r/$TW_ROUTE/openai/v1"
    if [ "$DRY_RUN" -eq 1 ]; then
      info "[dry-run] Would set openAIApiBase = \"$new_base\" in $cursor_cfg"
      CONFIGURED+=("Cursor (settings.json)")
    else
      if [ ! -f "$cursor_cfg.tokenwise.bak" ]; then
        cp -p "$cursor_cfg" "$cursor_cfg.tokenwise.bak"
      fi
      # Make sure the file is at least an empty JSON object before patching.
      if [ ! -s "$cursor_cfg" ]; then
        printf '%s\n' '{}' > "$cursor_cfg"
      fi
      json_set_string "$cursor_cfg" "openAIApiBase" "$new_base"
      ok "  Patched $cursor_cfg"
      CONFIGURED+=("Cursor (settings.json)")
    fi
  else
    SKIPPED+=("Cursor (user declined)")
  fi
else
  SKIPPED+=("Cursor (not installed)")
fi

printf "\n"

# ─── Step 5 — final summary ───────────────────────────────────────────────────
printf "%s━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━%s\n" "$BOLD" "$RESET"
ok "Paired device \"$DEVICE_NAME\" → workspace \"$WORKSPACE_NAME\""
if [ "${#CONFIGURED[@]}" -gt 0 ]; then
  for c in "${CONFIGURED[@]}"; do
    ok "Configured: $c"
  done
fi
if [ "${#SKIPPED[@]}" -gt 0 ]; then
  for s in "${SKIPPED[@]}"; do
    skip "Skipped: $s"
  done
fi
printf "\n"
log "Reload your shell or open a new terminal for the changes to take effect."
printf "\n"
log "To uninstall:  curl -fsSL https://tokenwisehq.com/disconnect.sh | bash"
log "View activity: https://tokenwisehq.com/settings/devices"

# trap on EXIT will run cleanup()
exit 0
