#!/usr/bin/env bash
# Codex4Less — Claude Code (Anthropic Messages) setup (macOS / Linux)
# Base URL is the Anthropic root URL: https://api.codex4less.com (no /v1).
# Usage:
#   curl -fsSL https://codex4less.com/scripts/install-claude-code.sh | bash -s -- --model "claude-sonnet-5-5"
#   ./install-claude-code.sh --help
#   ./install-claude-code.sh --uninstall

set -euo pipefail
umask 077

BASE_URL="https://api.codex4less.com"
MODEL="claude-sonnet-5-5"
API_KEY=""
DRY_RUN=0
UNINSTALL=0
YES=0
SETTINGS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
SETTINGS="$SETTINGS_DIR/settings.json"
ALLOWED_MODELS="gpt-6-astra gpt-6.1-sol gpt-5.6-sol claude-opus-5 claude-sonnet-5 claude-sonnet-5-5"

info() { printf '  %s\n' "$*"; }
ok()   { printf '  [OK] %s\n' "$*"; }
warn() { printf '  [!] %s\n' "$*"; }
fail() { printf '  [X] %s\n' "$*" >&2; }

usage() {
  cat <<'EOF'
Codex4Less — Claude Code setup

Options:
  --api-key <sk-...>     API key. If omitted, the script asks for it.
  --model <model-id>     Model to select. Default: claude-sonnet-5-5.
  --base-url <url>       Override the Anthropic base URL (https only, no /v1).
  --uninstall            Remove only the Codex4Less entries from settings.json.
  --dry-run              Print the plan without writing files or calling the API.
  --yes                  Do not ask for confirmation.
  --help                 Show this help.

Allowed models:
  gpt-6-astra
  gpt-6.1-sol
  gpt-5.6-sol
  claude-opus-5
  claude-sonnet-5
  claude-sonnet-5-5

Anthropic base URL must NOT end with /v1. Claude Code appends /v1/messages.
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --api-key) API_KEY="${2:-}"; shift 2 ;;
    --model) MODEL="${2:-}"; shift 2 ;;
    --base-url) BASE_URL="${2:-}"; shift 2 ;;
    --uninstall) UNINSTALL=1; shift ;;
    --dry-run) DRY_RUN=1; shift ;;
    --yes|-y) YES=1; shift ;;
    --help|-h) usage; exit 0 ;;
    *) fail "Unknown option: $1"; usage; exit 2 ;;
  esac
done

BASE_URL="${BASE_URL%/}"
case "$BASE_URL" in
  https://*) ;;
  *) fail "Base URL must use https:// (got: $BASE_URL)"; exit 2 ;;
esac
case "$BASE_URL" in
  */v1) fail "Anthropic base URL must not end with /v1 (got: $BASE_URL)"; exit 2 ;;
esac

is_allowed_model() {
  for m in $ALLOWED_MODELS; do
    [ "$m" = "$1" ] && return 0
  done
  return 1
}

if ! is_allowed_model "$MODEL"; then
  fail "Model is not in the Codex4Less catalog allowlist: $MODEL"
  info "Allowed: $ALLOWED_MODELS"
  exit 2
fi

if [ "$DRY_RUN" = "1" ]; then
  info "Dry run: no files will be written and no network calls will be made."
  info "Anthropic base URL: $BASE_URL (no /v1)"
  info "Model: $MODEL"
  info "Settings: $SETTINGS"
  info "Claude Code install: skipped if 'claude' is already on PATH; otherwise official installer/npm."
  info "Env: ANTHROPIC_AUTH_TOKEN (Bearer), model aliases, gateway discovery enabled."
  exit 0
fi

json_runner() {
  if command -v node >/dev/null 2>&1; then
    printf 'node'
  elif command -v python3 >/dev/null 2>&1; then
    printf 'python3'
  else
    printf ''
  fi
}

read_key() {
  if [ -z "$API_KEY" ] && [ -n "${CODEX4LESS_API_KEY:-}" ]; then
    API_KEY="$CODEX4LESS_API_KEY"
  fi
  if [ -n "$API_KEY" ]; then return 0; fi
  printf '\n  Paste your Codex4Less API key (sk-...):\n'
  printf '  API_KEY: '
  if [ -t 0 ]; then
    if ! read -r -s API_KEY; then API_KEY=""; fi
    printf '\n'
  elif [ -r /dev/tty ]; then
    if ! read -r -s API_KEY < /dev/tty; then API_KEY=""; fi
    printf '\n'
  fi
  API_KEY="$(printf '%s' "${API_KEY:-}" | tr -d '\r' | sed -e 's/^[[:space:]"]*//' -e 's/[[:space:]"]*$//')"
  if [ -z "$API_KEY" ]; then
    fail "No API key provided. Re-run with --api-key or from an interactive terminal."
    exit 1
  fi
}

