#!/usr/bin/env bash
#
# Install or audit the Claude GitHub Action workflow on a target repo, or
# bulk-set the CLAUDE_CODE_OAUTH_TOKEN or R2 image-upload secrets on many
# repos at once.
#
# Usage:
#   sync-claude-action.sh setup-tokens [--discover] [--yes] [OWNER/REPO...]
#   sync-claude-action.sh setup-r2 [--discover] [--yes] [OWNER/REPO...]
#   sync-claude-action.sh install OWNER/REPO
#   sync-claude-action.sh audit OWNER/REPO
#
# `setup-tokens` runs `claude setup-token` ONCE (browser auth), captures the
# resulting OAuth token IN MEMORY ONLY (never written to disk), validates it,
# then loops `gh secret set CLAUDE_CODE_OAUTH_TOKEN` over every named repo.
# One browser auth, one new token, N secret writes. Use this for initial
# rollout and for annual token rotation across all managed repos.
#
# `setup-r2` writes R2 image-upload secrets to each named repo. The three
# required secrets (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ACCOUNT_ID)
# must be set in the environment. Two optional secrets (R2_BUCKET,
# R2_PUBLIC_URL) override the bucket name and public base URL; when omitted
# the workflow defaults to `img` and `https://img.chriscalo.com`. Values
# come from environment variables of the same names — held in memory only,
# never written to disk.
#
# Flags:
#   --discover  Auto-find managed repos via `gh search code --filename
#               claude.yaml`. Use for annual rotation across the whole set
#               of repos that already have the workflow installed.
#   --yes       Skip the "Proceed? [y/N]" confirmation prompt.
#
# `install` sets up permissions, label, and workflow PR for one repo:
#   1. Flip Actions workflow permissions to write + allow PR creation
#   2. Create the `claude` label (used by the workflow's label trigger)
#   3. Open a PR adding/replacing .github/workflows/claude.yaml with the
#      canonical version from this directory
# Assumes the OAuth token secret is already set (run `setup-tokens` first).
#
# `audit` fetches the repo's current workflow file and asks `claude` to
# describe how it differs from canonical in plain language. Doesn't decide
# anything — operator reads the summary and chooses what to do.
#
# For batch install, wrap in a shell loop:
#   for r in repo1 repo2 repo3; do
#     sync-claude-action.sh install "chriscalo/$r"
#   done

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CANONICAL_WORKFLOW="$SCRIPT_DIR/claude-action-workflow.yaml"
CANONICAL_EXPIRY_WORKFLOW="$SCRIPT_DIR/claude-token-expiry-workflow.yaml"

REPO_BASE="https://github.com/chriscalo/dev-skills/blob/main"
CANONICAL_WORKFLOW_URL="$REPO_BASE/skills/github/claude-action-workflow.yaml"
CANONICAL_DOC_URL="$REPO_BASE/skills/github/claude-action.md#tool-policy"

RULE="────────────────────────────────────────────────────────────────────"

log()      { echo "$@" >&2; }
banner()   { log; log "$RULE"; log "  $1"; log "$RULE"; }
section()  { log; log "→ $1"; }
ok()       { log "  ✓ $1"; }
warn()     { log "  ! $1"; }
fail_msg() { log "  ✗ $1"; }
indented() { log "    $1"; }

usage() {
  cat >&2 <<EOF
Usage: $(basename "$0") setup-tokens [--discover] [--yes] [OWNER/REPO...]
       $(basename "$0") setup-r2 [--discover] [--yes] [OWNER/REPO...]
       $(basename "$0") install OWNER/REPO
       $(basename "$0") audit OWNER/REPO
  setup-tokens  One OAuth token written to N repos. --discover auto-finds
                every repo that already has claude.yaml installed.
  setup-r2      R2 image-upload secrets written to N repos. Reads
                R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_ACCOUNT_ID
                (required) plus R2_BUCKET / R2_PUBLIC_URL (optional) from
                env. --discover auto-finds installed repos.
  install       Set up permissions, label, open workflow PR for one repo.
  audit         Show how a repo's workflow differs from canonical.
EOF
  exit 2
}

require_command() {
  if ! command -v "$1" >/dev/null 2>&1; then
    log "error: $1 not found in PATH"
    exit 1
  fi
}

