#!/usr/bin/env bash
#
# Cosmos installer.
#
# Usage:
#   curl -fsSL https://cosmos.polarity-lab.com/install.sh | sh
#   curl -fsSL https://cosmos.polarity-lab.com/install.sh | sh -s -- --dry-run
#   curl -fsSL https://cosmos.polarity-lab.com/install.sh | sh -s -- --no-app
#
# Flags:
#   --dry-run   Print every step without making changes.
#   --no-app    Skip the macOS daemon app. Install MCP server only.
#   --help      Show this message.
#
# This script will:
#   1. Install @polarity-lab/cosmos-mcp from npm
#   2. Drop CosmosSync.app into /Applications and set it to launch at login
#      (macOS only, skipped with --no-app)
#   3. Register the cosmos-mcp:// URL scheme for one-tap key handoff
#   4. Merge cosmos into every MCP client you have installed (Claude Desktop,
#      Claude Code, Cursor, Codex, Zed, Continue) without touching any other
#      server you already have set up
#   5. Open your browser to cosmos.polarity-lab.com/connectors so you can
#      sign in and finish the keychain handoff
#
# The script is idempotent. Re-running it never duplicates entries and
# never overwrites existing MCP server config.
#

set -euo pipefail

DRY_RUN=0
SKIP_APP=0
NPM_PKG="@polarity-lab/cosmos-mcp"
COSMOS_URL="https://cosmos.polarity-lab.com/connectors?install=1"

while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) DRY_RUN=1; shift ;;
    --no-app)  SKIP_APP=1; shift ;;
    --help|-h)
      sed -n '2,30p' "$0" 2>/dev/null || true
      exit 0
      ;;
    *) echo "unknown flag $1" >&2; exit 2 ;;
  esac
done

# ----- pretty output -----
BOLD=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RESET=$(printf '\033[0m')
GREEN=$(printf '\033[32m'); YELLOW=$(printf '\033[33m'); RED=$(printf '\033[31m')

step()  { printf "${BOLD}%s${RESET} %s\n" "$1" "$2"; }
ok()    { printf "  ${GREEN}ok${RESET}    %s\n" "$1"; }
skip()  { printf "  ${DIM}skip${RESET}  %s\n" "$1"; }
warn()  { printf "  ${YELLOW}warn${RESET}  %s\n" "$1"; }
fail()  { printf "  ${RED}fail${RESET}  %s\n" "$1"; exit 1; }
note()  { printf "        ${DIM}%s${RESET}\n" "$1"; }

run() {
  if [ $DRY_RUN -eq 1 ]; then
    printf "  ${DIM}would run${RESET} %s\n" "$*"
  else
    "$@"
  fi
}

run_sh() {
  if [ $DRY_RUN -eq 1 ]; then
    printf "  ${DIM}would run${RESET} %s\n" "$*"
  else
    sh -c "$*"
  fi
}

backup_file() {
  local f="$1"
  [ -f "$f" ] || return 0
  local ts
  ts=$(date +%Y%m%d-%H%M%S)
  if [ $DRY_RUN -eq 1 ]; then
    printf "  ${DIM}would back up${RESET} %s -> %s\n" "$f" "$f.bak.$ts"
  else
    cp -p "$f" "$f.bak.$ts"
  fi
}

# ----- platform detection -----
PLATFORM="other"
case "$(uname -s)" in
  Darwin) PLATFORM="mac" ;;
  Linux)  PLATFORM="linux" ;;
esac

if [ "$PLATFORM" != "mac" ]; then
  SKIP_APP=1
fi

printf "\n${BOLD}cosmos installer${RESET}\n"
printf "${DIM}platform: %s · dry-run: %s · daemon app: %s${RESET}\n\n" \
  "$PLATFORM" \
  "$([ $DRY_RUN -eq 1 ] && echo yes || echo no)" \
  "$([ $SKIP_APP -eq 1 ] && echo no || echo yes)"

# ----- 1. node / npm -----
step "1." "checking node"
if ! command -v node >/dev/null 2>&1; then
  warn "node not found"
  if [ "$PLATFORM" = "mac" ] && command -v brew >/dev/null 2>&1; then
    note "installing node@20 via Homebrew"
    run brew install node@20
  else
    fail "install Node.js 20+ from https://nodejs.org and re-run"
  fi
fi
NODE_VER=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VER" -lt 20 ]; then
  fail "node $NODE_VER detected. cosmos needs Node 20+. upgrade and re-run"
fi
ok "node $(node -v)"

if ! command -v npm >/dev/null 2>&1; then
  fail "npm not found alongside node. something is wrong with your install"
fi

# ----- 2. npm package -----
step "2." "installing $NPM_PKG"
NPM_GLOBAL_ROOT=$(npm root -g 2>/dev/null || echo "")
if [ -z "$NPM_GLOBAL_ROOT" ]; then
  fail "npm root -g returned empty. check npm config"
fi

# Try a userland-friendly install. If it fails on EACCES, fall back to sudo.
if [ $DRY_RUN -eq 1 ]; then
  note "would run: npm install -g $NPM_PKG"
