#!/usr/bin/env bash
# Codex4Less — Cline provider setup (macOS / Linux)
# Requires the Cline VS Code extension. UI clicks are not simulated.
# Usage:
#   curl -fsSL https://codex4less.com/scripts/install-cline.sh | bash -s -- --model "gpt-6.1-sol"
#   ./install-cline.sh --help
#   ./install-cline.sh --uninstall
# The script preserves other providers and never touches chat databases.

set -euo pipefail
umask 077

BASE_URL="https://api.codex4less.com/v1"
PROVIDER_ID="codex4less"
MODEL="gpt-6-astra"
API_KEY=""
DRY_RUN=0
UNINSTALL=0
CLINE_ROOT="${CLINE_DIR:-$HOME/.cline}"
DATA_DIR="${CLINE_DATA_DIR:-$CLINE_ROOT/data}"
JSON_PATH="$DATA_DIR/settings/providers.json"
MARKER="$CLINE_ROOT/.codex4less-install.env"
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 — Cline setup

Options:
  --api-key <sk-...>     API key. If omitted, the script asks for it.
  --model <model-id>     Model to select. Default: gpt-6-astra.
  --base-url <url>       Override the API base URL (https only).
  --uninstall            Remove only the Codex4Less provider.
  --dry-run              Print the plan without writing files or calling the API.
  --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
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 ;;
    --help|-h) usage; exit 0 ;;
    *) fail "Unknown option: $1"; usage; exit 2 ;;
  esac
done

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

case "$BASE_URL" in
  https://*) ;;
  *) fail "Base URL must use https:// (got: $BASE_URL)"; exit 2 ;;
esac

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 -q '^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/models ..."
  code="$(curl -sS --max-time 20 -o /dev/null -w '%{http_code}' "$BASE_URL/models" -H "Authorization: Bearer $API_KEY" 2>/dev/null || printf '000')"
  case "$code" in
    200) ok "API key is valid." ;;
    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
}

backup_config() {
  if [ -f "$JSON_PATH" ]; then
    local backup="${JSON_PATH}.bak-$(date +%Y%m%d-%H%M%S)-$$"
    cp -p "$JSON_PATH" "$backup"
    printf '%s' "$backup"
  fi
}

settings_json() {
  local caps context
  case "$MODEL" in
    gpt-6-astra) caps='["streaming","tools","vision"]'; context=0 ;;
    gpt-6.1-sol) caps='["streaming","tools","vision"]'; context=400000 ;;
    gpt-5.6-sol) caps='["streaming","tools"]'; context=300000 ;;
    claude-opus-5|claude-sonnet-5|claude-sonnet-5-5) caps='["streaming","tools","vision"]'; context=200000 ;;
    *) caps='["streaming","tools"]'; context=0 ;;
  esac
  if [ "$context" -gt 0 ]; then
    printf '{"provider":"%s","apiKey":"%s","baseUrl":"%s","protocol":"openai-chat","client":"openai-compatible","model":"%s","contextWindow":%s,"capabilities":%s}' \
      "$PROVIDER_ID" "$API_KEY" "$BASE_URL" "$MODEL" "$context" "$caps"
  else
    printf '{"provider":"%s","apiKey":"%s","baseUrl":"%s","protocol":"openai-chat","client":"openai-compatible","model":"%s","capabilities":%s}' \
      "$PROVIDER_ID" "$API_KEY" "$BASE_URL" "$MODEL" "$caps"
  fi
}

previous_last_used() {
  if [ ! -f "$JSON_PATH" ]; then return 0; fi
  if command -v node >/dev/null 2>&1; then
    node -e 'const fs=require("fs");try{const j=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if(typeof j.lastUsedProvider==="string")console.log(j.lastUsedProvider)}catch(e){}' "$JSON_PATH"
  elif command -v python3 >/dev/null 2>&1; then
    python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));v=d.get("lastUsedProvider","");print(v if isinstance(v,str) else "")' "$JSON_PATH" 2>/dev/null || true
  fi
}