require_canonical() {
  if [ ! -f "$CANONICAL_WORKFLOW" ]; then
    log "error: canonical workflow missing at $CANONICAL_WORKFLOW"
    exit 1
  fi
  if [ ! -f "$CANONICAL_EXPIRY_WORKFLOW" ]; then
    log "error: canonical expiry workflow template missing at $CANONICAL_EXPIRY_WORKFLOW"
    exit 1
  fi
}

install_expiry_workflow() {
  local repo="$1"
  local issued_at lifetime_days expires_on
  issued_at=$(date +%Y-%m-%d)
  lifetime_days=365
  expires_on=$(date -d "$issued_at + $lifetime_days days" +%Y-%m-%d)

  # Compute 7 notification dates and build cron schedule entries.
  # Each entry fires on a specific D M date once per year. setup-tokens
  # rewrites this workflow file on each rotation, replacing stale entries.
  local offsets=(30 15 7 3 2 1 0)
  local day_labels=("30 days before expiry" "15 days before expiry" \
    "7 days before expiry" "3 days before expiry" "2 days before expiry" \
    "1 day before expiry" "expiry day")
  local cron_schedule=""
  local i
  for i in 0 1 2 3 4 5 6; do
    local offset="${offsets[$i]}"
    local label="${day_labels[$i]}"
    local notify_date
    notify_date=$(date -d "$expires_on - $offset days" +%Y-%m-%d)
    local d m
    d=$(date -d "$notify_date" +%-d)
    m=$(date -d "$notify_date" +%-m)
    # Build with actual newlines so the multi-line string passes to awk cleanly
    cron_schedule="${cron_schedule}    - cron: '0 9 $d $m *'  # $label ($notify_date)
"
  done

  # Substitute three placeholders in the canonical template:
  # __SCHEDULE_ENTRIES__ → the 7 cron lines built above
  # __ISSUED_AT__        → ISO issue date (e.g. "2026-06-01")
  # __EXPIRES_ON__       → ISO expiry date (e.g. "2027-06-01")
  # ENVIRON avoids -v escape-interpretation ambiguity across awk implementations.
  local workflow_yaml workflow_b64 existing_sha
  export _CLAUDE_EXPIRY_CRON="$cron_schedule"
  workflow_yaml=$(awk -v issued="$issued_at" -v expires="$expires_on" '
    /^    # __SCHEDULE_ENTRIES__$/ {
      printf "%s", ENVIRON["_CLAUDE_EXPIRY_CRON"]
      next
    }
    {
      gsub(/__ISSUED_AT__/, issued)
      gsub(/__EXPIRES_ON__/, expires)
      print
    }
  ' "$CANONICAL_EXPIRY_WORKFLOW")
  unset _CLAUDE_EXPIRY_CRON
  workflow_b64=$(printf '%s' "$workflow_yaml" | base64 | tr -d '\n')

  local workflow_path=".github/workflows/claude-token-expiry.yaml"
  existing_sha=$(gh api "repos/$repo/contents/$workflow_path" \
    --jq '.sha' 2>/dev/null || true)
  if [ -n "$existing_sha" ]; then
    gh api -X PUT "repos/$repo/contents/$workflow_path" \
      -f message="Update claude-token-expiry.yaml (notifications for $expires_on)" \
      -f content="$workflow_b64" \
      -f sha="$existing_sha" >/dev/null 2>&1
  else
    gh api -X PUT "repos/$repo/contents/$workflow_path" \
      -f message="Add claude-token-expiry.yaml (notifications for $expires_on)" \
      -f content="$workflow_b64" >/dev/null 2>&1
  fi

  # Migration: delete leftover claude-token-state.json if it exists.
  # The expiry workflow used to read this file at runtime; the new template
  # bakes the dates in directly. One rotation cycle migrates each repo.
  local state_sha
  state_sha=$(gh api "repos/$repo/contents/.github/claude-token-state.json" \
    --jq '.sha' 2>/dev/null || true)
  if [ -n "$state_sha" ]; then
    gh api -X DELETE "repos/$repo/contents/.github/claude-token-state.json" \
      -f message="Drop claude-token-state.json (dates now baked into expiry workflow)" \
      -f sha="$state_sha" >/dev/null 2>&1 \
      || indented "warning: failed to delete claude-token-state.json (non-fatal)"
  fi

  # Close any open claude-token-expiry issues — stale after rotation.
  local open_issue
  open_issue=$(gh issue list \
    --repo "$repo" \
    --label "claude-token-expiry" \
    --state open \
    --json number \
    --jq '.[0].number // ""' 2>/dev/null || true)
  if [ -n "$open_issue" ]; then
    gh issue close "$open_issue" \
      --repo "$repo" \
      --comment "Token rotated on $issued_at. New expiry: $expires_on. Notification schedule reset for the new expiry date." \
      2>/dev/null || indented "warning: failed to close old expiry issue (non-fatal)"
  fi
}