validate_key() {
  if ! printf '%s' "$API_KEY" | grep -qE '^sk-[A-Za-z0-9._-]+$'; then
    fail "Invalid API key format: it must start with sk- and not be quoted."
    exit 1
  fi
  info "Validating the key against $BASE_URL/v1/models ..."
  TMP="$(mktemp)"
  trap 'rm -f "$TMP"' EXIT
  code="$(curl -sS --max-time 30 -o "$TMP" -w '%{http_code}' "$BASE_URL/v1/models" \
    -H "Authorization: Bearer $API_KEY" \
    -H "anthropic-version: 2023-06-01" 2>/dev/null || printf '000')"
  case "$code" in
    200) ;;
    401) fail "Invalid API key (401). Check Dashboard > API Keys."; exit 1 ;;
    402) fail "No usable balance for this key (402). Top up in the dashboard."; exit 1 ;;
    403) fail "Account is not allowed to use the API (403). Check verification/account status."; exit 1 ;;
    429) fail "Rate limited while validating (429). Try again shortly."; exit 1 ;;
    000) fail "Could not reach Codex4Less. Check your network."; exit 1 ;;
    *) fail "Unexpected HTTP $code while validating the key."; exit 1 ;;
  esac

  RUNNER="$(json_runner)"
  if [ -z "$RUNNER" ]; then
    warn "node/python3 not found; skipping the model-list check."
  elif [ "$RUNNER" = "node" ]; then
    node - "$TMP" "$MODEL" <<'NODE'
const fs = require("fs");
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const ids = Array.isArray(data?.data) ? data.data.map((m) => m && m.id).filter(Boolean) : [];
if (!ids.includes(process.argv[3])) {
  console.error("  [X] Model is not in the active Codex4Less catalog: " + process.argv[3]);
  console.error("  Available: " + (ids.join(", ") || "(none)"));
  process.exit(2);
}
NODE
  else
    python3 - "$TMP" "$MODEL" <<'PY'
import json, sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
    data = json.load(f)
ids = [m.get("id") for m in data.get("data", []) if isinstance(m, dict) and m.get("id")]
if sys.argv[2] not in ids:
    print("  [X] Model is not in the active Codex4Less catalog: " + sys.argv[2])
    print("  Available: " + (", ".join(ids) or "(none)"))
    raise SystemExit(2)
PY
  fi
  ok "API key and model are valid."
}

install_claude() {
  export PATH="$HOME/.local/bin:$PATH"
  if command -v claude >/dev/null 2>&1; then
    info "Claude Code is already installed: $(claude --version 2>/dev/null || printf 'version unknown')"
    return 0
  fi
  info "Installing Claude Code ..."
  if command -v curl >/dev/null 2>&1; then
    if curl -fsSL https://claude.ai/install.sh | bash; then
      export PATH="$HOME/.local/bin:$PATH"
    else
      warn "Official installer failed; trying npm fallback."
    fi
  fi
  if ! command -v claude >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
    npm install -g @anthropic-ai/claude-code
    export PATH="$HOME/.local/bin:$PATH"
  fi
  if ! command -v claude >/dev/null 2>&1; then
    fail "Claude Code could not be installed automatically."
    info "Install it manually: curl -fsSL https://claude.ai/install.sh | bash"
    info "Then re-run this script."
    exit 1
  fi
  ok "Claude Code installed."
}