elif npm install -g "$NPM_PKG" >/tmp/cosmos-install-npm.log 2>&1; then
  ok "installed via npm"
elif grep -q EACCES /tmp/cosmos-install-npm.log; then
  warn "npm global install needs sudo on this machine"
  sudo npm install -g "$NPM_PKG"
  ok "installed via sudo npm"
else
  cat /tmp/cosmos-install-npm.log >&2
  fail "npm install failed. see output above"
fi

PKG_DIR="$NPM_GLOBAL_ROOT/$NPM_PKG"
SYNC_APP_SRC="$PKG_DIR/dist/CosmosSync.app"

# ----- 3. daemon app -----
if [ $SKIP_APP -eq 0 ]; then
  step "3." "installing Cosmos Sync.app"
  if [ ! -d "$SYNC_APP_SRC" ] && [ $DRY_RUN -eq 0 ]; then
    warn "CosmosSync.app not bundled in this version of the npm package"
    note "skipping app install. MCP server alone will still work"
    SKIP_APP=1
  else
    APP_DEST="/Applications/Cosmos Sync.app"
    if [ -d "$APP_DEST" ]; then
      skip "Cosmos Sync.app already in /Applications"
    else
      run cp -R "$SYNC_APP_SRC" "$APP_DEST"
      [ $DRY_RUN -eq 1 ] || ok "copied to /Applications"
    fi

    # Launch-at-login via System Events
    LOGIN_SCRIPT='tell application "System Events" to if not (exists login item "Cosmos Sync") then make login item at end with properties {path:"/Applications/Cosmos Sync.app", hidden:false}'
    if [ $DRY_RUN -eq 1 ]; then
      note "would register login item via osascript"
    else
      if osascript -e "$LOGIN_SCRIPT" >/dev/null 2>&1; then
        ok "set to launch at login"
      else
        warn "could not add login item (System Events permission?)"
      fi
    fi
  fi
else
  step "3." "skipping daemon app"
  if [ "$PLATFORM" != "mac" ]; then
    note "the Sync.app is macOS only. MCP server still works everywhere"
  fi
fi

# ----- 4. URL scheme handler -----
if [ "$PLATFORM" = "mac" ]; then
  step "4." "registering cosmos-mcp:// handler"
  if [ $DRY_RUN -eq 1 ]; then
    note "would run: npx -y $NPM_PKG install-handler"
  else
    npx -y "$NPM_PKG" install-handler >/dev/null 2>&1 \
      && ok "handler registered" \
      || warn "handler registration returned non-zero (re-run later if deep links do not work)"
  fi
else
  step "4." "skipping URL handler (mac only)"
fi

# ----- 5. MCP config merging -----
step "5." "wiring cosmos into your MCP clients"

# Use python3 for JSON because jq is not always installed.
PY=python3
if ! command -v $PY >/dev/null 2>&1; then
  warn "python3 not found. skipping MCP merge. add cosmos manually per https://cosmos.polarity-lab.com/install"
else

  merge_json_mcp() {
    # merge_json_mcp <config_path> [<key>]
    # Default key is "mcpServers" (Claude Desktop, Cursor). Pass
    # "context_servers" for Zed. Prints "present" or "added" to stdout.
    local cfg="$1"
    local key="${2:-mcpServers}"
    if [ $DRY_RUN -eq 1 ]; then
      if [ -f "$cfg" ] && "$PY" -c "import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if 'cosmos' in d.get(sys.argv[2],{}) else 1)" "$cfg" "$key" 2>/dev/null; then
        echo present
      else
        echo dry-add
      fi
      return
    fi
    mkdir -p "$(dirname "$cfg")"
    backup_file "$cfg"
    KEY="$key" "$PY" - "$cfg" <<'PYEOF'
import json, os, sys
path = sys.argv[1]
key = os.environ["KEY"]
try:
    with open(path) as f:
        data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
    data = {}
servers = data.setdefault(key, {})
if "cosmos" in servers:
    print("present")
    sys.exit(0)
servers["cosmos"] = {
    "command": "npx",
    "args": ["-y", "@polarity-lab/cosmos-mcp"]
}
tmp = path + ".tmp"
with open(tmp, "w") as f:
    json.dump(data, f, indent=2)
os.replace(tmp, path)
print("added")
PYEOF
  }

  report_merge() {
    # report_merge <client_label> <result>
    case "$2" in
      present) skip "$1 already has cosmos" ;;
      added)   ok   "$1 merged (other servers preserved)" ;;
      dry-add) note "would add cosmos to $1 (other servers preserved)" ;;
      *)       warn "$1 merge result unclear" ;;
    esac
  }

  # --- Claude Desktop ---
  CLAUDE_DESKTOP="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
  if [ "$PLATFORM" = "mac" ]; then
    report_merge "Claude Desktop" "$(merge_json_mcp "$CLAUDE_DESKTOP")"
  fi

  # --- Cursor ---
  CURSOR_CFG="$HOME/.cursor/mcp.json"
  if [ -d "$HOME/.cursor" ] || [ -f "$CURSOR_CFG" ]; then
    report_merge "Cursor" "$(merge_json_mcp "$CURSOR_CFG")"
  else
    skip "Cursor not installed"
  fi

  # --- Zed ---
  ZED_CFG="$HOME/.config/zed/settings.json"
  if [ -d "$HOME/.config/zed" ] || command -v zed >/dev/null 2>&1; then
    report_merge "Zed" "$(merge_json_mcp "$ZED_CFG" "context_servers")"
  else
    skip "Zed not installed"
  fi

  # --- Continue ---
  # Continue uses YAML now (config.yaml), with config.json as legacy. YAML
  # merge in a portable shell script is fragile (comment + format
  # preservation), so we only auto-merge the legacy JSON form. For YAML
  # users we print the copy-paste snippet they need.
  CONTINUE_YAML="$HOME/.continue/config.yaml"
  CONTINUE_JSON="$HOME/.continue/config.json"
  if [ -f "$CONTINUE_JSON" ]; then
    # Legacy JSON form. mcpServers is an array, not an object — handle
    # inline rather than via the object-shaped merge_json_mcp helper.
    if [ $DRY_RUN -eq 1 ]; then
      note "would add cosmos to Continue (legacy config.json)"
    else
      backup_file "$CONTINUE_JSON"
      "$PY" - "$CONTINUE_JSON" <<'PYEOF'
