#!/bin/bash
# Takumi Guard token provisioning
#
# Two invocation modes:
#
#   1. Legacy (all-in-one) mode -- back-compat with versions <= 0.4.0:
#
#        TG_BOT_API_KEY=... ./takumi-guard-setup-{VERSION}.sh <BOT_ID> <USER_IDENTIFIER> [SCOPES]
#
#      Mints (or reuses) a token and configures all package managers in one
#      invocation. The pre-0.5.0 contract is preserved bit-identically: same
#      stdout log lines, same exit codes, same backup/rollback behavior.
#
#   2. Primitive subcommand mode -- introduced in 0.5.0 for composing custom
#      deployment flows (e.g. one-token-per-device on multi-user machines):
#
#        setup.sh precheck [SCOPES]            -- exit 0 if anything is configurable
#        setup.sh discover                     -- print existing tg_org_* tokens (one per line)
#        setup.sh verify <TOKEN>               -- print active|revoked|unknown
#        setup.sh issue <USER_IDENTIFIER>      -- mint a new token; print it
#        setup.sh install <TOKEN> [SCOPES]     -- write package manager configs
#
#      In subcommand mode, machine-readable results are printed to stdout and
#      log messages are printed to stderr. The legacy mode keeps every line
#      on stdout because some MDM / remote-shell hosts capture only stdout.
#
# Supported package managers:
#   npm ecosystem      -- npm, pnpm, yarn v2+, bun
#   PyPI ecosystem     -- pip, uv, poetry
#   RubyGems ecosystem -- Bundler
#
# Arguments (legacy mode):
#   BOT_ID          Required. Bot ID from Shisho Cloud console.
#   USER_IDENTIFIER Required. Unique device/user identifier.
#                   Allowed characters: a-z, A-Z, 0-9, hyphen, underscore, dot, at sign, plus.
#                   Length: 4-255 characters.
#   SCOPES          Optional. Comma-separated ecosystems to configure
#                   (default: npm,pypi,rubygems).
#
# Environment:
#   TG_BOT_API_KEY  Required for legacy mode and for `verify` / `issue`.
#                   Bot API key from Shisho Cloud console. Passed via env to
#                   avoid shell history / process table exposure.
#   TG_BOT_ID       Required for `verify` / `issue` subcommands. Bot ID
#                   (positional in legacy mode).
#   TG_PREMINTED_TOKEN  Optional (legacy mode only). Pre-minted org token to
#                       use instead of calling the API. Validated against
#                       tg_org_ format before use. Subcommand callers should
#                       pass the token to `install` directly instead.
#   USER_HOME       Optional. Override the user home directory. When unset,
#                   $HOME is used. Required when the script is invoked
#                   outside the target user's session (e.g. an MDM agent that
#                   runs as root without `sudo -u`); without it config files
#                   would land under /var/root or /root and never reach the
#                   real user. Honored by every subcommand and by legacy mode.
#   TG_API_BASE_URL Optional. Override the API base URL (for staging /
#                   testing).
#
# Idempotency:
#   Legacy mode is safe to run multiple times. If a tg_org_* token is already
#   present in any config file, the script reuses it without minting a new
#   token. Config files for the specified scopes are still updated if needed.
#
# Backup and rollback:
#   Before modifying an existing config file, a timestamped backup is created
#   next to the original (e.g. ~/.npmrc-backup-20260408-162351). These
#   backups are preserved even if the script succeeds, so you can manually
#   restore the previous state at any time by copying the backup file back:
#
#     cp ~/.npmrc-backup-20260408-162351 ~/.npmrc
#
#   If the script fails midway through (e.g. a network error during token
#   mint, or a file write error), all changes made so far are automatically
#   rolled back to the pre-execution state. No manual intervention is needed.
#
# Config file handling:
#   - If a config file already exists, it is updated regardless of whether
#     the corresponding tool is installed. This is intentional: package
#     managers do not remove their config files on uninstall, and pre-placing
#     config ensures Guard is active as soon as the tool is (re)installed.
#   - If a config file does not exist, it is created only if the tool is
#     installed.
#   - Non-Guard settings in existing config files (e.g. ignore-scripts,
#     min-release-age) are preserved.
#
# Prerequisites:
#   curl must be installed on the target machine (legacy mode and
#   verify/issue subcommands only).

set -euE

# ---------------------------------------------------------------------------
# Dispatch + argument parsing
# ---------------------------------------------------------------------------

usage() {
  cat <<USAGE >&2
Usage:
  Legacy (one-shot):
    TG_BOT_API_KEY=... $0 <BOT_ID> <USER_IDENTIFIER> [SCOPES]

  Primitive subcommands:
    $0 precheck [SCOPES]
    $0 discover
    $0 verify <TOKEN>                       (env: TG_BOT_ID, TG_BOT_API_KEY)
    $0 issue <USER_IDENTIFIER>              (env: TG_BOT_ID, TG_BOT_API_KEY)
    $0 install <TOKEN> [SCOPES]
USAGE
}

# BOT_ID from the Shisho Cloud console always begins with `BT`, so it will
# never collide with a subcommand keyword (precheck/discover/verify/issue/
# install). First-arg dispatch is therefore unambiguous and safe to do
# without an explicit `--` separator.
SUBCOMMAND=""
SUB_TOKEN=""
case "${1:-}" in
  precheck|discover|verify|issue|install)
    SUBCOMMAND="$1"
    shift
    BOT_ID="${TG_BOT_ID:-}"
    USER_IDENTIFIER=""
    SCOPES=""
    ;;
  -h|--help|help)
    usage
    exit 0
    ;;
  "")
    usage
    exit 1
    ;;
  *)
    # Legacy mode (pre-0.5.0 contract).
    BOT_ID="$1"
    USER_IDENTIFIER="${2:?Usage: TG_BOT_API_KEY=... setup.sh BOT_ID USER_IDENTIFIER [SCOPES]}"
    SCOPES="${3:-npm,pypi,rubygems}"
    ;;
esac

# Stream routing:
#   Subcommand mode: redirect fd 1 (stdout) onto fd 2 (stderr) for the
#     remainder of the script, and dup the *original* stdout onto fd 3.
#     Every existing `echo "[...]"` log line then automatically lands on
#     stderr without per-call rewriting; primitives emit their structured
#     result with `>&3`.
#   Legacy mode: keep stdout untouched and alias fd 3 to it, so primitives
#     can use `>&3` uniformly without legacy-vs-subcommand branching.
if [ -n "$SUBCOMMAND" ]; then
  exec 3>&1 1>&2