merge_settings() {
  mkdir -p "$SETTINGS_DIR"
  if [ -f "$SETTINGS" ]; then
    backup="${SETTINGS}.bak-$(date +%Y%m%d-%H%M%S)-$$"
    cp -p "$SETTINGS" "$backup"
    info "Backup: $backup"
  fi

  RUNNER="$(json_runner)"
  if [ -z "$RUNNER" ]; then
    fail "node or python3 is required to update $SETTINGS."
    exit 1
  fi

  export CLAUDE_SETTINGS="$SETTINGS"
  export CLAUDE_BASE_URL="$BASE_URL"
  export CLAUDE_API_KEY="$API_KEY"
  export CLAUDE_MODEL="$MODEL"
  export CLAUDE_UNINSTALL="$UNINSTALL"

  if [ "$RUNNER" = "node" ]; then
    node <<'NODE'
const fs = require("fs");
const path = process.env.CLAUDE_SETTINGS;
const keys = [
  "ANTHROPIC_BASE_URL",
  "ANTHROPIC_AUTH_TOKEN",
  "ANTHROPIC_MODEL",
  "ANTHROPIC_DEFAULT_MODEL",
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
  "ANTHROPIC_DEFAULT_SONNET_MODEL",
  "ANTHROPIC_DEFAULT_HAIKU_MODEL",
  "ANTHROPIC_DEFAULT_FABLE_MODEL",
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
  "CLAUDE_CODE_ATTRIBUTION_HEADER",
];
let data = {};
if (fs.existsSync(path)) {
  try {
    data = JSON.parse(fs.readFileSync(path, "utf8"));
  } catch (e) {
    console.error("  [X] " + path + " is not valid JSON; fix it or use manual setup.");
    process.exit(1);
  }
}
if (!data || typeof data !== "object" || Array.isArray(data)) data = {};
if (!data.env || typeof data.env !== "object" || Array.isArray(data.env)) data.env = {};
if (process.env.CLAUDE_UNINSTALL === "1") {
  for (const key of keys) delete data.env[key];
  if (Object.keys(data.env).length === 0) delete data.env;
} else {
  Object.assign(data.env, {
    ANTHROPIC_BASE_URL: process.env.CLAUDE_BASE_URL,
    ANTHROPIC_AUTH_TOKEN: process.env.CLAUDE_API_KEY,
    ANTHROPIC_MODEL: process.env.CLAUDE_MODEL,
    ANTHROPIC_DEFAULT_MODEL: process.env.CLAUDE_MODEL,
    ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-opus-5",
    ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-5-5",
    ANTHROPIC_DEFAULT_HAIKU_MODEL: "claude-sonnet-5",
    ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-sonnet-5-5",
    CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
    CLAUDE_CODE_ATTRIBUTION_HEADER: "0",
  });
}
fs.writeFileSync(path, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
NODE
  else
    python3 <<'PY'
import json, os
path = os.environ["CLAUDE_SETTINGS"]
keys = [
    "ANTHROPIC_BASE_URL",
    "ANTHROPIC_AUTH_TOKEN",
    "ANTHROPIC_MODEL",
    "ANTHROPIC_DEFAULT_MODEL",
    "ANTHROPIC_DEFAULT_OPUS_MODEL",
    "ANTHROPIC_DEFAULT_SONNET_MODEL",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL",
    "ANTHROPIC_DEFAULT_FABLE_MODEL",
    "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
    "CLAUDE_CODE_ATTRIBUTION_HEADER",
]
data = {}
if os.path.exists(path):
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
    except Exception:
        print("  [X] " + path + " is not valid JSON; fix it or use manual setup.")
        raise SystemExit(1)
if not isinstance(data, dict):
    data = {}
if not isinstance(data.get("env"), dict):
    data["env"] = {}
if os.environ.get("CLAUDE_UNINSTALL") == "1":
    for key in keys:
        data["env"].pop(key, None)
    if not data["env"]:
        data.pop("env", None)
else:
    data["env"].update({
        "ANTHROPIC_BASE_URL": os.environ["CLAUDE_BASE_URL"],
        "ANTHROPIC_AUTH_TOKEN": os.environ["CLAUDE_API_KEY"],
        "ANTHROPIC_MODEL": os.environ["CLAUDE_MODEL"],
        "ANTHROPIC_DEFAULT_MODEL": os.environ["CLAUDE_MODEL"],
        "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-5",
        "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5-5",
        "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-sonnet-5",
        "ANTHROPIC_DEFAULT_FABLE_MODEL": "claude-sonnet-5-5",
        "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1",
        "CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
    })
with open(path, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)
    f.write("\n")
os.chmod(path, 0o600)
PY
  fi

  if [ "$UNINSTALL" = "1" ]; then
    ok "Codex4Less entries removed from $SETTINGS."
  else
    ok "Claude Code settings updated: $SETTINGS"
  fi
}

if [ "$UNINSTALL" = "1" ]; then
  if [ ! -f "$SETTINGS" ]; then
    info "No settings file found at $SETTINGS; nothing to remove."
    exit 0
  fi
  merge_settings
  info "Claude Code itself was not uninstalled; only Codex4Less settings were removed."
  exit 0
fi

read_key
validate_key
if [ "$YES" = "0" ] && [ -t 0 ]; then
  printf '\n  Configure Claude Code for %s (%s)? [Y/n] ' "$BASE_URL" "$MODEL"
  read -r answer
  case "$answer" in
    n|N|no|NO) info "Aborted."; exit 0 ;;
  esac
fi
install_claude
merge_settings

printf '\n'
ok "Claude Code is ready for Codex4Less."
info "Anthropic base URL: $BASE_URL (do not append /v1)"
info "Model: $MODEL"
info "Settings: $SETTINGS"
info "Run: claude"
info "Headless check: claude -p \"hello\""