import json, os, sys
path = sys.argv[1]
with open(path) as f:
    data = json.load(f)
servers = data.setdefault("mcpServers", [])
if any(s.get("name") == "cosmos" for s in servers):
    print("Continue (json) already has cosmos")
    sys.exit(0)
servers.append({
    "name": "cosmos",
    "command": "npx",
    "args": ["-y", "@polarity-lab/cosmos-mcp"]
})
tmp = path + ".tmp"
with open(tmp, "w") as f:
    json.dump(data, f, indent=2)
os.replace(tmp, path)
print("Continue (json) merged (other servers preserved)")
PYEOF
    fi
  elif [ -f "$CONTINUE_YAML" ]; then
    warn "Continue uses YAML config; auto-merge skipped"
    note "add this block under mcpServers in $CONTINUE_YAML:"
    note "  - name: cosmos"
    note "    command: npx"
    note "    args: [\"-y\", \"@polarity-lab/cosmos-mcp\"]"
  elif [ -d "$HOME/.continue" ]; then
    skip "Continue installed but no config found"
  else
    skip "Continue not installed"
  fi

  # --- Claude Code (CLI) ---
  # Prefer the CLI's own command so we do not hand-edit ~/.claude.json.
  if command -v claude >/dev/null 2>&1; then
    if claude mcp list 2>/dev/null | grep -q "^cosmos\b"; then
      skip "Claude Code already has cosmos"
    elif [ $DRY_RUN -eq 1 ]; then
      note "would run: claude mcp add cosmos --scope user -- npx -y $NPM_PKG"
    else
      claude mcp add cosmos --scope user -- npx -y "$NPM_PKG" >/dev/null 2>&1 \
        && ok "Claude Code wired (user scope)" \
        || warn "claude mcp add returned non-zero. add manually if needed"
    fi
  else
    skip "Claude Code CLI not installed"
  fi

  # --- Codex ---
  CODEX_CFG="$HOME/.codex/config.toml"
  if [ -f "$CODEX_CFG" ] || command -v codex >/dev/null 2>&1; then
    mkdir -p "$HOME/.codex"
    if grep -q '^\[mcp_servers\.cosmos\]' "$CODEX_CFG" 2>/dev/null; then
      skip "Codex already has cosmos"
    elif [ $DRY_RUN -eq 1 ]; then
      note "would append [mcp_servers.cosmos] block to $CODEX_CFG"
    else
      backup_file "$CODEX_CFG"
      cat >> "$CODEX_CFG" <<TOMLEOF

[mcp_servers.cosmos]
command = "npx"
args = ["-y", "@polarity-lab/cosmos-mcp"]
TOMLEOF
      ok "Codex appended (existing servers preserved)"
    fi
  else
    skip "Codex not installed"
  fi
fi

# ----- 6. open browser for key handoff -----
step "6." "opening cosmos.polarity-lab.com for key handoff"
if [ $DRY_RUN -eq 1 ]; then
  note "would open $COSMOS_URL"
elif [ "$PLATFORM" = "mac" ]; then
  open "$COSMOS_URL" 2>/dev/null || note "open this in your browser when ready: $COSMOS_URL"
elif command -v xdg-open >/dev/null 2>&1; then
  xdg-open "$COSMOS_URL" >/dev/null 2>&1 || true
else
  note "open this in your browser: $COSMOS_URL"
fi

# ----- done -----
printf "\n${BOLD}${GREEN}done.${RESET}\n"
printf "open Claude Code or Cursor and ask cosmos what it knows about you.\n\n"
printf "${DIM}docs:        https://cosmos.polarity-lab.com/install${RESET}\n"
printf "${DIM}source:      https://github.com/teampolarity/cosmos-mcp${RESET}\n"
printf "${DIM}every config above has a .bak.<timestamp> beside it if you want to roll back.${RESET}\n\n"