merge_json() {
  export PROVIDER_ID JSON_PATH PREVIOUS_LAST_USED="${PREVIOUS_LAST_USED:-}" UNINSTALL
  SETTINGS_JSON="$(settings_json)"
  export SETTINGS_JSON
  if command -v node >/dev/null 2>&1; then
    node <<'NODE'
const fs = require("fs");
const path = process.env.JSON_PATH;
const pid = process.env.PROVIDER_ID;
let cfg = { version: 1, modes: {}, providers: {} };
let existed = false;
if (fs.existsSync(path)) {
  existed = true;
  try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
  catch (e) { console.error("  [X] providers.json is not valid JSON; please fix it or use the manual setup."); process.exit(1); }
}
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
if (cfg.version !== 1) cfg.version = 1;
if (!cfg.providers || typeof cfg.providers !== "object" || Array.isArray(cfg.providers)) cfg.providers = {};
if (!cfg.modes || typeof cfg.modes !== "object" || Array.isArray(cfg.modes)) cfg.modes = {};
if (process.env.UNINSTALL === "1") {
  delete cfg.providers[pid];
  if (cfg.lastUsedProvider === pid) {
    if (process.env.PREVIOUS_LAST_USED) cfg.lastUsedProvider = process.env.PREVIOUS_LAST_USED;
    else delete cfg.lastUsedProvider;
  }
} else {
  cfg.providers[pid] = {
    settings: JSON.parse(process.env.SETTINGS_JSON),
    updatedAt: new Date().toISOString(),
    tokenSource: "manual"
  };
  cfg.lastUsedProvider = pid;
}
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
console.log(existed ? "  [OK] providers.json merged; other providers preserved." : "  [OK] providers.json created.");
NODE
  elif command -v python3 >/dev/null 2>&1; then
    python3 <<'PY'
import json, os, datetime
path = os.environ["JSON_PATH"]
pid = os.environ["PROVIDER_ID"]
cfg = {"version": 1, "modes": {}, "providers": {}}
existed = os.path.exists(path)
if existed:
    try:
        with open(path, "r", encoding="utf-8") as f:
            cfg = json.load(f)
    except Exception:
        print("  [X] providers.json is not valid JSON; please fix it or use the manual setup.")
        raise SystemExit(1)
if not isinstance(cfg, dict):
    cfg = {}
cfg["version"] = 1
if not isinstance(cfg.get("providers"), dict):
    cfg["providers"] = {}
if not isinstance(cfg.get("modes"), dict):
    cfg["modes"] = {}
if os.environ.get("UNINSTALL") == "1":
    cfg["providers"].pop(pid, None)
    if cfg.get("lastUsedProvider") == pid:
        prev = os.environ.get("PREVIOUS_LAST_USED", "")
        if prev:
            cfg["lastUsedProvider"] = prev
        else:
            cfg.pop("lastUsedProvider", None)
else:
    cfg["providers"][pid] = {
        "settings": json.loads(os.environ["SETTINGS_JSON"]),
        "updatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z"),
        "tokenSource": "manual",
    }
    cfg["lastUsedProvider"] = pid
with open(path, "w", encoding="utf-8") as f:
    json.dump(cfg, f, indent=2, ensure_ascii=False)
    f.write("\n")
try:
    os.chmod(path, 0o600)
except OSError:
    pass
print("  [OK] providers.json merged; other providers preserved." if existed else "  [OK] providers.json created.")
PY
  else
    fail "Node.js or Python 3 is required to merge providers.json."
    info "Install one of them, or use the manual setup on /docs/install/cline."
    exit 1
  fi
}

do_uninstall() {
  if [ -f "$MARKER" ]; then
    # shellcheck disable=SC1090
    . "$MARKER"
  fi
  if [ ! -f "$JSON_PATH" ]; then
    warn "No providers.json found; nothing to remove."
    rm -f "$MARKER"
    return 0
  fi
  local backup
  backup="$(backup_config)"
  [ -n "$backup" ] && info "Backup: $backup"
  PREVIOUS_LAST_USED="${PREVIOUS_LAST_USED:-}"
  export PREVIOUS_LAST_USED
  UNINSTALL=1 merge_json
  rm -f "$MARKER"
  ok "Codex4Less provider removed. Other providers were preserved."
}

if [ "$UNINSTALL" = "1" ]; then
  do_uninstall
  exit 0
fi

if [ "$DRY_RUN" = "1" ]; then
  info "Dry run: no files will be written and no API call will be made."
  info "Config file: $JSON_PATH"
  info "Provider: $PROVIDER_ID"
  info "Model: $MODEL"
  info "Base URL: $BASE_URL"
  info "Backup: timestamped .bak file before writing"
  exit 0
fi

read_key
validate_key

mkdir -p "$DATA_DIR/settings"
SAVED_PREV_LAST_USED=""
HAS_MARKER=0
if [ -f "$MARKER" ]; then
  # Keep the first install's previous value; a reinstall must not replace it
  # with the Codex4Less provider it wrote last time.
  # shellcheck disable=SC1090
  REQUESTED_MODEL="$MODEL"
  . "$MARKER"
  MODEL="$REQUESTED_MODEL"
  HAS_MARKER=1
  SAVED_PREV_LAST_USED="${PREVIOUS_LAST_USED:-}"
fi
if [ "$HAS_MARKER" = "1" ]; then
  PREVIOUS_LAST_USED="$SAVED_PREV_LAST_USED"
else
  PREVIOUS_LAST_USED="$(previous_last_used || true)"
fi
if [ -f "$JSON_PATH" ]; then
  backup="$(backup_config)"
  [ -n "$backup" ] && info "Backup: $backup"
fi

merge_json
chmod 600 "$JSON_PATH" 2>/dev/null || true

{
  printf 'PREVIOUS_LAST_USED=%q\n' "$PREVIOUS_LAST_USED"
  printf 'MODEL=%q\n' "$MODEL"
  printf 'PROVIDER=%q\n' "$PROVIDER_ID"
} > "$MARKER"

echo ""
ok "Cline is configured for Codex4Less."
info "Model: $MODEL"
info "Reload VS Code (or the Cline extension) and select the Codex4Less provider."
info "To undo: run this script with --uninstall."
info "Support: info@codex4less.com"
