#!/bin/sh
# Simulator86 MCP server installer — https://mcp.sim86.com
set -e

URL='https://mcp.sim86.com/mcp'
API='https://api.sim86.com'
NAME='sim86'

# Normalize and validate the requested client before opening a browser or
# minting a key. This also keeps the device-flow JSON payload predictable.
CLIENT=$(printf '%s' "${1:-claude}" | tr '[:upper:]' '[:lower:]')
case "$CLIENT" in
  claude|claude-code) CLIENT='claude' ;;
  codex|cursor|vscode|opencode|generic) ;;
  *)
    echo "Unsupported MCP client: $CLIENT" >&2
    echo 'Supported clients: claude, codex, cursor, vscode, opencode, generic, claude-code' >&2
    exit 1
    ;;
esac

# Client-specific prerequisites are checked before authentication, so a typo
# or missing CLI cannot leave an approved but unused API key behind.
CURSOR_RUNTIME=''
CURSOR_CONFIG=''
CODEX_DIR=''
OPENCODE_DIR=''
case "$CLIENT" in
  claude)
    command -v claude >/dev/null 2>&1 || {
      echo 'claude CLI not found — install Claude Code first: https://claude.com/claude-code' >&2
      exit 1
    }
    ;;
  codex)
    command -v codex >/dev/null 2>&1 || {
      echo 'codex CLI not found — install Codex first: https://developers.openai.com/codex/' >&2
      exit 1
    }
    if [ -n "${CODEX_HOME:-}" ]; then
      CODEX_DIR="$CODEX_HOME"
    elif [ -n "${HOME:-}" ]; then
      CODEX_DIR="$HOME/.codex"
    else
      echo 'Neither CODEX_HOME nor HOME is set; cannot locate Codex config.' >&2
      exit 1
    fi
    ;;
  vscode)
    command -v code >/dev/null 2>&1 || {
      echo "code CLI not found — install VS Code and add its shell command to PATH." >&2
      exit 1
    }
    ;;
  opencode)
    command -v opencode >/dev/null 2>&1 || {
      echo 'opencode CLI not found — install OpenCode first: https://opencode.ai' >&2
      exit 1
    }
    if [ -n "${XDG_CONFIG_HOME:-}" ]; then
      OPENCODE_DIR="$XDG_CONFIG_HOME/opencode"
    elif [ -n "${HOME:-}" ]; then
      OPENCODE_DIR="$HOME/.config/opencode"
    else
      echo 'Neither XDG_CONFIG_HOME nor HOME is set; cannot protect the OpenCode config.' >&2
      exit 1
    fi
    ;;
  cursor)
    if [ -z "${HOME:-}" ]; then
      echo 'HOME is not set; cannot locate ~/.cursor/mcp.json.' >&2
      exit 1
    fi
    CURSOR_CONFIG="$HOME/.cursor/mcp.json"
    if command -v node >/dev/null 2>&1; then
      CURSOR_RUNTIME='node'
    elif command -v python3 >/dev/null 2>&1; then
      CURSOR_RUNTIME='python3'
    else
      echo 'Cursor setup requires node or python3 to safely update ~/.cursor/mcp.json.' >&2
      exit 1
    fi
    ;;
esac

