#!/usr/bin/env bash
# figma-mcp-refresh.sh  -  silent Figma MCP (figu_) token renewal via OAuth refresh grant.
#
# Used by the Phase 0 token pre-flight (refs/phases/phase-0-init.md Step 0.7) and the
# expired-token decision (refs/keychain.md). Never prints token values  -  status lines only.
#
# Flow:
#   1. Resolve the access-token Keychain name from prefs.global.keychainMapping.figma_mcp.
#   2. Read the refresh token from "<name>_Refresh" (written by the user's generation script).
#   3. Read client credentials from .figma-oauth.json next to prefs.global.tokenScripts.figma_mcp.
#   4. POST grant_type=refresh_token to the Figma token endpoint.
#   5. On success save the new access token (and rotated refresh token) back to the Keychain.
#
# Exit codes:
#   0  renewed (access token replaced in Keychain)
#   1  refresh grant rejected by Figma (refresh token dead  -  run the generation script)
#   2  prerequisites missing (no mapping / no refresh entry / no oauth config)

set -euo pipefail

PREFS_FILE="${PREFS_FILE:-$HOME/.claude/multi-agent-preferences.json}"
TOKEN_ENDPOINT="https://api.figma.com/v1/oauth/token"

say() { printf '%s\n' "$*"; }

[ -f "$PREFS_FILE" ] || { say "figma-mcp-refresh: prefs file missing ($PREFS_FILE)"; exit 2; }

KEY_NAME=$(jq -r '.global.keychainMapping.figma_mcp // empty' "$PREFS_FILE")
[ -n "$KEY_NAME" ] || { say "figma-mcp-refresh: keychainMapping.figma_mcp not set"; exit 2; }

REFRESH_KEY="${KEY_NAME}_Refresh"

# Locate the resolver with an existence check, not a `.`-chain.
#
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
# message. Reordering does not help: whichever candidate is absent aborts at that point.
# Checking for the file before sourcing it is the only safe form.
for _cred_resolver in \
  "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
  "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
  "$HOME/.claude/lib/credential-store-resolver.sh" \
  "$HOME/.copilot/lib/credential-store-resolver.sh" \
  "$HOME/.codex/lib/credential-store-resolver.sh"; do
  [ -f "$_cred_resolver" ] || continue
  # shellcheck source=/dev/null
  . "$_cred_resolver" 2>/dev/null || true
  # `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
  # 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
  # candidate that does not resolve - the very case the loop exists to survive.
  if [ -n "${CRED_STORE:-}" ]; then break; fi
done
unset _cred_resolver
if [ -z "${CRED_STORE:-}" ]; then
  say "figma-mcp-refresh: credential helper not found  -  run the pipeline installer"
  exit 2
fi

REFRESH_TOKEN=$("$CRED_STORE" get "$REFRESH_KEY" 2>/dev/null || true)
[ -n "$REFRESH_TOKEN" ] || { say "figma-mcp-refresh: no refresh token at '$REFRESH_KEY'"; exit 2; }

SCRIPT_PATH=$(jq -r '.global.tokenScripts.figma_mcp // empty' "$PREFS_FILE")
OAUTH_CONFIG=""
if [ -n "$SCRIPT_PATH" ]; then
  OAUTH_CONFIG="$(dirname "$SCRIPT_PATH")/.figma-oauth.json"
fi
[ -n "$OAUTH_CONFIG" ] && [ -f "$OAUTH_CONFIG" ] || { say "figma-mcp-refresh: .figma-oauth.json not found (tokenScripts.figma_mcp unset or sibling config missing)"; exit 2; }

CLIENT_ID=$(jq -r '.client_id // empty' "$OAUTH_CONFIG")
CLIENT_SECRET=$(jq -r '.client_secret // empty' "$OAUTH_CONFIG")
[ -n "$CLIENT_ID" ] || { say "figma-mcp-refresh: client_id missing in .figma-oauth.json"; exit 2; }

# Refresh token + client secret go through a curl config fed via process
# substitution so they never appear in argv (argv is visible to ps).
refresh_grant_cfg() {
  printf 'data-urlencode = "grant_type=refresh_token"\n'
  printf 'data-urlencode = "refresh_token=%s"\n' "$REFRESH_TOKEN"
  printf 'data-urlencode = "client_id=%s"\n' "$CLIENT_ID"
  if [ -n "$CLIENT_SECRET" ]; then
    printf 'data-urlencode = "client_secret=%s"\n' "$CLIENT_SECRET"
  fi
}

RESPONSE=$(curl -s --max-time 20 -X POST "$TOKEN_ENDPOINT" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -K <(refresh_grant_cfg))

NEW_ACCESS=$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty' 2>/dev/null || true)
NEW_REFRESH=$(printf '%s' "$RESPONSE" | jq -r '.refresh_token // empty' 2>/dev/null || true)
EXPIRES_IN=$(printf '%s' "$RESPONSE" | jq -r '.expires_in // "?"' 2>/dev/null || true)

if [ -z "$NEW_ACCESS" ]; then
  # Distinguish a transient network failure (empty body) from an actual rejection
  # (body carries .error): the first is retryable and must NOT tell the user to
  # regenerate a credential that is still valid. Exit 2 = prerequisite/transient,
  # exit 1 = the grant was genuinely rejected.
  if [ -z "$RESPONSE" ]; then
    say "figma-mcp-refresh: no response from token endpoint (network/timeout)  -  retry; the credential is unchanged"
    exit 2
  fi
  ERR=$(printf '%s' "$RESPONSE" | jq -r '.error // .message // "unknown"' 2>/dev/null || echo "unknown")
  say "figma-mcp-refresh: refresh grant rejected ($ERR)  -  regenerate via tokenScripts.figma_mcp"
  exit 1
fi

# Keychain write through credential-store.sh with the `-` form, which reads the secret
# from stdin: the value never lands on any argv (visible to ps), which is the property
# the previous `security -i` call was carefully preserving. The store does the same thing
# per platform, so that property now holds on Linux and Windows as well.
save_keychain_secret() {
  local service="$1" secret="$2"
  printf '%s' "$secret" | "$CRED_STORE" set "$service" - >/dev/null
}

# Save the rotated REFRESH token first, then the access token. The grant at the
# curl above already invalidated the old refresh token server-side, so the
# rotated one is the only key that can renew again; if the access-token write
# then failed we would still hold a working refresh token and recover on the
# next run. The reverse order (access first) risked persisting access while
# losing the rotated refresh - a permanent dead end needing a full regenerate.
if [ -n "$NEW_REFRESH" ]; then
  save_keychain_secret "$REFRESH_KEY" "$NEW_REFRESH"
fi

save_keychain_secret "$KEY_NAME" "$NEW_ACCESS"

say "figma-mcp-refresh: renewed '$KEY_NAME' (expires_in=${EXPIRES_IN}s, refresh $( [ -n "$NEW_REFRESH" ] && echo rotated || echo unchanged ))"
exit 0
