# VERY IMPORTANT:
# This workflow has a lot of permissions!
# It should ONLY run on trusted maintainers code (e.g. wwwillchen)
name: PR Review Responder

on:
  workflow_run:
    workflows: ["CI"]
    # Regardless of success or fail, we want to run this workflow.
    # Why?
    # If it's failure, we want to fix the errors.
    # If it's success, we want to address the PR review comments from the AI code reviewers.
    #
    # The CI workflow is almost always the last workflow to finish, so that's why we wait for it.
    types: [completed]

jobs:
  respond-to-pr:
    environment: ai-bots
    runs-on: ubuntu-latest
    permissions:
      actions: write
      contents: write
      pull-requests: write
    steps:
      - name: Get PR info and check labels
        id: pr-info
        uses: actions/github-script@v7
        with:
          script: |
            const run = context.payload.workflow_run;

            // Get PR associated with this workflow run
            // Note: run.pull_requests can be empty for cross-repo PRs (forks)
            // So we also search by head SHA as a fallback
            let prNumber;

            if (run.pull_requests && run.pull_requests.length > 0) {
              prNumber = run.pull_requests[0].number;
            } else {
              // Search for PRs with matching head branch
              // Note: head_repository can be null if the fork was deleted
              if (!run.head_repository) {
                console.log('Head repository not available');
                core.setOutput('should_continue', 'false');
                return;
              }
              const { data: prs } = await github.rest.pulls.list({
                owner: context.repo.owner,
                repo: context.repo.repo,
                state: 'open',
                head: `${run.head_repository.owner.login}:${run.head_branch}`
              });

              if (prs.length === 0) {
                console.log('No pull requests found for this workflow run');
                core.setOutput('should_continue', 'false');
                return;
              }
              prNumber = prs[0].number;
            }

            // Fetch full PR details to get labels
            const { data: pr } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: prNumber
            });

            // Only allow wwwillchen and wwwillchen-bot to use this workflow
            if (pr.user.login !== 'wwwillchen' && pr.user.login !== 'wwwillchen-bot') {
              console.log(`PR #${prNumber} author ${pr.user.login} is not allowed to use this workflow`);
              core.setOutput('should_continue', 'false');
              return;
            }

            // Check for cc:request (initial) or cc:request:N (re-request) labels
            // If multiple labels exist, use the highest count to avoid resetting the retry counter
            let requestCount = -1;
            let currentLabel = '';

            for (const label of pr.labels) {
              if (label.name === 'cc:request') {
                if (requestCount < 0) {
                  requestCount = 0;
                  currentLabel = 'cc:request';
                }
              }
              const match = label.name.match(/^cc:request:(\d+)$/);
              if (match) {
                const count = parseInt(match[1], 10);
                if (count > requestCount) {
                  requestCount = count;
                  currentLabel = label.name;
                }
              }
            }

            if (requestCount === -1) {
              console.log(`PR #${prNumber} does not have a cc:request or cc:request:N label`);
              core.setOutput('should_continue', 'false');
              return;
            }

            // Guard: do not loop more than 3 retries (4 total runs: initial + 3 retries)
            // cc:request (0) -> cc:request:1 (1) -> cc:request:2 (2) -> cc:request:3 (3) -> blocked
            if (requestCount >= 4) {
              console.log(`PR #${prNumber} has reached max retry count (${requestCount}), adding cc:needs-human-review`);

              // Ensure the label exists
              try {
                await github.rest.issues.createLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  name: 'cc:needs-human-review',
                  color: 'd93f0b',
                  description: 'Claude Code has exhausted auto-retries; needs human review'
                });
              } catch (e) {
                // Label already exists, ignore
              }

              // Remove current label, cc:pending, and add needs-human-review
              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                name: currentLabel
              }).catch(() => {});

              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                name: 'cc:pending'
              }).catch(() => {});

              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                labels: ['cc:needs-human-review']
              });

              core.setOutput('should_continue', 'false');
              return;
            }

            console.log(`PR #${prNumber} has ${currentLabel} label (count=${requestCount}), proceeding with pr-fix`);
            core.setOutput('pr_number', prNumber);
            core.setOutput('should_continue', 'true');
            core.setOutput('request_count', requestCount);
            core.setOutput('current_label', currentLabel);

      - name: Checkout repository
        if: steps.pr-info.outputs.should_continue == 'true'
        uses: actions/checkout@v5
        with:
          # Security note: The pr.user.login check verifies who opened the PR, but this
          # checks out code from whoever last pushed. If "Allow edits from maintainers" is
          # enabled, other maintainers could push to the branch. This is acceptable because
          # maintainers already have write access to the repo and are trusted.
          repository: ${{ github.event.workflow_run.head_repository.full_name }}
          ref: ${{ github.event.workflow_run.head_branch }}
          fetch-depth: 0

      - name: Record HEAD before Claude Code
        if: steps.pr-info.outputs.should_continue == 'true'
        id: before-claude
        run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT

      - name: Update labels to pending
        if: steps.pr-info.outputs.should_continue == 'true'
        run: |
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "${{ steps.pr-info.outputs.current_label }}" --add-label "cc:pending"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Run PR Fix
        if: steps.pr-info.outputs.should_continue == 'true'
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          prompt: |
            /dyad:pr-fix ${{ steps.pr-info.outputs.pr_number }}

      - name: Re-trigger workflows if commits were pushed
        # Use always() to ensure commits get tested even if Claude Code fails partway through
        if: steps.pr-info.outputs.should_continue == 'true' && always()
        id: retrigger
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Fetch using the PR's head ref to handle both fork and same-repo PRs correctly
          # Use the GitHub API to get the current head SHA, avoiding race conditions with git fetch
          PR_HEAD_SHA=$(gh pr view ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --json headRefOid --jq '.headRefOid')
          if [ -z "$PR_HEAD_SHA" ]; then
            echo "Failed to get PR head SHA, falling back to git fetch"
            git fetch origin ${{ github.event.workflow_run.head_branch }} || git fetch origin
            PR_HEAD_SHA=$(git rev-parse FETCH_HEAD 2>/dev/null || git rev-parse origin/${{ github.event.workflow_run.head_branch }})
          fi

          if [ "${{ steps.before-claude.outputs.sha }}" != "$PR_HEAD_SHA" ]; then
            echo "Claude pushed new commits (before: ${{ steps.before-claude.outputs.sha }}, after: $PR_HEAD_SHA)"
            echo "Re-triggering workflows via workflow_dispatch"
            echo "commits_pushed=true" >> $GITHUB_OUTPUT
            # Use workflow_dispatch to trigger workflows - this works with GITHUB_TOKEN unlike PR events
            # which are blocked to prevent infinite loops
            #
            # Note: We use the default branch (no --ref) instead of head_branch because for fork PRs,
            # head_branch only exists in the fork repo, not the main repo. The workflows will checkout
            # the correct PR code using the pr_number input.

            # Trigger CI
            gh workflow run ci.yml \
              --repo ${{ github.repository }} \
              -f pr_number=${{ steps.pr-info.outputs.pr_number }} \
              || echo "::warning::Failed to trigger CI workflow"

            # Trigger BugBot
            gh workflow run bugbot-trigger.yml \
              --repo ${{ github.repository }} \
              -f pr_number=${{ steps.pr-info.outputs.pr_number }} \
              || echo "::warning::Failed to trigger BugBot workflow"

            # Trigger Claude PR Review
            gh workflow run claude-pr-review.yml \
              --repo ${{ github.repository }} \
              -f pr_number=${{ steps.pr-info.outputs.pr_number }} \
              || echo "::warning::Failed to trigger Claude PR Review workflow"
          else
            echo "No new commits pushed, skipping workflow re-triggers"
            echo "commits_pushed=false" >> $GITHUB_OUTPUT
          fi

      - name: Ensure next request label exists
        # Use always() so retry loop continues even if Claude Code fails after pushing commits
        if: steps.pr-info.outputs.should_continue == 'true' && always() && steps.retrigger.outputs.commits_pushed == 'true'
        uses: actions/github-script@v7
        with:
          script: |
            const nextCount = parseInt('${{ steps.pr-info.outputs.request_count }}', 10) + 1;
            const labelName = `cc:request:${nextCount}`;
            try {
              await github.rest.issues.createLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                name: labelName,
                color: '1d76db',
                description: `Claude Code auto-re-request iteration ${nextCount}`
              });
            } catch (e) {
              // Label already exists, ignore
            }

      - name: Update labels - re-request review (commits pushed)
        # Use always() so retry loop continues even if Claude Code fails after pushing commits
        if: steps.pr-info.outputs.should_continue == 'true' && always() && steps.retrigger.outputs.commits_pushed == 'true'
        run: |
          REQUEST_COUNT="${{ steps.pr-info.outputs.request_count }}"
          NEXT_COUNT=$(( ${REQUEST_COUNT:-0} + 1 ))
          echo "Setting cc:request:${NEXT_COUNT} to auto-re-request review (retry ${NEXT_COUNT} of 3)"
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "cc:pending" --add-label "cc:request:${NEXT_COUNT}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Update labels to done (no new commits)
        if: steps.pr-info.outputs.should_continue == 'true' && success() && steps.retrigger.outputs.commits_pushed == 'false'
        run: |
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "cc:pending" --add-label "cc:done"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Update labels to failed
        if: steps.pr-info.outputs.should_continue == 'true' && failure()
        run: |
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "cc:pending" --add-label "cc:failed"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
