name: Claude Code PR Review
on:
  pull_request:
    types: [opened, ready_for_review, reopened, synchronize]
  workflow_call:
    inputs:
      pr_number:
        required: true
        type: number
      effort:
        required: false
        type: string
        default: medium

concurrency:
  # PR number lives in different fields depending on entry point — coalesce so
  # the same PR shares a group regardless of whether we got here via
  # pull_request or via the /bot-review proxy.
  group: claude-code-review-${{ inputs.pr_number || github.event.pull_request.number }}-${{ github.event.action || 'manual' }}
  cancel-in-progress: true

jobs:
  review-with-tracking:
    # On `pull_request`, skip draft PRs — they get noisy synchronize pushes
    # while WIP, and running a full Claude review on every intermediate commit
    # gets expensive fast. ready_for_review picks it up on draft exit.
    # On `workflow_call`, trust the caller; the proxy workflow handles its own
    # auth gates.
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      id-token: write
    env:
      # Same coalesce as the concurrency group above (which can't read `env`,
      # so the expression appears twice).
      PR_NUMBER: ${{ inputs.pr_number || github.event.pull_request.number }}
    steps:
      - name: React to trigger comment
        # Quick ACK so the user who typed /bot-review or /bot-deep-review sees
        # the bot picked up the command — actual review takes a few minutes.
        if: github.event_name == 'issue_comment'
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          COMMENT_ID: ${{ github.event.comment.id }}
        run: |
          gh api --method POST \
            "repos/$REPO/issues/comments/$COMMENT_ID/reactions" \
            -f content=eyes \
            --silent

      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          # A fork PR's head branch doesn't exist in the base repo, so checking it
          # out by branch name fails. GitHub mirrors every PR head into the base
          # repo as refs/pull/<N>/head — readable with the workflow token
          ref: refs/pull/${{ env.PR_NUMBER }}/head
          fetch-depth: 1
          persist-credentials: false

      - name: Configure gh aliases for review-thread operations
        run: bash .github/actions/setup-claude-code-review.sh

      - name: Review
        id: review
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          track_progress: true
          # Allow PRs opened by coding-agent bots (e.g. cursor) to trigger reviews.
          allowed_bots: "cursor[bot]"
          prompt: |
            REPO: ${{ github.repository }}
            PR NUMBER: ${{ env.PR_NUMBER }}

            Perform a comprehensive code review with the following focus areas:

            1. **Code Quality**
               - Clean code principles and best practices
               - Proper error handling and edge cases
               - Code readability and maintainability

            2. **Security**
               - Check for potential security vulnerabilities
               - Validate input sanitization
               - Review authentication/authorization logic

            3. **Performance**
               - Identify potential performance bottlenecks
               - Review database queries for efficiency
               - Check for memory leaks or resource issues

            4. **Testing**
               - Verify adequate test coverage
               - Review test quality and edge cases
               - Check for missing test scenarios

            5. **Documentation**
               - Ensure code is properly documented
               - Verify README updates for new features
               - Check API documentation accuracy

            6. **Comments**
               - An explanatory comment is **3 lines max**. Flag a longer one and ask for the
                 load-bearing sentence, unless the reason genuinely cannot be stated shorter
               - A comment earns its place only when its absence would let a later edit
                 reintroduce a bug. Flag one a reader would lose nothing by deleting: a
                 restatement of the code under it, a banner over self-describing code,
                 narration of the change or of the review round that produced it, or a JSDoc
                 block whose tags only re-spell already-typed names
               - Prefer making the code carry the meaning — a named constant, a named
                 intermediate value, an extracted function whose name states the intent
               - Never raise this to ask for a comment to be **added**, and skip generated
                 files and comments another rule or tool requires

            Provide detailed feedback using inline comments for specific issues.
            Use top-level comments for general observations or praise.

            **Tracking comment formatting:**

            Applies only to the FINAL update of the tracking comment (the one that contains
            the completed review). While work is still in progress — todos partially checked,
            "working…" spinner, intermediate status updates — keep everything visible so the
            reader can watch progress without expanding anything.

            On the final update, hide EVERYTHING — including the todo checklist itself —
            inside a single collapsed `<details>` block. The completed checklist is just
            stale progress signal at that point; it belongs behind the spoiler alongside the
            rest of the review. Only a short one-line headline with the verdict and issue
            counts (e.g. "1 high, 2 medium, 5 low") stays visible outside the spoiler — the
            reader expands when they want details.

            Leave a blank line after `<summary>` and before `</details>`, otherwise GitHub
            won't render the markdown inside (tables and lists especially).

            **Resolving your own stale review threads:**

            Before posting new comments, list existing review threads on this PR using the
            preconfigured alias:

              gh list-review-threads ${{ github.repository_owner }} ${{ github.event.repository.name }} ${{ env.PR_NUMBER }}

            For each thread where ALL of the following hold:
              - `isResolved` is false
              - the first comment's `author.login` is `claude`
              - the concern is no longer applicable in the current diff (file/line gone,
                code rewritten, issue addressed)

            resolve it with:

              gh resolve-thread <thread-id>

            Do not resolve threads from human reviewers under any circumstance, even if the
            concern looks addressed — leave that decision to the reviewer. Only the two
            aliases above are available; raw `gh api graphql` is not permitted.

            **Avoiding duplicate inline comments:**

            Use the same `gh list-review-threads` output (from the resolve step above) to
            deduplicate against your own prior comments. Before calling
            `mcp__github_inline_comment__create_inline_comment` for a new issue, check whether
            an existing thread already covers it. Skip creating a new inline comment when ALL
            of the following hold for any thread in the list:
              - `isResolved` is false
              - the first comment's `author.login` is `claude`
              - the thread is on the same `path` and `line` as the new issue you would post
              - the existing comment's body raises substantively the same concern (same root
                cause, same fix direction — wording does not need to match)

            When you skip, briefly note it in your top-level summary (e.g. "Re-affirmed N
            prior threads still apply") so the reader knows you considered the issue. Do not
            post a "still applies" reply on the thread — silence is fine; the unresolved
            state already communicates that.

            If the issue is on a different line or the fix direction has shifted (different
            root cause), post a new inline comment as usual.

          # Tools for comprehensive PR review
          claude_args: |
            --allowedTools "mcp__github_inline_comment__create_inline_comment,mcp__github__get_issue_comments,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh list-review-threads:*),Bash(gh resolve-thread:*)"
            --model "claude-opus-5"
            --effort ${{ inputs.effort || 'medium' }}

      - name: Write run summary
        if: always() && steps.review.outputs.execution_file != ''
        env:
          EXECUTION_FILE: ${{ steps.review.outputs.execution_file }}
        run: |
          jq -r '
            [.[] | select(.type == "result")] | last as $r |
            ([.[] | select(.type == "assistant")] | length) as $turns |
            ([.[] | select(.type == "assistant") | .message.usage.input_tokens // 0] | add) as $in_tok |
            ([.[] | select(.type == "assistant") | .message.usage.output_tokens // 0] | add) as $out_tok |
            ([.[] | select(.type == "assistant") | .message.usage.cache_read_input_tokens // 0] | add) as $cache_tok |
            ([.[] | select(.type == "assistant") | .message.model] | last) as $model |
            if $r then
              "### Claude Code review run",
              "| Metric | Value |",
              "|---|---|",
              "| Model | `\($model // "?")` |",
              "| Cost | $\($r.total_cost_usd) |",
              "| Wall time | \(($r.duration_ms / 1000) | floor)s |",
              "| API time | \((($r.duration_api_ms // 0) / 1000) | floor)s |",
              "| Turns | \($turns) |",
              "| Input tokens | \($in_tok) |",
              "| Output tokens | \($out_tok) |",
              "| Cache read tokens | \($cache_tok) |"
            else empty end
          ' "$EXECUTION_FILE" >> "$GITHUB_STEP_SUMMARY"

      - name: React to trigger comment (completion)
        if: always() && github.event_name == 'issue_comment'
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          COMMENT_ID: ${{ github.event.comment.id }}
          REVIEW_OUTCOME: ${{ steps.review.outcome }}
        run: |
          if [ "$REVIEW_OUTCOME" = "success" ]; then
            reaction=rocket
          else
            reaction=confused
          fi
          gh api --method POST \
            "repos/$REPO/issues/comments/$COMMENT_ID/reactions" \
            -f "content=$reaction" \
            --silent
