#!/usr/bin/env bash
# Codex4Less — OpenCode CLI provider setup (macOS / Linux)
# Requires an existing OpenCode CLI install. The script does not install OpenCode.
# Usage:
#   curl -fsSL https://codex4less.com/scripts/install-opencode.sh | bash -s -- --model "gpt-6.1-sol"
#   ./install-opencode.sh --help
#   ./install-opencode.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
DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
JSON_PATH="$DIR/opencode.json"
JSONC_PATH="$DIR/opencode.jsonc"
MARKER="$DIR/.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 — OpenCode CLI 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
}

provider_json() {
  cat <<JSON
{
  "npm": "@ai-sdk/openai-compatible",
  "name": "Codex4Less",
  "options": {
    "baseURL": "$BASE_URL",
    "apiKey": "$API_KEY",
    "timeout": false,
    "headerTimeout": 120000,
    "chunkTimeout": 300000
  },
  "models": {
    "gpt-6-astra": {
      "name": "GPT-6 Astra",
      "attachment": true,
      "reasoning": true,
      "tool_call": true,
      "modalities": { "input": ["text", "image"], "output": ["text"] },
      "options": { "store": false },
      "variants": {
        "low": { "reasoningEffort": "low" },
        "medium": { "reasoningEffort": "medium" },
        "high": { "reasoningEffort": "high" },
        "xhigh": { "reasoningEffort": "xhigh" },
        "max": { "reasoningEffort": "max" }
      }
    },
    "gpt-6.1-sol": {
      "name": "GPT-6.1 Sol",
      "attachment": true,
      "reasoning": true,
      "tool_call": true,
      "limit": { "context": 400000 },
      "modalities": { "input": ["text", "image"], "output": ["text"] },
      "options": { "store": false },
      "variants": {
        "low": { "reasoningEffort": "low" },
        "medium": { "reasoningEffort": "medium" },
        "high": { "reasoningEffort": "high" },
        "xhigh": { "reasoningEffort": "xhigh" },
        "max": { "reasoningEffort": "max" }
      }
    },
    "gpt-5.6-sol": {
      "name": "GPT-5.6 Sol",
      "attachment": true,
      "reasoning": true,
      "tool_call": true,
      "limit": { "context": 300000 },
      "modalities": { "input": ["text"], "output": ["text"] },
      "options": { "store": false },
      "variants": {
        "low": { "reasoningEffort": "low" },
        "medium": { "reasoningEffort": "medium" },
        "high": { "reasoningEffort": "high" },
        "xhigh": { "reasoningEffort": "xhigh" },
        "max": { "reasoningEffort": "max" }
      }
    },
    "claude-opus-5": {
      "name": "Claude Opus 5",
      "attachment": true,
      "reasoning": true,
      "tool_call": true,
      "limit": { "context": 200000 },
      "modalities": { "input": ["text", "image"], "output": ["text"] },
      "options": { "store": false }
    },
    "claude-sonnet-5": {
      "name": "Claude Sonnet 5",
      "attachment": true,
      "reasoning": true,
      "tool_call": true,
      "limit": { "context": 200000 },
      "modalities": { "input": ["text", "image"], "output": ["text"] },
      "options": { "store": false }
    },
    "claude-sonnet-5-5": {
      "name": "Claude Sonnet 5.5",
      "attachment": true,
      "reasoning": true,
      "tool_call": true,
      "limit": { "context": 200000 },
      "modalities": { "input": ["text", "image"], "output": ["text"] },
      "options": { "store": false }
    }
  }
}
JSON
}

