#!/usr/bin/env bash
# claude-multiacc installer — macOS + Linux, idempotent, fully reversible.
# Installs an addon that PATH-shadows `claude`; NEVER touches the Claude Code app,
# its install dir, or its update machinery.
#
#   ./install.sh [--server user@host]     install or update (data untouched)
#   ./install.sh --no-server              install with NO sync target (local-only pool:
#                                         a panel/runner daemon distributes accounts)
#   ./install.sh --instance NAME          label this install's agents (default: derived
#                                         from the pool root when it is not the default)
#   ./install.sh --uninstall              remove addon (keeps ~/.claude-accounts)
#   ./install.sh --uninstall --purge-data remove addon + all account data
#
# INSTANCE-SCOPED POOLS: export CLAUDE_ACCOUNTS_ROOT / CODEX_ACCOUNTS_ROOT before
# running this and the whole install serves that pool — its LaunchAgents/cron entries
# get their own labels and carry the roots in their environment, so several app-robot
# instances on one machine never touch each other's accounts or agents.
set -u

REPO_DIR="$(cd "$(dirname "$0")" && pwd -P)"
# shellcheck source=lib/common.sh
. "$REPO_DIR/lib/common.sh"
. "$REPO_DIR/lib/install_actions.sh"
# common.sh is sourced in claude mode (ACC_ROOT = the claude pool); the codex pool
# root is needed here too for the codex agents and uninstall/purge messaging.
CODEX_ACC_ROOT="${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-$HOME/.codex-accounts}}"

UNINSTALL=0
PURGE=0
SERVER_OVERRIDE=""
NO_SCHEDULE=0
INSTANCE_OVERRIDE=""
while [ $# -gt 0 ]; do
  case "$1" in
    --help|-h) cat "$REPO_DIR/lib/install-help.txt"; exit 0 ;;
    --uninstall) UNINSTALL=1; shift ;;
    --purge-data) PURGE=1; shift ;;
    --server) SERVER_OVERRIDE="${2:?--server requires a value}"; shift 2 ;;
    --no-server) SERVER_OVERRIDE="none"; shift ;;
    --instance) INSTANCE_OVERRIDE="${2:?--instance requires a name}"; shift 2 ;;
    --no-schedule) NO_SCHEDULE=1; shift ;;
    *) echo "unknown flag: $1" >&2; exit 1 ;;
  esac
done

# ---- instance scoping -----------------------------------------------------------
# An install whose pool roots are the defaults keeps the historical labels and rc
# blocks byte-for-byte. Anything else is an INSTANCE: its agents get a suffix (two
# instances would otherwise overwrite each other's plists) and the shell rc block is
# left alone, because one interactive PATH cannot point at two pools at once.
default_pool_roots() {
  [ "$ACC_ROOT" = "$HOME/.claude-accounts" ] && [ "$CODEX_ACC_ROOT" = "$HOME/.codex-accounts" ]
}
short_hash() { # stable 8-char digest of $1, on macOS and Linux alike
  if command -v shasum >/dev/null 2>&1; then printf '%s' "$1" | shasum | cut -c1-8
  elif command -v sha1sum >/dev/null 2>&1; then printf '%s' "$1" | sha1sum | cut -c1-8
  else printf '%s' "$1" | cksum | tr -d ' ' | cut -c1-8; fi
}
if [ -n "$INSTANCE_OVERRIDE" ]; then
  case "$INSTANCE_OVERRIDE" in
    *[!A-Za-z0-9-]*) echo "--instance must be letters, digits and dashes: $INSTANCE_OVERRIDE" >&2; exit 1 ;;
  esac
  INSTANCE="$INSTANCE_OVERRIDE"
elif default_pool_roots; then
  INSTANCE=""
else
  INSTANCE="i$(short_hash "$ACC_ROOT")"
fi
# The roots are interpolated into plists and crontab lines; a quote or newline in one
# would break out of that context, so such a root is refused up front.
case "$ACC_ROOT$CODEX_ACC_ROOT" in
  *["'\"<>&"]*|*"
"*) echo "pool root contains a character that cannot be scheduled safely: $ACC_ROOT / $CODEX_ACC_ROOT" >&2; exit 1 ;;
esac

