# Auto-label issues and PRs.
#
# Two passes per item:
#   1. LLM classification (Claude Haiku) over title + body for engine::*,
#      area::*, runtime::*, component, and type labels. Runs once per
#      newly opened issue/PR.
#   2. Deterministic priority::warm / priority::hot from community
#      engagement counts (author + commenters + supporting reactions,
#      excluding bots and write-access users). Refreshed on every new
#      comment (engagement event) and via a daily cron (to pick up
#      reaction-only changes, which don't fire webhook events).
#
# *** Labels applied by this workflow are ADVISORY ONLY. ***
# Do not use them as inputs to CI gates, release gates, security
# decisions, or any policy check — the model sees untrusted user-supplied
# title/body content and the classification is best-effort.
#
# Add-only for human / LLM labels — never removes a human-applied label.
# The bot may transition between its own priority labels (drops warm
# when promoting to hot) so the two never coexist.
#
# Priority is sticky: once warm or hot is applied, it is NEVER demoted
# or removed if engagement later drops back below the threshold. The
# label reflects peak engagement; demotion, if ever needed, is manual.
name: Auto-label issues and PRs

on:
  issues:
    types: [opened]
  pull_request_target:
    types: [opened]
  issue_comment:
    types: [created]
  schedule:
    # Daily at 06:00 UTC — catches priority transitions driven by
    # reactions, which don't fire webhook events.
    - cron: "0 6 * * *"
  workflow_dispatch:
    inputs:
      target:
        description: "Issue/PR number to label (leave blank to backfill all unlabeled open items)"
        required: false
        type: string
      dry_run:
        description: "Log decisions without applying labels (recommended for spot-testing)"
        required: false
        type: boolean
        default: true
      mode:
        description: "all = LLM classification + priority refresh. priority-only = skip the LLM (no Anthropic call)."
        required: false
        type: choice
        options:
          - all
          - priority-only
        default: all

permissions:
  contents: read
  issues: write
  # Required for labels on PRs. GITHUB_TOKEN's label-write scope is
  # checked per resource type — `gh issue edit --add-label` against a
  # PR number routes through the issues API but the permission check is
  # against the underlying pull_request resource.
  pull-requests: write

jobs:
  label:
    runs-on: ubuntu-latest
    # Skip workflow-bot comments to avoid self-triggering loops.
    if: github.event_name != 'issue_comment' || github.event.comment.user.type != 'Bot'
    concurrency:
      group: auto-label-${{ github.event.issue.number || github.event.pull_request.number || inputs.target || github.event_name }}
      cancel-in-progress: false
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Resolve items, dry-run, and mode
        id: resolve
        env:
          TARGET_INPUT: ${{ inputs.target }}
          DRY_RUN_INPUT: ${{ inputs.dry_run }}
          MODE_INPUT: ${{ inputs.mode }}
        run: |
          set -euo pipefail
          case "${{ github.event_name }}" in
            issues)
              echo "items=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
              echo "dry_run=false" >> "$GITHUB_OUTPUT"
              echo "mode=all" >> "$GITHUB_OUTPUT"
              ;;
            pull_request_target)
              echo "items=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
              echo "dry_run=false" >> "$GITHUB_OUTPUT"
              echo "mode=all" >> "$GITHUB_OUTPUT"
              ;;
            issue_comment)
              # Engagement event — refresh priority labels only. The LLM
              # already classified this item when it was opened; running
              # it again on every comment would burn tokens with no new
              # signal.
              echo "items=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
              echo "dry_run=false" >> "$GITHUB_OUTPUT"
              echo "mode=priority-only" >> "$GITHUB_OUTPUT"
              ;;
            schedule)
              # Daily sweep: re-evaluate priority across every open item
              # to pick up reaction-only changes since the last run.
              ITEMS=$(gh api --paginate \
                "/repos/${{ github.repository }}/issues?state=open&per_page=100" \
                --jq '.[].number' | xargs)
              echo "items=$ITEMS" >> "$GITHUB_OUTPUT"
              echo "dry_run=false" >> "$GITHUB_OUTPUT"
              echo "mode=priority-only" >> "$GITHUB_OUTPUT"
              ;;
            workflow_dispatch)
              if [ -n "$TARGET_INPUT" ]; then
                if ! [[ "$TARGET_INPUT" =~ ^[0-9]+$ ]]; then
                  echo "::error::target must be a positive integer (got: $TARGET_INPUT)"
                  exit 1
                fi
                echo "items=$TARGET_INPUT" >> "$GITHUB_OUTPUT"
              else
                # Backfill: every open issue + PR missing both engine::*
                # and area::*. /repos/.../issues includes PRs because PRs
                # are issues in the API model.
                ITEMS=$(gh api --paginate \
                  "/repos/${{ github.repository }}/issues?state=open&per_page=100" \
                  --jq '.[] | select((.labels | map(.name) | any(startswith("engine::") or startswith("area::"))) | not) | .number' \
                  | xargs)
                echo "items=$ITEMS" >> "$GITHUB_OUTPUT"
              fi
              echo "dry_run=$DRY_RUN_INPUT" >> "$GITHUB_OUTPUT"
              echo "mode=$MODE_INPUT" >> "$GITHUB_OUTPUT"
              ;;
          esac

      - name: Run auto-labeler (LLM + priority)
        if: steps.resolve.outputs.mode == 'all'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          set -euo pipefail
          ITEMS="${{ steps.resolve.outputs.items }}"
          if [ -z "$ITEMS" ]; then
            echo "No items to label."
            exit 0
          fi
          if [ "${{ steps.resolve.outputs.dry_run }}" = "true" ]; then
            python .github/scripts/auto_label.py $ITEMS --dry-run
          else
            python .github/scripts/auto_label.py $ITEMS
          fi

      - name: Run auto-labeler (priority only)
        if: steps.resolve.outputs.mode == 'priority-only'
        run: |
          set -euo pipefail
          ITEMS="${{ steps.resolve.outputs.items }}"
          if [ -z "$ITEMS" ]; then
            echo "No items to refresh."
            exit 0
          fi
          if [ "${{ steps.resolve.outputs.dry_run }}" = "true" ]; then
            python .github/scripts/auto_label.py $ITEMS --priority-only --dry-run
          else
            python .github/scripts/auto_label.py $ITEMS --priority-only
          fi