# Preferred: browser sign-in (device flow). Override: SIM86_API_KEY in the env.
json_str() { sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'; }

KEY="${SIM86_API_KEY:-}"
if [ -z "$KEY" ]; then
  command -v curl >/dev/null 2>&1 || {
    echo 'curl is required for browser sign-in.' >&2
    exit 1
  }

  HOST=$(hostname 2>/dev/null || true)
  HOST=$(printf '%s' "$HOST" | tr -cd 'A-Za-z0-9._-')
  [ -n "$HOST" ] || HOST='device'
  CODE_JSON=$(curl -fsS -X POST "$API/device/code" -H 'content-type: application/json'     -d "{\"clientName\":\"$CLIENT · $HOST\"}") || {
      echo "Could not reach $API — check your connection." >&2
      exit 1
    }

  DEVICE_CODE=$(printf '%s' "$CODE_JSON" | json_str deviceCode)
  USER_CODE=$(printf '%s' "$CODE_JSON" | json_str userCode)
  VERIFY=$(printf '%s' "$CODE_JSON" | json_str verificationUriComplete)
  if printf '%s' "$CODE_JSON" | grep -q '"interval"[[:space:]]*:'; then
    INTERVAL=$(printf '%s' "$CODE_JSON" | sed -n 's/.*"interval"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
  else
    INTERVAL=3
  fi
  if printf '%s' "$CODE_JSON" | grep -q '"expiresIn"[[:space:]]*:'; then
    EXPIRES_IN=$(printf '%s' "$CODE_JSON" | sed -n 's/.*"expiresIn"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
  else
    EXPIRES_IN=600
  fi
  if [ -z "$DEVICE_CODE" ] || [ -z "$USER_CODE" ] || [ -z "$VERIFY" ]; then
    echo 'The device authorization response was incomplete; please try again.' >&2
    exit 1
  fi
  case "$INTERVAL:$EXPIRES_IN" in
    *[!0-9:]*|:*|*:)
      echo 'The device authorization response contained invalid timing values.' >&2
      exit 1
      ;;
  esac
  if [ "$INTERVAL" -lt 1 ] || [ "$EXPIRES_IN" -lt 1 ]; then
    echo 'The device authorization response contained invalid timing values.' >&2
    exit 1
  fi

  echo ''
  echo "  To connect $CLIENT, open this URL and approve:"
  echo ''
  echo "      $VERIFY"
  echo ''
  echo "  (verification code: $USER_CODE)"
  echo ''
  { command -v open >/dev/null 2>&1 && open "$VERIFY" >/dev/null 2>&1; } ||   { command -v xdg-open >/dev/null 2>&1 && xdg-open "$VERIFY" >/dev/null 2>&1; } || true

  printf '  Waiting for approval'
  ELAPSED=0
  while :; do
    sleep "$INTERVAL"
    ELAPSED=$((ELAPSED + INTERVAL))
    if [ "$ELAPSED" -ge "$EXPIRES_IN" ]; then
      echo ''
      echo 'Code expired — re-run to try again.' >&2
      exit 1
    fi
    printf '.'
    TOK=$(curl -sS -X POST "$API/device/token" -H 'content-type: application/json'       -d "{\"deviceCode\":\"$DEVICE_CODE\"}" 2>/dev/null || true)
    case "$TOK" in
      *'"apiKey"'*)
        KEY=$(printf '%s' "$TOK" | json_str apiKey)
        echo ' approved.'
        break
        ;;
      *access_denied*)
        echo ''
        echo 'Request was denied.' >&2
        exit 1
        ;;
      *expired_token*)
        echo ''
        echo 'Code expired — re-run to try again.' >&2
        exit 1
        ;;
      *) : ;;
    esac
  done
fi

# A credential is written into client configuration below. Accept only the
# exact API-key shape issued by Simulator86 before placing it in JSON or TOML.
KEY_SUFFIX=${KEY#sim86_live_}
if [ "$KEY_SUFFIX" = "$KEY" ] || [ "${#KEY_SUFFIX}" -ne 32 ]; then
  echo 'Invalid Simulator86 API key; expected sim86_live_ followed by 32 letters or digits.' >&2
  exit 1
fi
case "$KEY_SUFFIX" in
  *[!A-Za-z0-9]*)
    echo 'Invalid Simulator86 API key; expected sim86_live_ followed by 32 letters or digits.' >&2
    exit 1
    ;;
esac

case "$CLIENT" in
  claude)
    # Older installer versions used Claude's default local scope. Remove that
    # private current-project entry so it cannot shadow the new user entry.
    claude mcp remove --scope local "$NAME" </dev/null >/dev/null 2>&1 || true
    claude mcp remove --scope user "$NAME" </dev/null >/dev/null 2>&1 || true
    if ! claude mcp add --scope user --transport http "$NAME" "$URL"       --header "Authorization: Bearer $KEY" </dev/null >/dev/null 2>&1; then
      echo "Could not add '$NAME' to Claude Code." >&2
      exit 1
    fi
    echo "Added '$NAME' to Claude Code at user scope. Open a fresh session to use it."
    ;;
  codex)
    codex mcp remove "$NAME" </dev/null >/dev/null 2>&1 || true
    if ! codex mcp add "$NAME" --url "$URL" </dev/null >/dev/null 2>&1; then
      echo "Could not add '$NAME' to Codex." >&2
      exit 1
    fi
    mkdir -p "$CODEX_DIR"
    printf '