flip_permissions() {
  local repo="$1"
  local result perm pr
  result=$(gh api "repos/$repo/actions/permissions/workflow" 2>/dev/null \
    || true)
  perm=$(echo "$result" | jq -r '.default_workflow_permissions // ""')
  pr=$(echo "$result" | jq -r '.can_approve_pull_request_reviews // ""')
  if [ "$perm" = "write" ] && [ "$pr" = "true" ]; then
    ok "Actions permissions already write + allow PR creation"
    return
  fi
  gh api -X PUT "repos/$repo/actions/permissions/workflow" \
    -F default_workflow_permissions=write \
    -F can_approve_pull_request_reviews=true >/dev/null
  ok "Flipped Actions permissions to write + allow PR creation"
}

create_label() {
  local repo="$1"
  gh label create claude \
    --description "Trigger Claude Code Action" \
    --color FF6B35 \
    --force \
    --repo "$repo" >/dev/null
  ok "'claude' label exists (created or updated)"
}

open_workflow_pr() (
  # Subshell isolates the trap and `cd` from the parent shell.
  set -euo pipefail
  local repo="$1"
  local tmpdir
  tmpdir=$(mktemp -d -t claude-sync-XXXXXX)
  trap "rm -rf '$tmpdir'" EXIT
  gh repo clone "$repo" "$tmpdir/repo" -- --depth=1 --quiet
  ok "Cloned to $tmpdir/repo"
  cd "$tmpdir/repo"
  local default_branch
  default_branch=$(git rev-parse --abbrev-ref HEAD)
  local action="install"
  if [ -f .github/workflows/claude.yaml ] || \
     [ -f .github/workflows/claude.yml ]; then
    action="standardize"
  fi
  local branch="claude/${action}-claude-action-workflow"
  # If a prior sync left the branch on the remote (typically because the PR
  # was never merged or closed), check it out and commit on top instead of
  # starting fresh from the default branch. Without this, the later push
  # fails as non-fast-forward and the script aborts.
  local existing_pr_url=""
  if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
    git fetch origin "$branch" --depth=2 --quiet
    git checkout -b "$branch" FETCH_HEAD >/dev/null 2>&1
    existing_pr_url=$(gh pr list --repo "$repo" --head "$branch" \
      --state open --json url --jq '.[0].url // empty')
    if [ -n "$existing_pr_url" ]; then
      ok "Refreshing existing branch $branch (open PR: $existing_pr_url)"
    else
      ok "Reusing existing branch $branch (no open PR)"
    fi
  else
    git checkout -b "$branch" >/dev/null 2>&1
    ok "Created branch $branch"
  fi
  mkdir -p .github/workflows
  cp "$CANONICAL_WORKFLOW" .github/workflows/claude.yaml
  rm -f .github/workflows/claude.yml
  local needs_commit=true
  if git diff --quiet && [ -z "$(git status --porcelain)" ]; then
    # Working tree already matches branch HEAD (canonical was already present).
    # With an open PR the branch is fine as-is.  In the stale-branch/no-open-PR
    # case the branch may carry canonical from a prior closed PR while the
    # default branch was never updated — only skip when the default branch is
    # also already up-to-date.
    if [ -n "$existing_pr_url" ] || \
       git diff --quiet "origin/$default_branch" -- .github/workflows/claude.yaml 2>/dev/null; then
      warn "Workflow already matches canonical — no PR needed"
      return
    fi
    # Stale branch already has canonical; push it as-is and open a new PR.
    warn "Branch has canonical workflow but no open PR — opening PR"
    needs_commit=false
  fi
  if [ "$needs_commit" = true ]; then
    ok "Wrote .github/workflows/claude.yaml ($action)"
    git add .github/workflows/claude.yaml
    git add .github/workflows/claude.yml 2>/dev/null || true
  fi
  local commit_msg
  if [ "$needs_commit" = true ]; then
    if [ -n "$existing_pr_url" ]; then
      commit_msg="Refresh canonical workflow"
    elif [ "$action" = "install" ]; then
      commit_msg="Install Claude GitHub Action workflow"
    else
      commit_msg="Standardize Claude GitHub Action workflow"
    fi
    git commit -q -m "$commit_msg

Adds (or replaces) .github/workflows/claude.yaml with the canonical
workflow from chriscalo/dev-skills:skills/github/claude-action-workflow.yaml.

Tool policy: --allowedTools \"Read,Grep,Glob,Edit,Write,Skill,Task,Agent,Bash(*),WebFetch,WebSearch\"
with a targeted --disallowedTools list (force-push, push-to-main, branch/repo/
release delete, mutating gh api, npm publish, gh secret/auth ops). See
chriscalo/dev-skills:skills/github/claude-action.md section 'Tool policy'
for rationale.
"
  fi
  git push -u origin "$branch" --quiet
  ok "Pushed branch to origin"
  if [ -n "$existing_pr_url" ]; then
    ok "PR refreshed: $existing_pr_url"
    return
  fi
  local pr_title
  if [ "$action" = "install" ]; then
    pr_title="Install Claude GitHub Action workflow"
  else
    pr_title="Standardize Claude GitHub Action workflow"
  fi
  local pr_body
  pr_body="Adds (or replaces) \`.github/workflows/claude.yaml\` with the
canonical workflow from
[chriscalo/dev-skills]($CANONICAL_WORKFLOW_URL).

**Tool policy**: \`--allowedTools \"Read,Grep,Glob,Edit,Write,Skill,Task,Agent,Bash(*),WebFetch,WebSearch\"\`
with a targeted \`--disallowedTools\` list. See
[claude-action.md \"Tool policy\"]($CANONICAL_DOC_URL)
for rationale.

Future drift can be detected via
\`sync-claude-action.sh audit OWNER/REPO\`."
  local pr_url
  pr_url=$(gh pr create \
    --repo "$repo" \
    --base "$default_branch" \
    --head "$branch" \
    --title "$pr_title" \
    --body "$pr_body")
  ok "PR opened: $pr_url"
)

discover_managed_repos() {
  local owner
  owner=$(gh api user --jq '.login')
  gh search code --filename claude.yaml --owner "$owner" \
    --json repository --jq '.[].repository.nameWithOwner' | sort -u
}

cmd_setup_tokens() {
  local discover=false
  local yes=false
  local repos=()
  local arg
  for arg in "$@"; do
    case "$arg" in
      --discover) discover=true ;;
      --yes|-y) yes=true ;;
      --*)
        log "error: unknown flag: $arg"
        usage
        ;;
      */*) repos+=("$arg") ;;
      *)
        log "error: invalid repo arg (expected OWNER/REPO): $arg"
        usage
        ;;
    esac
  done
  if [ "$discover" = true ]; then
    if [ ${#repos[@]} -gt 0 ]; then
      log "error: --discover and explicit repos are mutually exclusive"
      exit 2
    fi
    log "Discovering managed repos via gh search..."
    local discovered
    discovered=$(discover_managed_repos)
    if [ -z "$discovered" ]; then
      log "error: --discover found no repos with claude.yaml installed"
      exit 1
    fi
    while IFS= read -r line; do
      repos+=("$line")
    done <<< "$discovered"
  fi
  if [ ${#repos[@]} -eq 0 ]; then
    log "error: setup-tokens needs at least one OWNER/REPO or --discover"
    usage
  fi
  banner "Setup CLAUDE_CODE_OAUTH_TOKEN on ${#repos[@]} repo(s)"
  log "This will issue ONE new OAuth token via 'claude setup-token'"
  log "(browser auth) and write it as the CLAUDE_CODE_OAUTH_TOKEN secret"
  log "on each repo below. The token is held in memory only — never on"
  log "disk. Existing tokens on these repos will be overwritten."
  log
  log "Repos to update:"
  local repo i=0 width
  width=${#repos[@]}
  width=${#width}
  for repo in "${repos[@]}"; do
    i=$((i + 1))
    printf "  %${width}d. %s\n" "$i" "$repo" >&2
  done
  log
  if [ "$yes" != true ]; then
    local reply
    read -r -p "Proceed? [y/N] " reply
    case "$reply" in
      [yY]|[yY][eE][sS]) ;;
      *)
        log "aborted"
        exit 1
        ;;
    esac
  fi
  section "Generating OAuth token via 'claude setup-token' (browser auth)"
  local token
  token=$(claude setup-token \
    | grep -oE 'sk-ant-oat[0-9]+-[A-Za-z0-9_-]{50,}' | head -1)
  if [ -z "$token" ]; then
    fail_msg "no sk-ant-oat token matched in setup-token output"
    indented "the token format may have changed —"
    indented "check Anthropic docs and update the regex"
    exit 1
  fi
  if [ "${#token}" -lt 80 ] || [ "${#token}" -gt 256 ]; then
    fail_msg "extracted token has suspicious length (${#token})"
    exit 1
  fi
  ok "Token captured: ${token:0:14}... (${#token} chars), held in memory only"
  section "Writing secret to ${#repos[@]} repo(s)"
  local total=${#repos[@]}
  local pad=${#total}
  local n=0 ok_count=0 fail_count=0
  local failed=()
  for repo in "${repos[@]}"; do
    n=$((n + 1))
    if gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$repo" \
        --body "$token" 2>/dev/null; then
      printf "  [%${pad}d/%d] ✓ %s\n" "$n" "$total" "$repo" >&2
      ok_count=$((ok_count + 1))
      if install_expiry_workflow "$repo" 2>/dev/null; then
        indented "wrote expiry workflow"
      else
        indented "warning: failed to write expiry workflow (non-fatal)"
      fi
    else
      printf "  [%${pad}d/%d] ✗ %s\n" "$n" "$total" "$repo" >&2
      fail_count=$((fail_count + 1))
      failed+=("$repo")
    fi
  done
  unset token
  section "Discarding token from memory"
  ok "Token cleared"
  banner "Result: $ok_count succeeded, $fail_count failed"
  if [ "$fail_count" -gt 0 ]; then
    log "Failed repos:"
    for repo in "${failed[@]}"; do
      log "  $repo"
    done
    log
    log "Re-run with the failed repos to retry. The succeeded repos already"
    log "have the new token; re-running on them just overwrites with the same"
    log "(or another new) token."
    exit 1
  fi
  log
  log "Next steps:"
  log "  • Install/standardize the workflow on each repo:"
  log "      sync-claude-action.sh install OWNER/REPO"
  log "  • For batch install, wrap in a shell loop:"
  log "      for r in repo1 repo2 ...; do"
  log "        sync-claude-action.sh install \"OWNER/\$r\""
  log "      done"
  log "  • Audit drift on existing installs:"
  log "      sync-claude-action.sh audit OWNER/REPO"
}

cmd_setup_r2() {
  local discover=false
  local yes=false
  local repos=()
  local arg
  for arg in "$@"; do
    case "$arg" in
      --discover) discover=true ;;
      --yes|-y) yes=true ;;
      --*)
        log "error: unknown flag: $arg"
        usage
        ;;
      */*) repos+=("$arg") ;;
      *)
        log "error: invalid repo arg (expected OWNER/REPO): $arg"
        usage
        ;;
    esac
  done
  if [ "$discover" = true ]; then
    if [ ${#repos[@]} -gt 0 ]; then
      log "error: --discover and explicit repos are mutually exclusive"
      exit 2
    fi
    log "Discovering managed repos via gh search..."
    local discovered
    discovered=$(discover_managed_repos)
    if [ -z "$discovered" ]; then
      log "error: --discover found no repos with claude.yaml installed"
      exit 1
    fi
    while IFS= read -r line; do
      repos+=("$line")
    done <<< "$discovered"
  fi
  if [ ${#repos[@]} -eq 0 ]; then
    log "error: setup-r2 needs at least one OWNER/REPO or --discover"
    usage
  fi
  # Pull values from env (memory only — never written to disk by this
  # script). The three required vars authenticate to R2; the two optional
  # vars override bucket name and public URL (defaults: img /
  # https://img.chriscalo.com).
  local missing=()
  [ -n "${R2_ACCESS_KEY_ID:-}" ]     || missing+=(R2_ACCESS_KEY_ID)
  [ -n "${R2_SECRET_ACCESS_KEY:-}" ] || missing+=(R2_SECRET_ACCESS_KEY)
  [ -n "${R2_ACCOUNT_ID:-}" ]        || missing+=(R2_ACCOUNT_ID)
  if [ ${#missing[@]} -gt 0 ]; then
    log "error: missing required env vars: ${missing[*]}"
    log
    log "Export the R2 credentials before running, e.g.:"
    log "  export R2_ACCESS_KEY_ID=..."
    log "  export R2_SECRET_ACCESS_KEY=..."
    log "  export R2_ACCOUNT_ID=..."
    log "Optionally also set R2_BUCKET (default: img) and R2_PUBLIC_URL"
    log "(default: https://img.chriscalo.com) to target a different bucket."
    log "Get the access key + secret from a Cloudflare R2 API token with"
    log "Object Read & Write on the bucket; the account ID is in the"
    log "Cloudflare dashboard URL or in any R2 endpoint URL."
    exit 1
  fi
  local secret_count=3
  [ -n "${R2_BUCKET:-}" ]      && secret_count=$((secret_count + 1))
  [ -n "${R2_PUBLIC_URL:-}" ]  && secret_count=$((secret_count + 1))
  banner "Setup R2 image-upload secrets on ${#repos[@]} repo(s)"
  log "Writes R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ACCOUNT_ID"
  [ -n "${R2_BUCKET:-}" ]     && log "       R2_BUCKET"
  [ -n "${R2_PUBLIC_URL:-}" ] && log "       R2_PUBLIC_URL"
  log "to each repo below. Values come from environment variables of the"
  log "same names — held in memory only, never written to disk by this"
  log "script. Existing secrets on these repos will be overwritten."
  log
  log "Repos to update:"
  local repo i=0 width
  width=${#repos[@]}
  width=${#width}
  for repo in "${repos[@]}"; do
    i=$((i + 1))
    printf "  %${width}d. %s\n" "$i" "$repo" >&2
  done
  log
  if [ "$yes" != true ]; then
    local reply
    read -r -p "Proceed? [y/N] " reply
    case "$reply" in
      [yY]|[yY][eE][sS]) ;;
      *)
        log "aborted"
        exit 1
        ;;
    esac
  fi
  section "Writing $secret_count secrets per repo to ${#repos[@]} repo(s)"
  local total=${#repos[@]}
  local pad=${#total}
  local n=0 ok_count=0 fail_count=0
  local failed=()
  for repo in "${repos[@]}"; do
    n=$((n + 1))
    local repo_ok=true
    gh secret set R2_ACCESS_KEY_ID --repo "$repo" \
      --body "$R2_ACCESS_KEY_ID" 2>/dev/null || repo_ok=false
    gh secret set R2_SECRET_ACCESS_KEY --repo "$repo" \
      --body "$R2_SECRET_ACCESS_KEY" 2>/dev/null || repo_ok=false
    gh secret set R2_ACCOUNT_ID --repo "$repo" \
      --body "$R2_ACCOUNT_ID" 2>/dev/null || repo_ok=false
    if [ -n "${R2_BUCKET:-}" ]; then
      gh secret set R2_BUCKET --repo "$repo" \
        --body "$R2_BUCKET" 2>/dev/null || repo_ok=false
    fi
    if [ -n "${R2_PUBLIC_URL:-}" ]; then
      gh secret set R2_PUBLIC_URL --repo "$repo" \
        --body "$R2_PUBLIC_URL" 2>/dev/null || repo_ok=false
    fi
    if [ "$repo_ok" = true ]; then
      printf "  [%${pad}d/%d] ✓ %s\n" "$n" "$total" "$repo" >&2
      ok_count=$((ok_count + 1))
    else
      printf "  [%${pad}d/%d] ✗ %s\n" "$n" "$total" "$repo" >&2
      fail_count=$((fail_count + 1))
      failed+=("$repo")
    fi
  done
  banner "Result: $ok_count succeeded, $fail_count failed"
  if [ "$fail_count" -gt 0 ]; then
    log "Failed repos:"
    for repo in "${failed[@]}"; do
      log "  $repo"
    done
    log
    log "Re-run with the failed repos to retry. Partial success on a repo"
    log "(some secrets set, others not) is possible — re-running is safe;"
    log "it overwrites each secret with the same value."
    exit 1
  fi
  log
  log "Next steps:"
  log "  • Verify by opening a PR in one of the repos and asking Claude"
  log "    to post a screenshot. The image should render inline."
  log "  • If a repo's workflow doesn't have the R2 env block yet,"
  log "    re-sync to canonical:"
  log "      sync-claude-action.sh install OWNER/REPO"
}

cmd_install() {
  local repo="$1"
  banner "Install Claude Action workflow on $repo"
  section "[1/3] Checking Actions workflow permissions"
  flip_permissions "$repo"
  section "[2/3] Ensuring 'claude' label exists"
  create_label "$repo"
  section "[3/3] Opening workflow PR"
  open_workflow_pr "$repo"
  banner "Done"
  log
  log "Next steps:"
  log "  • Review and merge the PR (or close it to skip)."
  log "  • Test the action by mentioning @claude in an issue, or by"
  log "    applying the 'claude' label to an issue."
  log "  • If the action fails to authenticate, confirm setup-tokens has"
  log "    run on this repo (CLAUDE_CODE_OAUTH_TOKEN secret must exist)."
}

cmd_audit() {
  local repo="$1"
  banner "Audit $repo workflow against canonical"
  section "Fetching .github/workflows/claude.yaml"
  local current
  current=$(gh api "repos/$repo/contents/.github/workflows/claude.yaml" \
    --jq '.content' 2>/dev/null | base64 -d 2>/dev/null || true)
  local found_as=".github/workflows/claude.yaml"
  if [ -z "$current" ]; then
    current=$(gh api "repos/$repo/contents/.github/workflows/claude.yml" \
      --jq '.content' 2>/dev/null | base64 -d 2>/dev/null || true)
    found_as=".github/workflows/claude.yml"
  fi
  if [ -z "$current" ]; then
    warn "No workflow file found in $repo"
    log
    log "Next steps:"
    log "  • Install the workflow:"
    log "      sync-claude-action.sh install $repo"
    return
  fi
  ok "Found $found_as (${#current} bytes)"
  section "Asking claude to summarize differences vs canonical"
  local canonical
  canonical=$(cat "$CANONICAL_WORKFLOW")
  local prompt
  prompt="You are summarizing the difference between two GitHub Actions
workflow files. Describe in plain language how the REPO version differs
from the CANONICAL version. Don't decide what to do — just describe.
Focus on functional differences (auth method, triggers, permissions,
allowedTools/disallowedTools, action versions). Ignore comments and
whitespace.

CANONICAL:
$canonical

REPO:
$current"
  log
  echo "$prompt" | claude -p
  log
  banner "Done"
  log
  log "Next steps:"
  log "  • If the diff matters: sync-claude-action.sh install $repo"
  log "    (the install subcommand replaces an existing workflow with"
  log "    canonical via PR)"
  log "  • If the diff is intentional: leave as-is. Note the reason for"
  log "    future you."
}

main() {
  if [ $# -lt 1 ]; then
    usage
  fi
  require_command gh
  require_command jq
  require_command git
  require_canonical
  local cmd="$1"
  shift
  case "$cmd" in
    setup-tokens)
      require_command claude
      cmd_setup_tokens "$@"
      ;;
    setup-r2)
      cmd_setup_r2 "$@"
      ;;
    install)
      if [ $# -lt 1 ]; then usage; fi
      cmd_install "$1"
      ;;
    audit)
      require_command claude
      if [ $# -lt 1 ]; then usage; fi
      cmd_audit "$1"
      ;;
    *)
      usage
      ;;
  esac
}

main "$@"