merge_json() {
  export PROVIDER_ID BASE_URL MODEL API_KEY
  PROVIDER_JSON="$(provider_json)"
  export PROVIDER_JSON
  export JSON_PATH PREVIOUS_MODEL="${PREVIOUS_MODEL:-}" UNINSTALL
  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 = {};
let existed = false;
if (fs.existsSync(path)) {
  existed = true;
  try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
  catch (e) { console.error("  [X] opencode.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 = {};
}
cfg["$schema"] = cfg["$schema"] || "https://opencode.ai/config.json";
if (!cfg.provider || typeof cfg.provider !== "object" || Array.isArray(cfg.provider)) cfg.provider = {};
if (process.env.UNINSTALL === "1") {
  delete cfg.provider[pid];
  if (typeof cfg.model === "string" && cfg.model.startsWith(pid + "/")) {
    if (process.env.PREVIOUS_MODEL) cfg.model = process.env.PREVIOUS_MODEL;
    else delete cfg.model;
  }
} else {
  const provider = JSON.parse(process.env.PROVIDER_JSON);
  cfg.provider[pid] = provider;
  cfg.model = pid + "/" + process.env.MODEL;
}
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
console.log(existed ? "  [OK] opencode.json merged; other providers preserved." : "  [OK] opencode.json created.");
NODE
  elif command -v python3 >/dev/null 2>&1; then
    python3 <<'PY'
import json, os
path = os.environ["JSON_PATH"]
pid = os.environ["PROVIDER_ID"]
cfg = {}
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] opencode.json is not valid JSON; please fix it or use the manual setup.")
        raise SystemExit(1)
    if not isinstance(cfg, dict):
        cfg = {}
cfg.setdefault("$schema", "https://opencode.ai/config.json")
if not isinstance(cfg.get("provider"), dict):
    cfg["provider"] = {}
if os.environ.get("UNINSTALL") == "1":
    cfg["provider"].pop(pid, None)
    model = cfg.get("model")
    if isinstance(model, str) and model.startswith(pid + "/"):
        prev = os.environ.get("PREVIOUS_MODEL", "")
        if prev:
            cfg["model"] = prev
        else:
            cfg.pop("model", None)
else:
    cfg["provider"][pid] = json.loads(os.environ["PROVIDER_JSON"])
    cfg["model"] = pid + "/" + os.environ["MODEL"]
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] opencode.json merged; other providers preserved." if existed else "  [OK] opencode.json created.")
PY
  else
    fail "Node.js or Python 3 is required to merge opencode.json."
    info "Install one of them, or use the manual setup on /docs/install/opencode."
    exit 1
  fi
}

previous_model() {
  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.model==="string")console.log(j.model)}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]));print(d.get("model","") if isinstance(d.get("model"),str) else "")' "$JSON_PATH" 2>/dev/null || true
  fi
}

do_uninstall() {
  if [ -f "$MARKER" ]; then
    # shellcheck disable=SC1090
    . "$MARKER"
  fi
  if [ -f "$JSONC_PATH" ] && [ ! -f "$JSON_PATH" ]; then
    warn "Only opencode.jsonc exists. Remove the Codex4Less provider manually; comments are not merged."
    exit 1
  fi
  if [ ! -f "$JSON_PATH" ]; then
    warn "No opencode.json found; nothing to remove."
    rm -f "$MARKER"
    return 0
  fi
  local backup
  backup="$(backup_config)"
  [ -n "$backup" ] && info "Backup: $backup"
  PREVIOUS_MODEL="${PREVIOUS_MODEL:-}"
  export PREVIOUS_MODEL
  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: $PROVIDER_ID/$MODEL"
  info "Base URL: $BASE_URL"
  info "Backup: timestamped .bak file before writing"
  exit 0
fi

if [ -f "$JSONC_PATH" ] && [ ! -f "$JSON_PATH" ]; then
  fail "Only opencode.jsonc exists. Comments cannot be merged safely."
  info "Rename it to opencode.json after removing comments, or use the manual setup on /docs/install/opencode."
  exit 1
fi

read_key
validate_key

mkdir -p "$DIR"
SAVED_PREV_MODEL=""
HAS_MARKER=0
if [ -f "$MARKER" ]; then
  # Keep the first install's previous value; a reinstall must not replace it
  # with the Codex4Less model it wrote last time.
  # shellcheck disable=SC1090
  REQUESTED_MODEL="$MODEL"
  . "$MARKER"
  MODEL="$REQUESTED_MODEL"
  HAS_MARKER=1
  SAVED_PREV_MODEL="${PREVIOUS_MODEL:-}"
fi
if [ "$HAS_MARKER" = "1" ]; then
  PREVIOUS_MODEL="$SAVED_PREV_MODEL"
else
  PREVIOUS_MODEL="$(previous_model || 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_MODEL=%q\n' "$PREVIOUS_MODEL"
  printf 'MODEL=%q\n' "$MODEL"
  printf 'PROVIDER=%q\n' "$PROVIDER_ID"
} > "$MARKER"

echo ""
ok "OpenCode CLI is configured for Codex4Less."
info "Model: $PROVIDER_ID/$MODEL"
info "Reopen OpenCode and select the Codex4Less model."
info "To undo: run this script with --uninstall."
info "Support: info@codex4less.com"