# A pool root under a temp directory is by definition gone tomorrow, but the launchd
# agent or crontab line naming it is not: it keeps firing forever against a path that
# no longer exists, one leaked set per run. That is how a Mac collected 57 orphaned
# agent sets and a server 4 stray cron blocks. EITHER root disqualifies the whole set,
# because one install writes agents for both providers: the leaked sets above named a
# temp CLAUDE root and the REAL codex pool, so they went right on polling a live usage
# endpoint every five minutes. Install the binaries for such a root; never schedule it.
ephemeral_root() { # $1 = a pool root
  case "$1" in
    /tmp/*|/private/tmp/*|/var/tmp/*|/private/var/tmp/*|/var/folders/*|/private/var/folders/*)
      return 0 ;;
    *) return 1 ;;
  esac
}
if [ "$NO_SCHEDULE" != "1" ] && { ephemeral_root "$ACC_ROOT" || ephemeral_root "$CODEX_ACC_ROOT"; }; then
  NO_SCHEDULE=1
  echo "  note: a pool root is under a temp directory (claude: $ACC_ROOT, codex: $CODEX_ACC_ROOT)" >&2
  echo "        — installing WITHOUT schedulers, which would outlive it." >&2
  echo "        Pass --no-schedule to silence this." >&2
fi

LABEL="com.claude-multiacc${INSTANCE:+.$INSTANCE}"
MARK_BEGIN="# >>> claude-multiacc >>>"
MARK_END="# <<< claude-multiacc <<<"
CRON_TAG="# claude-multiacc${INSTANCE:+ $INSTANCE}"
PLIST_LIMITS="$HOME/Library/LaunchAgents/$LABEL.limits.plist"
PLIST_HEALTH="$HOME/Library/LaunchAgents/$LABEL.health.plist"
PLIST_UPDATE="$HOME/Library/LaunchAgents/$LABEL.update.plist"
PLIST_CODEX_LIMITS="$HOME/Library/LaunchAgents/$LABEL.codex-limits.plist"
PLIST_CODEX_HEALTH="$HOME/Library/LaunchAgents/$LABEL.codex-health.plist"
PROFILED="/etc/profile.d/claude-multiacc.sh"

# Environment for a scheduled agent: a usable PATH always, plus this instance's pool
# roots when the install is instance-scoped.
#
# launchd hands an agent PATH=/usr/bin:/bin:/usr/sbin:/sbin — no /opt/homebrew, no
# nvm — so `npm`, `node` and a homebrew `git` are all absent from a plain agent run.
# The daily self-update agent logged "npm not found; skipping" every night for days
# while reporting nothing wrong, and every Mac's npm copy silently froze. The code
# now resolves npm by absolute path too (lib/common.sh find_npm), but an agent whose
# environment cannot even find node is a trap waiting for the next tool we add.
plist_env_block() {
  printf '  <key>EnvironmentVariables</key><dict>\n'
  printf '    <key>PATH</key><string>%s</string>\n' \
    "/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
  if [ -n "$INSTANCE" ]; then
    printf '    <key>CLAUDE_ACCOUNTS_ROOT</key><string>%s</string>\n' "$ACC_ROOT"
    printf '    <key>CODEX_ACCOUNTS_ROOT</key><string>%s</string>\n' "$CODEX_ACC_ROOT"
  fi
  printf '  </dict>\n'
}
cron_env_prefix() {
  [ -n "$INSTANCE" ] || return 0
  printf "CLAUDE_ACCOUNTS_ROOT='%s' CODEX_ACCOUNTS_ROOT='%s' " "$ACC_ROOT" "$CODEX_ACC_ROOT"
}

mac_schedule_install() {
  mkdir -p "$HOME/Library/LaunchAgents"
  . "$REPO_DIR/lib/install_calendar.sh"
  cat > "$PLIST_LIMITS" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>$LABEL.limits</string>
  <key>ProgramArguments</key><array>
    <string>$REPO_DIR/bin/claude-accounts</string>
    <string>limits</string>
    <string>--quiet</string>
  </array>
$(plist_env_block)  <key>StartInterval</key><integer>900</integer>
  <key>RunAtLoad</key><true/>
  <!-- Preserve the detached telemetry push after this job exits. -->
  <key>AbandonProcessGroup</key><true/>
  <key>StandardOutPath</key><string>/dev/null</string>
  <key>StandardErrorPath</key><string>/dev/null</string>
</dict></plist>
EOF
  cat > "$PLIST_CODEX_LIMITS" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>$LABEL.codex-limits</string>
  <key>ProgramArguments</key><array>
    <string>$REPO_DIR/bin/codex-accounts</string>
    <string>limits</string>
    <string>--quiet</string>
  </array>
$(plist_env_block)  <key>StartInterval</key><integer>300</integer>
  <key>RunAtLoad</key><true/>
  <key>AbandonProcessGroup</key><true/>
  <key>StandardOutPath</key><string>/dev/null</string>
  <key>StandardErrorPath</key><string>/dev/null</string>
</dict></plist>
EOF
  local plist
  for plist in "$PLIST_LIMITS" "$PLIST_HEALTH" "$PLIST_UPDATE" "$PLIST_CODEX_LIMITS" "$PLIST_CODEX_HEALTH"; do
    launchctl unload "$plist" 2>/dev/null || true
  done
  launchctl load -w "$PLIST_LIMITS" 2>/dev/null || echo "  (limits agent failed — the shim will refresh on demand)"
  launchctl load -w "$PLIST_HEALTH" 2>/dev/null || true
  launchctl load -w "$PLIST_CODEX_LIMITS" 2>/dev/null || true
  launchctl load -w "$PLIST_CODEX_HEALTH" 2>/dev/null || true
  [ "${CLAUDE_MULTIACC_AUTOUPDATE:-1}" = "1" ] && launchctl load -w "$PLIST_UPDATE" 2>/dev/null || true
  echo "  launchd: limits refresh (claude 15m / codex 5m) + weekly health checks + daily auto-update"
}

mac_schedule_remove() {
  launchctl unload "$PLIST_LIMITS" 2>/dev/null || true
  launchctl unload "$PLIST_HEALTH" 2>/dev/null || true
  launchctl unload "$PLIST_UPDATE" 2>/dev/null || true
  launchctl unload "$PLIST_CODEX_LIMITS" 2>/dev/null || true
  launchctl unload "$PLIST_CODEX_HEALTH" 2>/dev/null || true
  rm -f "$PLIST_LIMITS" "$PLIST_HEALTH" "$PLIST_UPDATE" "$PLIST_CODEX_LIMITS" "$PLIST_CODEX_HEALTH"
}

# Only THIS instance's lines are dropped: the tag is matched anchored to end of line,
# so a default install ("# claude-multiacc") never removes an instance's lines
# ("# claude-multiacc iXXXXXXXX") or the other way round.
cron_strip_ours() { grep -v -E "$CRON_TAG\$" || true; }

linux_schedule_install() {
  local tmp env
  tmp="$(mktemp)"
  env="$(cron_env_prefix)"
  { crontab -l 2>/dev/null | cron_strip_ours; } > "$tmp"
  {
    echo "*/15 * * * * $env$REPO_DIR/bin/claude-accounts limits --quiet >/dev/null 2>&1 $CRON_TAG"
    echo "*/5 * * * * $env$REPO_DIR/bin/codex-accounts limits --quiet >/dev/null 2>&1 $CRON_TAG"
    echo "17 9 * * 1 $env$REPO_DIR/bin/claude-accounts health >/dev/null 2>&1 $CRON_TAG"
    echo "37 9 * * 1 $env$REPO_DIR/bin/codex-accounts health >/dev/null 2>&1 $CRON_TAG"
    [ "${CLAUDE_MULTIACC_AUTOUPDATE:-1}" = "1" ] && \
      echo "7 4 * * * $env$REPO_DIR/bin/claude-accounts self-update --quiet >/dev/null 2>&1 $CRON_TAG"
  } >> "$tmp"
  crontab "$tmp"
  rm -f "$tmp"
  echo "  cron: limits refresh (claude 15m / codex 5m) + weekly health checks + daily auto-update"
}

linux_schedule_remove() {
  local tmp
  tmp="$(mktemp)"
  { crontab -l 2>/dev/null | cron_strip_ours; } > "$tmp"
  crontab "$tmp"
  rm -f "$tmp"
}

if [ "$UNINSTALL" = "1" ]; then
  do_uninstall
else
  do_install
fi