[mcp_servers.sim86.http_headers]
Authorization = "Bearer %s"
' "$KEY"       >> "$CODEX_DIR/config.toml"
    chmod 600 "$CODEX_DIR/config.toml"
    echo "Added '$NAME' to Codex. Restart Codex or open a fresh session to use it."
    ;;
  vscode)
    VSCODE_JSON=$(printf '{"name":"sim86","type":"http","url":"%s","headers":{"Authorization":"Bearer %s"}}' "$URL" "$KEY")
    if ! code --add-mcp "$VSCODE_JSON" </dev/null >/dev/null 2>&1; then
      echo "Could not add '$NAME' to VS Code." >&2
      exit 1
    fi
    echo "Added '$NAME' to VS Code. Restart the extension or open a fresh session to use it."
    ;;
  opencode)
    if ! opencode mcp add "$NAME" --url "$URL"       --header "Authorization=Bearer $KEY" </dev/null >/dev/null 2>&1; then
      echo "Could not add '$NAME' to OpenCode." >&2
      exit 1
    fi
    OPENCODE_CONFIG_FOUND=0
    for CONFIG_FILE in "$OPENCODE_DIR/opencode.jsonc" "$OPENCODE_DIR/opencode.json"; do
      if [ -f "$CONFIG_FILE" ]; then
        chmod 600 "$CONFIG_FILE"
        OPENCODE_CONFIG_FOUND=1
      fi
    done
    if [ -n "${OPENCODE_CONFIG:-}" ] && [ -f "$OPENCODE_CONFIG" ]; then
      chmod 600 "$OPENCODE_CONFIG"
      OPENCODE_CONFIG_FOUND=1
    fi
    if [ "$OPENCODE_CONFIG_FOUND" -ne 1 ]; then
      echo 'OpenCode added the server, but its config file could not be located safely.' >&2
      exit 1
    fi
    echo "Added '$NAME' to OpenCode. Open a fresh session to use it."
    ;;
  cursor)
    if [ "$CURSOR_RUNTIME" = 'node' ]; then
      SIM86_CURSOR_CONFIG="$CURSOR_CONFIG" SIM86_MCP_URL="$URL" SIM86_MCP_KEY="$KEY"         node --input-type=commonjs <<'SIM86_NODE'
const fs = require("node:fs");
const path = require("node:path");

const file = process.env.SIM86_CURSOR_CONFIG;
const url = process.env.SIM86_MCP_URL;
const key = process.env.SIM86_MCP_KEY;
const raw = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
let config = {};
if (raw.trim()) {
  try {
    config = JSON.parse(raw);
  } catch {
    throw new Error("Cursor MCP config is not valid JSON: " + file);
  }
}
if (!config || Array.isArray(config) || typeof config !== "object") {
  throw new Error("Cursor MCP config root must be a JSON object: " + file);
}
if (config.mcpServers === undefined) config.mcpServers = {};
if (!config.mcpServers || Array.isArray(config.mcpServers) || typeof config.mcpServers !== "object") {
  throw new Error("Cursor mcpServers must be a JSON object: " + file);
}
config.mcpServers.sim86 = {
  type: "http",
  url,
  headers: { Authorization: "Bearer " + key },
};

fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
const temporary = file + ".tmp-" + process.pid + "-" + Date.now();
try {
  fs.writeFileSync(temporary, JSON.stringify(config, null, 2) + "\n", {
    encoding: "utf8",
    flag: "wx",
    mode: 0o600,
  });
  fs.chmodSync(temporary, 0o600);
  fs.renameSync(temporary, file);
} catch (error) {
  try { fs.unlinkSync(temporary); } catch {}
  throw error;
}
SIM86_NODE
    else
      SIM86_CURSOR_CONFIG="$CURSOR_CONFIG" SIM86_MCP_URL="$URL" SIM86_MCP_KEY="$KEY"         python3 - <<'SIM86_PYTHON'
import json
import os
import tempfile

file = os.environ["SIM86_CURSOR_CONFIG"]
url = os.environ["SIM86_MCP_URL"]
key = os.environ["SIM86_MCP_KEY"]
config = {}
if os.path.exists(file):
    with open(file, encoding="utf-8") as source:
        raw = source.read()
    if raw.strip():
        try:
            config = json.loads(raw)
        except json.JSONDecodeError as error:
            raise RuntimeError("Cursor MCP config is not valid JSON: " + file) from error
if not isinstance(config, dict):
    raise RuntimeError("Cursor MCP config root must be a JSON object: " + file)
servers = config.setdefault("mcpServers", {})
if not isinstance(servers, dict):
    raise RuntimeError("Cursor mcpServers must be a JSON object: " + file)
servers["sim86"] = {
    "type": "http",
    "url": url,
    "headers": {"Authorization": "Bearer " + key},
}

directory = os.path.dirname(file)
os.makedirs(directory, mode=0o700, exist_ok=True)
descriptor, temporary = tempfile.mkstemp(prefix=".mcp.json.", dir=directory)
try:
    os.fchmod(descriptor, 0o600)
    with os.fdopen(descriptor, "w", encoding="utf-8") as destination:
        json.dump(config, destination, indent=2)
        destination.write("\n")
    os.replace(temporary, file)
except BaseException:
    try:
        os.unlink(temporary)
    except FileNotFoundError:
        pass
    raise
SIM86_PYTHON
    fi
    echo "Added '$NAME' to Cursor's user MCP config. Restart Cursor to use it."
    ;;
  generic)
    printf '
Simulator86 MCP endpoint:
  URL: %s
  Header: Authorization: Bearer %s

' "$URL" "$KEY"
    echo 'Add that HTTP endpoint and header using your MCP client configuration.'
    echo '(Manage keys at https://sim86.com → Cloud → API keys.)'
    ;;
esac