else
  exec 3>&1
fi

# Per-subcommand positional-arg binding.
case "$SUBCOMMAND" in
  precheck)
    SCOPES="${1:-npm,pypi,rubygems}"
    ;;
  discover)
    : # no positional args
    ;;
  verify)
    SUB_TOKEN="${1:?Usage: $0 verify <TOKEN>}"
    ;;
  issue)
    USER_IDENTIFIER="${1:?Usage: $0 issue <USER_IDENTIFIER>}"
    ;;
  install)
    SUB_TOKEN="${1:?Usage: $0 install <TOKEN> [SCOPES]}"
    SCOPES="${2:-npm,pypi,rubygems}"
    ;;
esac

API_KEY="${TG_BOT_API_KEY:-}"

# Curl prerequisite -- only modes that talk to the API need it.
if [ -z "$SUBCOMMAND" ] || [ "$SUBCOMMAND" = verify ] || [ "$SUBCOMMAND" = issue ]; then
  if ! command -v curl >/dev/null 2>&1; then
    echo "[Error] Required command not found: curl" >&2
    exit 1
  fi
fi

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

# Locate a command via PATH or well-known install directories.
# MDM tools may execute this script in a non-interactive shell where ~/.zshrc
# or ~/.bashrc is not loaded, so command -v alone may miss installed tools.
# Prints the containing directory on stdout when found; returns 1 otherwise.
find_command_dir() {
  local cmd="$1"
  local resolved
  # `|| true` keeps a missing tool (non-zero exit) from aborting the script.
  resolved=$(command -v "$cmd" 2>/dev/null || true)
  # `command -v` returns bare names for shell builtins/aliases; only accept
  # absolute paths so the well-known-dir fallback below can run.
  case "$resolved" in
    /*)
      dirname "$resolved"
      return 0
      ;;
  esac
  for dir in \
    "/usr/local/bin" \
    "/opt/homebrew/bin" \
    "/opt/homebrew/opt/ruby/bin" \
    "/usr/local/opt/ruby/bin" \
    "$USER_HOME/.local/bin" \
    "$USER_HOME/.cargo/bin" \
    "${PNPM_HOME:-$USER_HOME/Library/pnpm}" \
    "$USER_HOME/.local/share/pnpm" \
    "$USER_HOME/.bun/bin" \
    "$USER_HOME/.yarn/bin" \
    "$USER_HOME/.rbenv/shims" \
    ${RBENV_ROOT:+"$RBENV_ROOT/shims"} \
    "$USER_HOME/.asdf/shims" \
    "${XDG_DATA_HOME:-$USER_HOME/.local/share}/mise/shims" \
    "$USER_HOME/.rvm/bin"; do
    [ -x "$dir/$cmd" ] && { printf '%s\n' "$dir"; return 0; }
  done
  # chruby-style installers (PATH-switching only, no shims): glob version dirs.
  # Literal paths are returned when no match exists, so the -x check stays safe.
  for dir in "$USER_HOME/.rubies"/*/bin "/opt/rubies"/*/bin; do
    [ -x "$dir/$cmd" ] && { printf '%s\n' "$dir"; return 0; }
  done
  return 1
}

# Boolean wrapper around find_command_dir for callers that only need hit/miss.
find_command() { find_command_dir "$1" >/dev/null; }

# Resolve the Bundler user config file path (BUNDLE_USER_CONFIG > BUNDLE_USER_HOME/config > $USER_HOME/.bundle/config).
# Bundler does NOT follow XDG_CONFIG_HOME. Treat empty env vars the same as unset.
bundle_config_path() {
  echo "${BUNDLE_USER_CONFIG:-${BUNDLE_USER_HOME:-$USER_HOME/.bundle}/config}"
}

# Portable in-place file edit (works on both BSD sed and GNU sed).
# Uses cat to preserve original file's inode, permissions, and ownership.
sed_inplace() {
  local pattern="$1"
  local file="$2"
  local tmp
  tmp=$(mktemp)
  sed "$pattern" "$file" > "$tmp" && cat "$tmp" > "$file" && rm "$tmp"
}

# Escape a string for safe use as a literal in a sed BRE pattern (LHS).
# Required when the input may contain regex metacharacters such as `.`, `*`,
# `[`, `^`, `$`, `\` or the chosen delimiter `/`. Without this, a future change
# in a config key (e.g. backend renames it to contain `.`) would silently
# mismatch and append a duplicate entry instead of replacing the existing one.
escape_for_sed_basic() {
  printf '%s' "$1" | sed 's/[][\\.*^$/]/\\&/g'
}

# Restrict file permissions to owner-only (0600). Token-bearing config files
# must not be world- or group-readable. Failures are downgraded to a warning
# (not fatal) so the script can still complete on shared/network filesystems
# where chmod is rejected, mirroring the PowerShell side's behavior.
restrict_permissions() {
  local path="$1"
  [ -f "$path" ] || return 0
  if ! chmod 600 "$path" 2>/dev/null; then
    echo "[WARN] Failed to restrict permissions on $path" >&2
  fi
}

# Append a trailing newline to a non-empty file whose last byte is not '\n'.
# Required before appending a new key to YAML-like configs whose last line may
# not be newline-terminated (e.g. user-handwritten files), so the new key does
# not concatenate with the previous one.
ensure_trailing_newline() {
  local file="$1"
  if [ -s "$file" ] && [ -n "$(tail -c1 "$file")" ]; then
    printf '\n' >> "$file"
  fi
}

has_scope() {
  echo ",$SCOPES," | grep -q ",$1,"
}

validate_safe_string() {
  local label="$1"
  local value="$2"
  if ! printf '%s\n' "$value" | grep -qE '^[0-9a-zA-Z._@+\-]+$'; then
    echo "[Error] $label contains invalid characters" >&2
    return 1
  fi
}

validate_user_identifier() {
  local id="$1"
  local len
  len=$(printf '%s' "$id" | wc -c | tr -d ' ')
  if [ "$len" -lt 4 ] || [ "$len" -gt 255 ]; then
    echo "[Error] USER_IDENTIFIER must be 4-255 characters (got $len)" >&2
    return 1
  fi
  validate_safe_string "USER_IDENTIFIER" "$id"
}

# ---------------------------------------------------------------------------
# USER_HOME resolution
# ---------------------------------------------------------------------------

# Resolve target user home directory. When this script runs from an MDM
# agent without `sudo -u <user>`, $HOME points at /var/root or /root and
# writing to $USER_HOME/.bundle/config etc. silently misses the real user.
# Wrappers can set USER_HOME to redirect every $HOME-derived path used
# downstream.
#
# `${USER_HOME+1}` records whether the wrapper provided USER_HOME at all; we
# use this below to fail-fast on bare `sudo ./setup.sh` (root + default
# $HOME) while still allowing wrappers that intentionally set USER_HOME to
# root paths.
_user_home_explicit="${USER_HOME+1}"
USER_HOME="${USER_HOME:-$HOME}"
if [ -z "$USER_HOME" ]; then
  echo "[Error] Could not determine user home directory. Set USER_HOME explicitly." >&2
  exit 1
fi

# Symmetric to setup.ps1's LocalSystem fail-fast. If we are root and the
# wrapper did not explicitly set USER_HOME, refuse rather than write the
# real user's config files under root's home directory.
#
# Read-only API subcommands (verify, issue) do not touch any per-user
# files, so this fail-fast does not apply to them -- their callers (the
# per-device wrapper in particular) invoke them as root without
# USER_HOME, and that is intentional.
case "$SUBCOMMAND" in
  verify|issue) ;;
  *)
    if [ "$(id -u)" -eq 0 ] && [ -z "$_user_home_explicit" ]; then
      case "$USER_HOME" in
        /var/root|/root)
          echo "[Error] Running as root with default \$HOME=$USER_HOME without USER_HOME. Set USER_HOME to the target user's home directory (e.g. USER_HOME=/Users/alice ./setup.sh ...) or invoke via 'sudo -u <user>'." >&2
          exit 1
          ;;
      esac
    fi
    ;;
esac
unset _user_home_explicit

# Resolve once: BUNDLE_USER_CONFIG / BUNDLE_USER_HOME / USER_HOME are all
# static for the duration of this script, so caching avoids repeated subshell
# forks at every check-site downstream.
BUNDLE_CONFIG_PATH=$(bundle_config_path)

# ---------------------------------------------------------------------------
# API client (verify / issue)
# ---------------------------------------------------------------------------

# Base URL for the Takumi Guard org-user token endpoint family. Both the
# mint path (POST $API_BASE_URL) and the validation path (POST
# $API_BASE_URL/status) derive from this value. Keep the two consumers in
# sync -- the status probe runs before mint, so a typo or env override that
# changes one MUST change the other.
API_BASE_URL="${TG_API_BASE_URL:-https://apiv2.cloud.shisho.dev/v1/guard/tokens/org-user}"

# Probe the API to classify the supplied tg_org_* token as one of:
#   active   -- server confirmed the token is currently active.
#   revoked  -- server confirmed the token is not active (revoked, never
#               issued, or owned by a different organisation -- the server
#               collapses these three cases to a single 404 so the script
#               treats them uniformly).
#   unknown  -- anything else: network failure, timeout, 5xx, 401, malformed
#               body on a 200 response, locally malformed token.
# Output: one of the three strings above on stdout. Always returns 0; read
# the stdout, not the exit code.
get_token_status() {
  local token="$1"
  local response_file http_status body_oneline

  # Reject locally-malformed tokens before they reach the JSON body.
  if ! printf '%s' "$token" | grep -qE '^tg_org_[A-Za-z0-9_-]+$'; then
    printf 'unknown'
    return 0
  fi

  response_file=$(mktemp 2>/dev/null || true)
  if [ -z "$response_file" ]; then
    printf 'unknown'
    return 0
  fi

  # Pass the request body via stdin so $API_KEY does not appear in process
  # listings. `|| true` keeps curl-level failures (timeout, DNS, refused) in
  # the "unknown" classification rather than aborting.
  http_status=$(curl -s --max-time 5 -o "$response_file" -w "%{http_code}" \
    -X POST "${API_BASE_URL}/status" \
    -H "Content-Type: application/json" \
    --data-binary @- <<EOF || true
{"bot_id":"$BOT_ID","api_key":"$API_KEY","token":"$token"}
EOF
)

  # Collapse the body to a single line so the regex anchors tolerate any
  # whitespace or newlines the server may include.
  body_oneline=$(tr -d '\n' < "$response_file" 2>/dev/null || true)
  rm -f "$response_file"

  case "$http_status" in
    200)
      # A 200 with the documented status payload signals "active". Any 200
      # carrying an unexpected body -- empty object, wrong keys, etc. -- is
      # treated as "unknown" so a server bug or upstream proxy rewrite
      # cannot accidentally classify a dead token as active.
      if printf '%s' "$body_oneline" | grep -qE '"created_at"[[:space:]]*:[[:space:]]*"[^"]+"'; then
        printf 'active'
      else
        printf 'unknown'
      fi
      return 0
      ;;
    404)
      # A 404 is "revoked" ONLY when the body matches BOTH:
      #   1. the documented JSON error shape from /status -- i.e. an
      #      `"error":"<message>"` field, AND
      #   2. the literal substring `token not active`, which is the exact
      #      message HandleStatus writes server-side (see the
      #      "CLIENT CONTRACT" comment in
      #      backend/services/api/internal/orgusertoken/handler.go).
      # A bare 404 with a plain-text / HTML body -- e.g. "404 page not
      # found" from a CDN or from a deployment window where /status is not
      # yet served -- or a JSON 404 carrying some other error message from
      # a different endpoint, must be classified as "unknown" instead.
      # Classifying a generic 404 as "revoked" would mark every discovered
      # token as dead and fall through to mint, BURNING A FRESH BILLED
      # TOKEN ON EVERY SETUP RUN until /status comes back.
      if printf '%s' "$body_oneline" | grep -qE '"error"[[:space:]]*:[[:space:]]*"[^"]+"' \
        && printf '%s' "$body_oneline" | grep -qF 'token not active'; then
        printf 'revoked'
      else
        printf 'unknown'
      fi
      return 0
      ;;
    *)
      # Network failure (curl returned 0), 401, 403, 5xx, or any other
      # status: classify as unknown. Unknown means "do NOT re-mint" -- the
      # validation endpoint is fail-closed.
      printf 'unknown'
      return 0
      ;;
  esac
}

# Mint a new org-user token via the public API. Prints the resulting token
# to stdout on success; emits `[Error]` lines on stderr and returns 1 on
# failure. Does NOT emit the legacy "[OK] Token minted" line -- the caller
# is responsible for that so legacy mode and the `issue` subcommand can
# differ in surface logs.
mint_token() {
  local user_id="$1"
  local response body status token

  # Pass the request body via stdin so $API_KEY is not visible in
  # `ps`/`/proc/*/cmdline`. All three values are restricted by
  # validate_safe_string to ^[0-9a-zA-Z._@+\-]+$, so they need no JSON
  # escaping inside the heredoc.
  response=$(curl -s -w "\n%{http_code}" -X POST "$API_BASE_URL" \
    -H "Content-Type: application/json" \
    --data-binary @- <<EOF
{"bot_id":"$BOT_ID","api_key":"$API_KEY","user_identifier":"$user_id"}
EOF
)

  body=$(echo "$response" | sed '$d')
  status=$(echo "$response" | tail -1)

  if ! [ "$status" -eq 201 ] 2>/dev/null; then
    echo "[Error] Token API returned HTTP $status" >&2
    echo "$body" >&2
    return 1
  fi

  token=$(printf '%s' "$body" | grep -oE '"token"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"token"[[:space:]]*:[[:space:]]*"//;s/"$//')

  if [ -z "$token" ]; then
    echo "[Error] Failed to extract token from API response" >&2
    return 1
  fi

  # Validate token format (tg_org_ prefix + 20+ Base64URL characters).
  if ! printf '%s' "$token" | grep -qE '^tg_org_[A-Za-z0-9_-]{20,}$'; then
    echo "[Error] Token format unexpected" >&2
    return 1
  fi

  printf '%s\n' "$token"
}

# ---------------------------------------------------------------------------
# Discovery (find existing tokens on disk)
# ---------------------------------------------------------------------------

# Walk every Guard-managed config path under USER_HOME and emit each unique
# tg_org_* value found, one per line. The deduplication preserves discovery
# order (awk-with-seen[]; `sort -u` would lose it) so the caller's
# "first hit wins" logic mirrors the per-file iteration above.
collect_existing_tokens() {
  local tokens_found=""
  local check_file found_in_file
  for check_file in \
    "$USER_HOME/.npmrc" \
    "$USER_HOME/Library/Preferences/pnpm/rc" \
    "${XDG_CONFIG_HOME:-$USER_HOME/.config}/pnpm/rc" \
    "$USER_HOME/.yarnrc.yml" \
    "$USER_HOME/.bunfig.toml" \
    "$USER_HOME/.config/pip/pip.conf" \
    "$USER_HOME/.config/uv/uv.toml" \
    "$USER_HOME/.config/pypoetry/auth.toml" \
    "$USER_HOME/Library/Application Support/pypoetry/auth.toml" \
    "$BUNDLE_CONFIG_PATH"; do
    if [ -f "$check_file" ]; then
      found_in_file=$(grep -o 'tg_org_[A-Za-z0-9_-]*' "$check_file" 2>/dev/null | head -1 || true)
      if [ -n "$found_in_file" ]; then
        tokens_found="${tokens_found}${found_in_file}
"
      fi
    fi
  done
  printf '%s' "$tokens_found" | awk 'NF && !seen[$0]++' || true
}

# ---------------------------------------------------------------------------
# Pre-check (does this user have anything configurable?)
# ---------------------------------------------------------------------------

# Returns 0 (true) when at least one tool or pre-existing config file exists
# for any of the requested scopes, 1 otherwise. SCOPES is read from the
# enclosing variable so the legacy and `precheck` callers share the result.
has_target() {
  local found=false
  if has_scope npm; then
    [ -f "$USER_HOME/.npmrc" ] || command -v npm >/dev/null 2>&1 && found=true
    find_command pnpm && found=true
    [ -f "$USER_HOME/.yarnrc.yml" ] && found=true
    find_command yarn && found=true
    [ -f "$USER_HOME/.bunfig.toml" ] && found=true
    find_command bun && found=true
  fi
  if has_scope pypi; then
    [ -f "$USER_HOME/.config/pip/pip.conf" ] && found=true
    command -v pip3 >/dev/null 2>&1 && found=true
    command -v pip >/dev/null 2>&1 && found=true
    [ -f "$USER_HOME/.config/uv/uv.toml" ] && found=true
    find_command uv && found=true
    find_command poetry && found=true
  fi
  if has_scope rubygems; then
    [ -f "$BUNDLE_CONFIG_PATH" ] && found=true
    find_command bundle && found=true
    find_command ruby && found=true
  fi
  [ "$found" = true ]
}

# ---------------------------------------------------------------------------
# Backup and rollback
# ---------------------------------------------------------------------------

TIMESTAMP=$(date +%Y%m%d-%H%M%S)
TMPBACKUP_DIR=$(mktemp -d)
# Bash arrays so paths containing spaces (e.g. "Library/Application
# Support/...") survive iteration during rollback. macOS ships bash 3.2 which
# supports both `arr+=("x")` and `"${arr[@]}"`.
TMPBACKUP_FILES=()
CREATED_FILES=()

# Creates a persistent timestamped backup for manual rollback,
# and a temporary backup for auto-rollback on failure.
backup_file() {
  local src="$1"
  if [ -f "$src" ]; then
    local dir name backup_path
    dir=$(dirname "$src")
    name=$(basename "$src")
    backup_path="$dir/$name-backup-$TIMESTAMP"
    cp "$src" "$backup_path"
    # cp preserves the source's mode, so a 0644 source bearing a tg_org_*
    # token would leave a world-readable backup. Lock token-bearing backups
    # down to 0600 to match the live config files restrict_permissions writes.
    restrict_permissions "$backup_path"
    echo "[Backup] Created $backup_path"
    # Use array index as the temp filename to avoid basename collisions
    # across different directories (e.g. multiple `config` files).
    cp "$src" "$TMPBACKUP_DIR/${#TMPBACKUP_FILES[@]}"
    TMPBACKUP_FILES+=("$src")
  fi
}

# Track newly created files so rollback can remove them.
track_created_file() {
  local path="$1"
  CREATED_FILES+=("$path")
}

rollback() {
  trap '' ERR
  # No-op if nothing has been tracked yet, so callers can invoke rollback
  # unconditionally without a misleading log line.
  if [ "${#TMPBACKUP_FILES[@]}" -eq 0 ] && [ "${#CREATED_FILES[@]}" -eq 0 ]; then
    rm -rf "$TMPBACKUP_DIR" 2>/dev/null || true
    return 0
  fi
  echo "[Error] setup.sh failed. Rolling back changes..." >&2
  if [ "${#TMPBACKUP_FILES[@]}" -gt 0 ]; then
    local i=0
    for src in "${TMPBACKUP_FILES[@]}"; do
      if [ -f "$TMPBACKUP_DIR/$i" ]; then
        cp "$TMPBACKUP_DIR/$i" "$src" || true
        echo "[Rollback] Restored $src" >&2
      fi
      i=$((i + 1))
    done
  fi
  if [ "${#CREATED_FILES[@]}" -gt 0 ]; then
    for src in "${CREATED_FILES[@]}"; do
      if [ -f "$src" ]; then
        rm -f "$src" || true
        echo "[Rollback] Removed $src" >&2
      fi
    done
  fi
  rm -rf "$TMPBACKUP_DIR" 2>/dev/null || true
}

cleanup() {
  trap '' ERR
  rm -rf "$TMPBACKUP_DIR" 2>/dev/null || true
}

# Install signal/error traps. Called from legacy mode and from the `install`
# subcommand -- the only two paths that mutate config files.
arm_traps() {
  # Restore on signal-driven termination as well as on ERR. Without these
  # traps a partial config file (e.g. mirror key written but not the
  # credential key) would survive an external SIGTERM and the next run
  # would misdetect it as already-configured.
  trap 'rollback; exit 130' INT
  trap 'rollback; exit 143' TERM
  trap rollback ERR
  trap cleanup EXIT
}

# ---------------------------------------------------------------------------
# Install (write config files for one resolved token)
# ---------------------------------------------------------------------------

install_configs() {
  local TOKEN="$1"

  # ---- npm ecosystem ----

  if has_scope npm; then

    # --- npm / pnpm (.npmrc) ---

    if [ -f "$USER_HOME/.npmrc" ] || command -v npm >/dev/null 2>&1 || find_command pnpm; then
      if grep -qF "$TOKEN" "$USER_HOME/.npmrc" 2>/dev/null; then
        echo "[OK] npm already configured"
      else
        if [ ! -f "$USER_HOME/.npmrc" ]; then
          track_created_file "$USER_HOME/.npmrc"
        fi
        backup_file "$USER_HOME/.npmrc"

        if command -v npm >/dev/null 2>&1; then
          local EXISTING_REGISTRY
          EXISTING_REGISTRY=$(npm config get registry --location=user 2>/dev/null || echo "")
          if [ -n "$EXISTING_REGISTRY" ] \
             && [ "$EXISTING_REGISTRY" != "https://npm.flatt.tech/" ] \
             && [ "$EXISTING_REGISTRY" != "https://registry.npmjs.org/" ] \
             && [ "$EXISTING_REGISTRY" != "undefined" ]; then
            echo "[WARN] Existing npm registry will be overwritten: $EXISTING_REGISTRY"
          fi

          npm config set "//npm.flatt.tech/:_authToken" "$TOKEN" --location=user
          npm config set registry "https://npm.flatt.tech/" --location=user
        else
          # npm not installed but .npmrc exists -- edit file directly
          if grep -qF "npm.flatt.tech" "$USER_HOME/.npmrc" 2>/dev/null; then
            sed_inplace 's|//npm.flatt.tech/:_authToken=.*|//npm.flatt.tech/:_authToken='"$TOKEN"'|' "$USER_HOME/.npmrc"
          else
            ensure_trailing_newline "$USER_HOME/.npmrc"
            printf '//npm.flatt.tech/:_authToken=%s\nregistry=https://npm.flatt.tech/\n' "$TOKEN" >> "$USER_HOME/.npmrc"
          fi
        fi
        restrict_permissions "$USER_HOME/.npmrc"
        echo "[OK] npm configured"
      fi # end tg_org_ check
    else
      echo "[SKIP] npm not available"
    fi

    # --- pnpm (global rc) ---

    local pnpm_rc_dir pnpm_rc_path
    if [ -n "${XDG_CONFIG_HOME:-}" ]; then
      pnpm_rc_dir="$XDG_CONFIG_HOME/pnpm"
    elif [ "$(uname)" = "Darwin" ]; then
      pnpm_rc_dir="$USER_HOME/Library/Preferences/pnpm"
    else
      pnpm_rc_dir="$USER_HOME/.config/pnpm"
    fi
    pnpm_rc_path="$pnpm_rc_dir/rc"

    if [ -f "$pnpm_rc_path" ]; then
      if grep -qF "$TOKEN" "$pnpm_rc_path" 2>/dev/null; then
        echo "[OK] pnpm already configured"
      else
        backup_file "$pnpm_rc_path"
        if grep -qF "npm.flatt.tech" "$pnpm_rc_path" 2>/dev/null; then
          sed_inplace 's|//npm.flatt.tech/:_authToken=.*|//npm.flatt.tech/:_authToken='"$TOKEN"'|' "$pnpm_rc_path"
        else
          ensure_trailing_newline "$pnpm_rc_path"
          printf '//npm.flatt.tech/:_authToken=%s\nregistry=https://npm.flatt.tech/\n' "$TOKEN" >> "$pnpm_rc_path"
        fi
        restrict_permissions "$pnpm_rc_path"
        echo "[OK] pnpm configured"
      fi
    elif find_command pnpm; then
      [ ! -d "$pnpm_rc_dir" ] && mkdir -p "$pnpm_rc_dir"
      printf '//npm.flatt.tech/:_authToken=%s\nregistry=https://npm.flatt.tech/\n' "$TOKEN" > "$pnpm_rc_path"
      track_created_file "$pnpm_rc_path"
      restrict_permissions "$pnpm_rc_path"
      echo "[OK] pnpm configured"
    else
      echo "[SKIP] pnpm not available"
    fi

    # --- yarn v2+ (.yarnrc.yml) ---

    if [ -f "$USER_HOME/.yarnrc.yml" ]; then
      if grep -qF "$TOKEN" "$USER_HOME/.yarnrc.yml" 2>/dev/null; then
        echo "[OK] yarn already configured"
      else
        backup_file "$USER_HOME/.yarnrc.yml"

        if grep -qF "npm.flatt.tech" "$USER_HOME/.yarnrc.yml" 2>/dev/null; then
          if grep -qF "npmAuthToken" "$USER_HOME/.yarnrc.yml" 2>/dev/null; then
            sed_inplace 's|npmAuthToken:.*|npmAuthToken: "'"$TOKEN"'"|' "$USER_HOME/.yarnrc.yml"
          else
            sed_inplace '/npmRegistryServer:/a\
npmAuthToken: "'"$TOKEN"'"' "$USER_HOME/.yarnrc.yml"
          fi
        else
          if grep -qF "npmRegistryServer" "$USER_HOME/.yarnrc.yml" 2>/dev/null; then
            sed_inplace 's|npmRegistryServer:.*|npmRegistryServer: "https://npm.flatt.tech/"|' "$USER_HOME/.yarnrc.yml"
            sed_inplace '/npmRegistryServer:/a\
npmAuthToken: "'"$TOKEN"'"' "$USER_HOME/.yarnrc.yml"
          else
            printf '\nnpmRegistryServer: "https://npm.flatt.tech/"\n' >> "$USER_HOME/.yarnrc.yml"
            printf 'npmAuthToken: "%s"\n' "$TOKEN" >> "$USER_HOME/.yarnrc.yml"
          fi
        fi
        restrict_permissions "$USER_HOME/.yarnrc.yml"
        echo "[OK] yarn configured"
      fi # end tg_org_ check
    elif find_command yarn; then
      printf 'npmRegistryServer: "https://npm.flatt.tech/"\n' > "$USER_HOME/.yarnrc.yml"
      printf 'npmAuthToken: "%s"\n' "$TOKEN" >> "$USER_HOME/.yarnrc.yml"
      track_created_file "$USER_HOME/.yarnrc.yml"
      restrict_permissions "$USER_HOME/.yarnrc.yml"
      echo "[OK] yarn configured"
    else
      echo "[SKIP] yarn not available"
    fi

    # --- bun (.bunfig.toml) ---

    if [ -f "$USER_HOME/.bunfig.toml" ]; then
      if grep -qF "$TOKEN" "$USER_HOME/.bunfig.toml" 2>/dev/null; then
        echo "[OK] bun already configured"
      else
        backup_file "$USER_HOME/.bunfig.toml"

        if grep -qF "npm.flatt.tech" "$USER_HOME/.bunfig.toml" 2>/dev/null; then
          sed_inplace '/npm\.flatt\.tech/s|token = "[^"]*"|token = "'"$TOKEN"'"|' "$USER_HOME/.bunfig.toml"
        else
          if grep -q '^\[install\]' "$USER_HOME/.bunfig.toml" 2>/dev/null; then
            sed_inplace '/^\[install\]/a\
registry = { url = "https://npm.flatt.tech/", token = "'"$TOKEN"'" }' "$USER_HOME/.bunfig.toml"
          else
            printf '\n[install]\n' >> "$USER_HOME/.bunfig.toml"
            printf 'registry = { url = "https://npm.flatt.tech/", token = "%s" }\n' "$TOKEN" >> "$USER_HOME/.bunfig.toml"
          fi
        fi
        restrict_permissions "$USER_HOME/.bunfig.toml"
        echo "[OK] bun configured"
      fi # end tg_org_ check
    elif find_command bun; then
      printf '[install]\nregistry = { url = "https://npm.flatt.tech/", token = "%s" }\n' "$TOKEN" > "$USER_HOME/.bunfig.toml"
      track_created_file "$USER_HOME/.bunfig.toml"
      restrict_permissions "$USER_HOME/.bunfig.toml"
      echo "[OK] bun configured"
    else
      echo "[SKIP] bun not available"
    fi

  fi # has_scope npm

  # ---- PyPI ecosystem ----

  if has_scope pypi; then

    # --- pip (~/.config/pip/pip.conf) ---

    local pip_conf_dir pip_conf_path
    pip_conf_dir="$USER_HOME/.config/pip"
    pip_conf_path="$pip_conf_dir/pip.conf"

    if [ -f "$pip_conf_path" ]; then
      if grep -qF "$TOKEN" "$pip_conf_path" 2>/dev/null; then
        echo "[OK] pip already configured"
      else
        backup_file "$pip_conf_path"

        if grep -qF "pypi.flatt.tech" "$pip_conf_path" 2>/dev/null; then
          sed_inplace 's|index-url = .*pypi\.flatt\.tech.*|index-url = https://token:'"$TOKEN"'@pypi.flatt.tech/simple/|' "$pip_conf_path"
        else
          if grep -q '^\[global\]' "$pip_conf_path" 2>/dev/null; then
            sed_inplace '/^\[global\]/a\
index-url = https://token:'"$TOKEN"'@pypi.flatt.tech/simple/' "$pip_conf_path"
          else
            printf '\n[global]\nindex-url = https://token:%s@pypi.flatt.tech/simple/\n' "$TOKEN" >> "$pip_conf_path"
          fi
        fi
        restrict_permissions "$pip_conf_path"
        echo "[OK] pip configured"
      fi # end tg_org_ check
    elif command -v pip3 >/dev/null 2>&1 || command -v pip >/dev/null 2>&1; then
      [ ! -d "$pip_conf_dir" ] && mkdir -p "$pip_conf_dir"
      printf '[global]\nindex-url = https://token:%s@pypi.flatt.tech/simple/\n' "$TOKEN" > "$pip_conf_path"
      track_created_file "$pip_conf_path"
      restrict_permissions "$pip_conf_path"
      echo "[OK] pip configured"
    else
      echo "[SKIP] pip not available"
    fi

    # --- uv (~/.config/uv/uv.toml) ---

    local uv_toml_dir uv_toml_path
    uv_toml_dir="$USER_HOME/.config/uv"
    uv_toml_path="$uv_toml_dir/uv.toml"

    if [ -f "$uv_toml_path" ]; then
      if grep -qF "$TOKEN" "$uv_toml_path" 2>/dev/null; then
        echo "[OK] uv already configured"
      else
        backup_file "$uv_toml_path"

        if grep -qF "pypi.flatt.tech" "$uv_toml_path" 2>/dev/null; then
          sed_inplace 's|url = ".*pypi\.flatt\.tech.*"|url = "https://token:'"$TOKEN"'@pypi.flatt.tech/simple/"|' "$uv_toml_path"
        else
          if grep -q 'default = true' "$uv_toml_path" 2>/dev/null; then
            sed_inplace 's|default = true|default = false|' "$uv_toml_path"
          fi
          printf '\n[[index]]\nurl = "https://token:%s@pypi.flatt.tech/simple/"\ndefault = true\n' "$TOKEN" >> "$uv_toml_path"
        fi
        restrict_permissions "$uv_toml_path"
        echo "[OK] uv configured"
      fi # end tg_org_ check
    elif find_command uv; then
      [ ! -d "$uv_toml_dir" ] && mkdir -p "$uv_toml_dir"
      printf '[[index]]\nurl = "https://token:%s@pypi.flatt.tech/simple/"\ndefault = true\n' "$TOKEN" > "$uv_toml_path"
      track_created_file "$uv_toml_path"
      restrict_permissions "$uv_toml_path"
      echo "[OK] uv configured"
    else
      echo "[SKIP] uv not available"
    fi

    # --- poetry ---

    # `find_command_dir` returns 1 when poetry is not installed. `|| true`
    # keeps a non-zero from the subshell from triggering rollback.
    local poetry_dir poetry_config_dir poetry_auth poetry_conf
    poetry_dir=$(find_command_dir poetry || true)
    if [ -n "$poetry_dir" ]; then
      # Poetry config path: macOS = ~/Library/Application Support/pypoetry/
      #                     Linux = ${XDG_CONFIG_HOME:-~/.config}/pypoetry/
      if [ "$(uname)" = "Darwin" ]; then
        poetry_config_dir="$USER_HOME/Library/Application Support/pypoetry"
      else
        poetry_config_dir="${XDG_CONFIG_HOME:-$USER_HOME/.config}/pypoetry"
      fi
      poetry_auth="$poetry_config_dir/auth.toml"
      poetry_conf="$poetry_config_dir/config.toml"
      if [ -f "$poetry_auth" ] && grep -qF "$TOKEN" "$poetry_auth" 2>/dev/null; then
        echo "[OK] poetry already configured"
      else
        # `poetry config` writes the repository URL to config.toml and
        # credentials to auth.toml, so both need rollback tracking even if
        # only one currently exists.
        if [ -f "$poetry_conf" ]; then backup_file "$poetry_conf"; else track_created_file "$poetry_conf"; fi
        if [ -f "$poetry_auth" ]; then backup_file "$poetry_auth"; else track_created_file "$poetry_auth"; fi
        # Prepend the discovered directory to PATH so the subsequent
        # `poetry` invocation resolves even when the shell PATH is minimal
        # (e.g. MDM non-interactive shells where /opt/homebrew/bin is not
        # on PATH). Disable keyring to prevent interactive prompts and
        # errors in non-GUI environments.
        PATH="$poetry_dir:$PATH" PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring poetry config repositories.takumi-guard https://pypi.flatt.tech/simple/ 2>/dev/null
        PATH="$poetry_dir:$PATH" PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring poetry config http-basic.takumi-guard token "$TOKEN" 2>/dev/null
        # auth.toml holds the credential; config.toml only the repository
        # URL. Lock both down for defense-in-depth.
        restrict_permissions "$poetry_auth"
        restrict_permissions "$poetry_conf"
        echo "[OK] poetry configured"
      fi
    else
      echo "[SKIP] poetry not available"
    fi

  fi # has_scope pypi

  # ---- RubyGems ecosystem ----

  if has_scope rubygems; then

    # --- Bundler (~/.bundle/config) ---
    # Config file path resolution:
    #   BUNDLE_USER_CONFIG > BUNDLE_USER_HOME/config > $USER_HOME/.bundle/config
    # Bundler does NOT follow XDG_CONFIG_HOME.
    # Key format: BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/ (trailing slash required).
    # File format is identical between Bundler 1.17.2 and 2.x.
    #
    # Mirror URL and credentials are written as two separate keys
    # (BUNDLE_MIRROR__... + BUNDLE_<HOST_WITH_DOTS_AS_DOUBLE_UNDERSCORES>) so
    # the token does not appear in `bundle env` output or in Bundler error
    # messages that echo back the mirror URL. Both keys are required for
    # Bundler to authenticate the mirrored fetch; supported on Bundler >=1.13.

    local bundle_config bundle_config_dir
    bundle_config="$BUNDLE_CONFIG_PATH"
    bundle_config_dir=$(dirname "$bundle_config")
    local bundle_mirror_key='BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/'
    local bundle_mirror_value='https://rubygems.flatt.tech/'
    local bundle_cred_key='BUNDLE_RUBYGEMS__FLATT__TECH'
    local bundle_cred_value="token:$TOKEN"

    # Replace an existing key in-place, or append it (with a trailing-newline
    # guard).
    bundle_set_key() {
      local file="$1" key="$2" value="$3"
      # Use the same key-escape and ^anchor for both grep and sed so the
      # existence probe and the replace pattern stay in lockstep. A naive
      # `grep -qF "${key}:"` would lose the line-anchor and would (1) match
      # a commented-out occurrence such as "# legacy: BUNDLE_*: ..." or any
      # other non-anchored substring, then (2) the anchored sed would no-op,
      # then (3) the else-branch append is also skipped -- silently dropping
      # the key.
      local key_esc
      key_esc=$(escape_for_sed_basic "$key")
      if grep -qE "^${key_esc}:" "$file" 2>/dev/null; then
        # Detect by KEY, not by value: any existing setting (including one
        # pointing elsewhere) is replaced so Guard's config takes effect
        # without duplication.
        sed_inplace 's|^'"$key_esc"':.*|'"$key"': "'"$value"'"|' "$file"
      else
        # Existing files may lack a trailing newline (echo -n, GUI editors).
        # Without this guard the new key concatenates with the previous one
        # and breaks YAML parsing in Bundler.
        ensure_trailing_newline "$file"
        printf '%s: "%s"\n' "$key" "$value" >> "$file"
      fi
    }

    if [ -f "$bundle_config" ]; then
      if grep -qF "$TOKEN" "$bundle_config" 2>/dev/null; then
        echo "[OK] bundler already configured"
      else
        backup_file "$bundle_config"
        bundle_set_key "$bundle_config" "$bundle_mirror_key" "$bundle_mirror_value"
        bundle_set_key "$bundle_config" "$bundle_cred_key" "$bundle_cred_value"
        restrict_permissions "$bundle_config"
        echo "[OK] bundler configured"
      fi
    elif find_command bundle || find_command ruby; then
      [ ! -d "$bundle_config_dir" ] && mkdir -p "$bundle_config_dir"
      track_created_file "$bundle_config"
      printf -- '---\n%s: "%s"\n%s: "%s"\n' \
        "$bundle_mirror_key" "$bundle_mirror_value" \
        "$bundle_cred_key" "$bundle_cred_value" > "$bundle_config"
      restrict_permissions "$bundle_config"
      echo "[OK] bundler configured"
    else
      echo "[SKIP] bundler not available"
    fi

  fi # has_scope rubygems
}

# ---------------------------------------------------------------------------
# Legacy (all-in-one) orchestrator
# ---------------------------------------------------------------------------

run_legacy() {
  validate_user_identifier "$USER_IDENTIFIER"
  validate_safe_string "BOT_ID" "$BOT_ID"
  validate_safe_string "TG_BOT_API_KEY" "$API_KEY"

  arm_traps

  if ! has_target; then
    echo "[Done] No configurable tools found for scopes: $SCOPES. Skipping token mint."
    exit 0
  fi

  local UNIQUE_TOKENS TOKEN abort_unknown
  UNIQUE_TOKENS=$(collect_existing_tokens)
  TOKEN=""
  abort_unknown=false

  # Walk every discovered candidate and classify it against the server:
  #   active   -- adopt this token, stop iterating, skip mint.
  #   revoked  -- discard this token, try the next candidate.
  #   unknown  -- abort. Silently rewriting config files under an unverified
  #               server state could propagate a dead token across every
  #               package manager.
  # If every candidate is revoked, or none exists, fall through to mint.
  #
  # TG_PREMINTED_TOKEN suppresses the /status probe so that environments
  # without working API access still succeed via the existing-token path.
  # Locally discovered tokens still win over the env-supplied value.
  if [ -n "$UNIQUE_TOKENS" ]; then
    if [ -n "${TG_PREMINTED_TOKEN:-}" ]; then
      # Local discovery wins over the env-supplied fallback; pick the first
      # discovered value without probing /status. The downstream
      # "[Skip] Existing org token found, reusing" log line then fires.
      TOKEN=$(printf '%s\n' "$UNIQUE_TOKENS" | head -n 1)
    else
      while IFS= read -r candidate; do
        [ -n "$candidate" ] || continue
        case "$(get_token_status "$candidate")" in
          active)
            TOKEN="$candidate"
            echo "[OK] Existing org token validated as active"
            break
            ;;
          revoked)
            # Definitively dead; try the next candidate.
            ;;
          unknown)
            abort_unknown=true
            break
            ;;
        esac
      done <<EOF
$UNIQUE_TOKENS
EOF
    fi
  fi

  if [ "$abort_unknown" = true ]; then
    echo "[Error] Could not verify org token status against the Shisho Cloud API (unreachable, timed out, or returned an unexpected response)." >&2
    echo "[Error] Aborting to avoid rewriting config files with an unverified token. Re-run after the API becomes reachable." >&2
    rollback  # revert anything tracked so far; no-op if nothing yet
    exit 1
  fi

  if [ -z "$TOKEN" ] && [ -n "$UNIQUE_TOKENS" ] && [ -z "${TG_PREMINTED_TOKEN:-}" ]; then
    echo "[Info] All discovered org tokens are inactive or do not belong to this organisation; minting a fresh one"
    echo "[Warn] Existing tokens will be overwritten with the new token; any registry access via a different organisation on this device will be lost."
  fi

  if [ -n "$TOKEN" ]; then
    echo "[Skip] Existing org token found, reusing"
    # Continue to configure scopes (don't exit -- allows incremental scope
    # addition).
  elif [ -n "${TG_PREMINTED_TOKEN:-}" ]; then
    TOKEN="$TG_PREMINTED_TOKEN"
    if ! printf '%s' "$TOKEN" | grep -qE '^tg_org_[A-Za-z0-9_-]{20,}$'; then
      echo "[Error] TG_PREMINTED_TOKEN format unexpected" >&2
      exit 1
    fi
    echo "[OK] Using pre-minted token"
  else
    TOKEN=$(mint_token "$USER_IDENTIFIER")
    echo "[OK] Token minted"
  fi

  install_configs "$TOKEN"

  echo "[Done] Takumi Guard setup complete"
}

# ---------------------------------------------------------------------------
# Subcommand entry points
# ---------------------------------------------------------------------------

require_api_credentials() {
  if [ -z "$BOT_ID" ]; then
    echo "[Error] TG_BOT_ID environment variable is required for $SUBCOMMAND" >&2
    exit 1
  fi
  if [ -z "$API_KEY" ]; then
    echo "[Error] TG_BOT_API_KEY environment variable is required for $SUBCOMMAND" >&2
    exit 1
  fi
  validate_safe_string "BOT_ID" "$BOT_ID"
  validate_safe_string "TG_BOT_API_KEY" "$API_KEY"
}

case "$SUBCOMMAND" in
  precheck)
    if has_target; then
      exit 0
    else
      exit 1
    fi
    ;;
  discover)
    tokens=$(collect_existing_tokens)
    if [ -n "$tokens" ]; then
      printf '%s\n' "$tokens" >&3
      exit 0
    fi
    exit 1
    ;;
  verify)
    require_api_credentials
    result=$(get_token_status "$SUB_TOKEN")
    printf '%s\n' "$result" >&3
    exit 0
    ;;
  issue)
    require_api_credentials
    validate_user_identifier "$USER_IDENTIFIER"
    token=$(mint_token "$USER_IDENTIFIER")
    printf '%s\n' "$token" >&3
    exit 0
    ;;
  install)
    if ! printf '%s' "$SUB_TOKEN" | grep -qE '^tg_org_[A-Za-z0-9_-]{20,}$'; then
      echo "[Error] Token format unexpected" >&2
      exit 1
    fi
    arm_traps
    install_configs "$SUB_TOKEN"
    exit 0
    ;;
  "")
    run_legacy
    ;;
esac
