#!/bin/bash
# Takumi Guard token provisioning
# Usage: TG_BOT_API_KEY=... ./takumi-guard-setup-{VERSION}.sh <BOT_ID> <USER_IDENTIFIER> [SCOPES]
#
# Mints a new org user token and configures package managers to use Takumi Guard.
# Designed to be called directly or via an MDM deployment wrapper script.
#
# Supported:
#   npm ecosystem      -- npm, pnpm, yarn v2+, bun
#   PyPI ecosystem     -- pip, uv, poetry
#   RubyGems ecosystem -- Bundler
#
# Arguments:
#   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. Bot API key from Shisho Cloud console.
#                   Passed via env var to avoid shell history / process table exposure.
#   TG_PREMINTED_TOKEN  Optional. Pre-minted org token to use instead of calling the API.
#                       Validated against tg_org_ format before use.
#   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. The PowerShell counterpart accepts the same variable.
#
# Idempotency:
#   This script 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.

set -euE

# ---------------------------------------------------------------------------
# Prerequisites
# ---------------------------------------------------------------------------

if ! command -v curl >/dev/null 2>&1; then
  echo "[Error] Required command not found: curl" >&2
  exit 1
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
}

# ---------------------------------------------------------------------------
# Argument parsing and validation
# ---------------------------------------------------------------------------

BOT_ID="${1:?Usage: TG_BOT_API_KEY=... setup.sh BOT_ID USER_IDENTIFIER [SCOPES]}"
USER_IDENTIFIER="${2:?Usage: TG_BOT_API_KEY=... setup.sh BOT_ID USER_IDENTIFIER [SCOPES]}"
API_KEY="${TG_BOT_API_KEY:?Set TG_BOT_API_KEY environment variable}"
SCOPES="${3:-npm,pypi,rubygems}"

# 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.
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
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)

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

# 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 of the
      # following:
      #   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. The double check turns that silent
      # failure into an explicit fail-closed abort.
      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" —
      # fail-closed per the validation-endpoint design.
      printf 'unknown'
      return 0
      ;;
  esac
}

# Validate that a value contains only safe characters for JSON string embedding.
# Prevents JSON injection when constructing request bodies.
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"
}

validate_user_identifier "$USER_IDENTIFIER"
validate_safe_string "BOT_ID" "$BOT_ID"
validate_safe_string "TG_BOT_API_KEY" "$API_KEY"

# ---------------------------------------------------------------------------
# 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
}

# 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

# ---------------------------------------------------------------------------
# Check for existing tg_org_* token
# ---------------------------------------------------------------------------

# Collect every distinct tg_org_* value already present on disk. The
# iteration below adopts the first ACTIVE candidate, so a revoked token
# in one config file does not mask an active one in another.
TOKENS_FOUND=""
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

# Deduplicate while preserving discovery order (`sort -u` would lose order;
# awk with seen[] keeps the first occurrence). The result is one token per
# line, possibly empty if nothing was found.
UNIQUE_TOKENS=$(printf '%s' "$TOKENS_FOUND" | awk 'NF && !seen[$0]++' || true)

# ---------------------------------------------------------------------------
# Pre-check: ensure at least one configurable tool or config file exists
# ---------------------------------------------------------------------------
# Without this check, a clean environment (no tools, no config files) would
# mint a token on every run but never write it anywhere -- wasting tokens.

HAS_TARGET=false
if has_scope npm; then
  [ -f "$USER_HOME/.npmrc" ] || command -v npm >/dev/null 2>&1 && HAS_TARGET=true
  find_command pnpm && HAS_TARGET=true
  [ -f "$USER_HOME/.yarnrc.yml" ] && HAS_TARGET=true
  find_command yarn && HAS_TARGET=true
  [ -f "$USER_HOME/.bunfig.toml" ] && HAS_TARGET=true
  find_command bun && HAS_TARGET=true
fi
if has_scope pypi; then
  [ -f "$USER_HOME/.config/pip/pip.conf" ] && HAS_TARGET=true
  command -v pip3 >/dev/null 2>&1 && HAS_TARGET=true
  command -v pip >/dev/null 2>&1 && HAS_TARGET=true
  [ -f "$USER_HOME/.config/uv/uv.toml" ] && HAS_TARGET=true
  find_command uv && HAS_TARGET=true
  find_command poetry && HAS_TARGET=true
fi
if has_scope rubygems; then
  [ -f "$BUNDLE_CONFIG_PATH" ] && HAS_TARGET=true
  find_command bundle && HAS_TARGET=true
  find_command ruby && HAS_TARGET=true
fi

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

# 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.
TOKEN=""
abort_unknown=false
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

# ---------------------------------------------------------------------------
# Mint a new org user 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.
HTTP_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_IDENTIFIER"}
EOF
)

HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tail -1)

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

# Extract token from JSON response
TOKEN=$(printf '%s' "$HTTP_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
  exit 1
fi

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

echo "[OK] Token minted"

fi # end of mint-or-reuse block

# ---------------------------------------------------------------------------
# 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
    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) ---

pnpm_rc_dir=""
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) ---

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) ---

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.
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 (uniform 0600 across token-adjacent files).
    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.

bundle_config="$BUNDLE_CONFIG_PATH"
bundle_config_dir=$(dirname "$bundle_config")
bundle_mirror_key='BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/'
bundle_mirror_value='https://rubygems.flatt.tech/'
bundle_cred_key='BUNDLE_RUBYGEMS__FLATT__TECH'
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

echo "[Done] Takumi Guard setup complete"
