#!/usr/bin/env bash
# Installer actions, sourced by install.sh. Bash 3.2 compatible.

is_pool_root() { # true when $1 is safe to delete as an account pool
  case "$1" in
    ''|/|"$HOME") return 1 ;;
    */.claude-accounts|*/.codex-accounts) return 0 ;;
    /*) [ -f "$1/accounts.json" ] ;;
    *) return 1 ;;
  esac
}

strip_block() { # remove our marked block from a file (portable, no sed -i)
  local f="$1"
  [ -f "$f" ] || return 0
  # Unmatched begin marker (end line hand-deleted): stripping would eat the rest
  # of the rc file. Leave it alone and say so.
  if grep -qF "$MARK_BEGIN" "$f" && ! grep -qF "$MARK_END" "$f"; then
    echo "  WARNING: $f has an unterminated claude-multiacc block — fix it by hand; not touching this file" >&2
    return 1
  fi
  awk -v b="$MARK_BEGIN" -v e="$MARK_END" '
    $0 == b { skip = 1; next }
    $0 == e { skip = 0; next }
    !skip { print }
  ' "$f" > "$f.claude-multiacc.tmp" && mv "$f.claude-multiacc.tmp" "$f"
}

path_block_body() {
  # Move (not just add) the shim dir to the front: later rc lines prepend
  # ~/.local/bin, so a plain add-once guard would leave the real binary first.
  printf 'PATH="$(printf %%s ":$PATH:" | sed '\''s|:%s/bin:|:|g; s|^:||; s|:$||'\'')"\n' "$REPO_DIR"
  printf 'export PATH="%s/bin:$PATH"\n' "$REPO_DIR"
}

path_guard_body() {
  # The two lines above win only while they are the LAST thing to touch PATH. A later
  # rc line, a tool that rewrites PATH (conda, nvm, a venv) or an rc file that was
  # INTERRUPTED before reaching them (a Ctrl-C at a slow `conda` hook, my-mini
  # 2026-09-17) leaves ~/.local/bin in front again — and the next `claude` typed at
  # that prompt runs the real binary on the machine's own ~/.claude login (or on no
  # login at all), never reaching the pool. Nothing inside the shim can notice a launch
  # that never reached it, so the rc block also registers a prompt hook (zsh precmd /
  # bash PROMPT_COMMAND) that puts the shim dir back in front before EVERY prompt.
  # Fork-free while PATH is already right, registered once, inert once the addon is
  # gone. The array syntax is wrapped in eval so sh/dash — which read ~/.profile and
  # /etc/profile.d as well — can parse the block.
  printf '_claude_multiacc_path_guard() {\n'
  printf '  [ -x "%s/bin/claude" ] || return 0\n' "$REPO_DIR"
  printf '  case ":$PATH:" in ":%s/bin:"*) return 0 ;; esac\n' "$REPO_DIR"
  printf '  PATH="$(printf %%s ":$PATH:" | sed '\''s|:%s/bin:|:|g; s|^:||; s|:$||'\'')"\n' "$REPO_DIR"
  printf '  export PATH="%s/bin:$PATH"\n' "$REPO_DIR"
  printf '}\n'
  # Registered LAST on purpose, in both shells: a hook that runs after this one and
  # rewrites PATH (direnv's, a venv's) would otherwise have the final say at the
  # prompt. zsh removes any earlier registration and re-appends, so the copy of this
  # block at the END of ~/.zshrc outranks hooks that ~/.zshrc registered before it;
  # the filter is IFS-independent. bash appends once — its rc block is the last thing
  # sourced, so an append lands last.
  printf 'if [ -n "${ZSH_VERSION:-}" ]; then\n'
  printf '  eval '\''precmd_functions=(${precmd_functions:#_claude_multiacc_path_guard} _claude_multiacc_path_guard)'\''\n'
  printf 'elif [ -n "${BASH_VERSION:-}" ]; then\n'
  printf '  case ";${PROMPT_COMMAND:-};" in *";_claude_multiacc_path_guard;"*) ;; *) PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND;}_claude_multiacc_path_guard" ;; esac\n'
  printf 'fi\n'
}

rc_block() { # the complete marked block, for every rc file and /etc/profile.d alike
  printf '%s\n' "$MARK_BEGIN"
  path_block_body
  path_guard_body
  printf '%s\n' "$MARK_END"
}

append_block() { # strip then append our PATH block to a file
  local f="$1"
  strip_block "$f" || return 1   # never create a second block next to a broken one
  rc_block >> "$f"
}

do_uninstall() {
  echo "claude-multiacc: uninstalling (restoring stock behavior)"
  # An instance install never wrote a shell rc block, a /etc/profile.d file or the
  # /usr/local/bin shims — those belong to the DEFAULT install and are shared with it.
  if [ -n "$INSTANCE" ]; then
    echo "  instance $INSTANCE: removing its agents only (PATH block and shims belong to the default install)"
  else
    strip_block "$HOME/.zshenv"
    strip_block "$HOME/.zprofile"
    strip_block "$HOME/.zshrc"
    strip_block "$HOME/.bashrc"
    strip_block "$HOME/.bash_profile"
    strip_block "$HOME/.profile"
  fi
  if [ "$(machine_kind)" = "mac" ]; then
    mac_schedule_remove
  else
    linux_schedule_remove
    if [ -z "$INSTANCE" ]; then
      [ -f "$PROFILED" ] && rm -f "$PROFILED"
      if [ -L /usr/local/bin/claude ] \
        && [ "$(canon_path /usr/local/bin/claude)" = "$(canon_path "$REPO_DIR/bin/claude")" ]; then
        rm -f /usr/local/bin/claude
        echo "  removed /usr/local/bin/claude shim symlink"
      fi
      if [ -L /usr/local/bin/codex ] \
        && [ "$(canon_path /usr/local/bin/codex)" = "$(canon_path "$REPO_DIR/bin/codex")" ]; then
        rm -f /usr/local/bin/codex
        echo "  removed /usr/local/bin/codex shim symlink"
      fi
    fi
  fi
  local root
  for root in "$ACC_ROOT" "$CODEX_ACC_ROOT"; do
    if [ "$PURGE" = "1" ]; then
      # Either the conventional location, or a directory that is provably a pool (it
      # holds a manifest) — an instance root lives anywhere, so the manifest is what
      # makes `rm -rf` safe. Anything else is left alone.
      if is_pool_root "$root"; then
        rm -rf "$root"; echo "  purged $root"
      else
        echo "  refusing to purge unusual accounts root: $root" >&2
      fi
    fi
  done
  if [ "$PURGE" != "1" ]; then
    echo "  account data kept at $ACC_ROOT and $CODEX_ACC_ROOT (use --purge-data to remove)"
  fi
  echo "uninstall complete — stock claude behavior restored"
}

install_binaries() {
  [ -x "$REPO_DIR/bin/claude" ] || chmod +x "$REPO_DIR/bin/claude" 2>/dev/null || true
  [ -x "$REPO_DIR/bin/claude-accounts" ] || chmod +x "$REPO_DIR/bin/claude-accounts" 2>/dev/null || true
  [ -x "$REPO_DIR/bin/codex" ] || chmod +x "$REPO_DIR/bin/codex" 2>/dev/null || true
  [ -x "$REPO_DIR/bin/codex-accounts" ] || chmod +x "$REPO_DIR/bin/codex-accounts" 2>/dev/null || true

  local real
  if real="$(find_real_claude "$REPO_DIR/bin/claude")"; then
    echo "  real claude binary: $real ($("$real" --version 2>/dev/null | head -1 || echo 'version unknown'))"
  else
    echo "  WARNING: no real claude binary found — install Claude Code (https://claude.com/claude-code)" >&2
  fi
  local real_codex
  if real_codex="$(find_real_codex "$REPO_DIR/bin/codex")"; then
    echo "  real codex binary: $real_codex"
    "$real_codex" --version 2>/dev/null | head -1 || echo '  version unknown'
  else
    echo "  note: no codex binary found — install Codex CLI with: npm i -g @openai/codex"
  fi

  # Keychain-mode note: Claude Code keeps per-config-dir logins in the login Keychain
  # (one item per dir) whenever the session can open it, and only sessions without
  # keychain access (ssh, launchd background jobs) fall back to .credentials.json. The
  # pool reads both (lib/keychain.py); what an operator has to know is that a login
  # made from a GUI session is invisible to their ssh sessions — mint a portable
  # token for anything that must work from everywhere.
  if [ "$kind" = "mac" ] && [ ! -f "$HOME/.claude/.credentials.json" ] \
    && security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then
    echo "  note: this Mac keeps Claude Code logins in the Keychain. Per-dir logins made here"
    echo "  are read from it; ssh/background sessions cannot open it (they see 'locked') —"
    echo "  use 'claude-accounts mint <acct-NN>' for accounts that must work from everywhere."
  fi
}

install_pools() {
  manifest_init "${SERVER_OVERRIDE:-$DEFAULT_SERVER}"
  if [ -n "$SERVER_OVERRIDE" ]; then
    "$PYBIN" - "$MANIFEST" "$SERVER_OVERRIDE" <<'PYEOF'
import json, os, sys
doc = json.load(open(sys.argv[1]))
doc['server'] = sys.argv[2]
with open(sys.argv[1] + '.tmp', 'w') as f:
    json.dump(doc, f, indent=2)
    f.write('\n')
os.replace(sys.argv[1] + '.tmp', sys.argv[1])
PYEOF
  fi
  if sync_target_is_local "$(sync_target)"; then
    echo "  account pool: $ACC_ROOT (manifest ready; sync target: none — local-only)"
  else
    echo "  account pool: $ACC_ROOT (manifest ready; sync target: $(sync_target))"
  fi
  # The codex pool gets its own skeleton + manifest (same schema, separate root).
  if ! "$REPO_DIR/bin/codex-accounts" init-pool "${SERVER_OVERRIDE:-}" >/dev/null 2>&1; then
    echo "  WARNING: codex pool init failed (codex-accounts init-pool)" >&2
  else
    echo "  codex account pool: $CODEX_ACC_ROOT (manifest ready)"
  fi
}

# bash and sh read ~/.profile for a LOGIN shell when ~/.bash_profile and ~/.bash_login
# are absent — `bash -lc`, `sh -lc`, a tmux/ssh session whose login shell is bash. Left
# without a block, such a shell ends its startup with whatever /etc/profile and its own
# ~/.local/bin prepend put in front and never sees the shim (my-mini 2026-09-17: every
# `bash -l` resolved the real binary). The file is CREATED when absent — a stock macOS
# account has no ~/.profile at all, and without one the bash/sh modes stay bypassed and
# the shim probe fails `health` with a remedy that changes nothing — exactly as the
# installer already creates ~/.zshenv and ~/.zprofile. It never creates ~/.bash_profile:
# that file's mere existence stops bash reading ~/.profile.
install_profile_block() {
  touch "$HOME/.profile" 2>/dev/null || {
    echo "  WARNING: cannot create $HOME/.profile — bash/sh login shells will not see the shim" >&2
    return 0
  }
  if append_block "$HOME/.profile"; then
    echo "  PATH block: also at the end of ~/.profile (bash/sh login shells)"
  else
    echo "  WARNING: ~/.profile was NOT updated (see above) — bash/sh login shells will not see the shim" >&2
  fi
}

install_mac_paths() {
  # .zshenv covers non-interactive zsh; the .zshrc block must be LAST so it wins
  # over ~/.local/bin re-prepends done earlier in .zshrc/.zprofile.
  # zsh reads: .zshenv always; .zprofile for login; .zshrc for interactive.
  # The block must end each file that later re-prepends ~/.local/bin, and every copy
  # also carries the prompt hook (path_guard_body) for the rc-interrupted case.
  if [ -z "$INSTANCE" ]; then
    touch "$HOME/.zshenv"
    append_block "$HOME/.zshenv"
    touch "$HOME/.zprofile"
    append_block "$HOME/.zprofile"
    touch "$HOME/.zshrc"
    append_block "$HOME/.zshrc"
    [ -f "$HOME/.bash_profile" ] && append_block "$HOME/.bash_profile"
    [ -f "$HOME/.bashrc" ] && append_block "$HOME/.bashrc"
    echo "  PATH block: end of ~/.zshenv, ~/.zprofile, ~/.zshrc (+ bash rc files if present), with a prompt hook that re-asserts it"
    install_profile_block
  fi
}

install_linux_paths() {
  if [ -z "$INSTANCE" ]; then
    touch "$HOME/.bashrc"
    append_block "$HOME/.bashrc"
    if [ -w /etc/profile.d ] 2>/dev/null || [ "$(id -u)" = "0" ]; then
      rc_block > "$PROFILED"
      echo "  PATH block: ~/.bashrc + $PROFILED (with a prompt hook that re-asserts it)"
    fi
    install_profile_block
  fi
  if [ -z "$INSTANCE" ] && [ "$(id -u)" = "0" ]; then
    if [ -e /usr/local/bin/claude ] && [ ! -L /usr/local/bin/claude ]; then
      echo "  WARNING: /usr/local/bin/claude exists and is a real file — NOT overwriting." >&2
    else
      ln -sfn "$REPO_DIR/bin/claude" /usr/local/bin/claude
      echo "  shim: /usr/local/bin/claude -> $REPO_DIR/bin/claude (systemd-PATH compatible)"
    fi
    if [ -e /usr/local/bin/codex ] && [ ! -L /usr/local/bin/codex ]; then
      echo "  WARNING: /usr/local/bin/codex exists and is a real file — NOT overwriting." >&2
    else
      ln -sfn "$REPO_DIR/bin/codex" /usr/local/bin/codex
      echo "  shim: /usr/local/bin/codex -> $REPO_DIR/bin/codex (systemd-PATH compatible)"
    fi
  fi
}

do_install() {
  local kind
  kind="$(machine_kind)"
  echo "claude-multiacc: installing (mode: $kind, repo: $REPO_DIR)"
  if [ -n "$INSTANCE" ]; then
    echo "  instance: $INSTANCE (agents labelled $LABEL.*, pools $ACC_ROOT + $CODEX_ACC_ROOT)"
  fi

  install_binaries
  install_pools

  # An INSTANCE install never rewrites the shell rc blocks: one interactive PATH
  # cannot serve two pools, and clobbering the default install's block would point
  # the operator's shell at an instance pool. Its agents (below) carry the roots
  # instead, and the runner daemon invokes the shims by absolute path.
  if [ -n "$INSTANCE" ]; then
    echo "  PATH block: skipped (instance install) — for a shell against this pool:"
    echo "    export CLAUDE_ACCOUNTS_ROOT='$ACC_ROOT' CODEX_ACCOUNTS_ROOT='$CODEX_ACC_ROOT'"
    echo "    export PATH=\"$REPO_DIR/bin:\$PATH\""
  fi
  if [ "$kind" = "mac" ]; then
    install_mac_paths
    [ "$NO_SCHEDULE" = "1" ] || mac_schedule_install
  else
    install_linux_paths
    [ "$NO_SCHEDULE" = "1" ] || linux_schedule_install
  fi

  echo
  echo "install complete. Open a new shell (or 'export PATH=\"$REPO_DIR/bin:\$PATH\"'), then:"
  echo "  claude-accounts list      # the Claude Code pool"
  echo "  codex-accounts list       # the Codex pool"
  echo "  claude-accounts status   # Claude auth + limits detail"
  echo "  codex-accounts status    # Codex auth + limits detail"
}
