name: Droid Code Review

on:
  issue_comment:
    types: [created]

concurrency:
  group: droid-review-${{ github.event.issue.number }}
  cancel-in-progress: true

permissions:
  pull-requests: write
  contents: read
  issues: write

jobs:
  code-review:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    # Only run on PR comments that contain "@droid review" from authorized users
    if: |
      github.repository == 'rspamd/rspamd' &&
      github.event.issue.pull_request &&
      contains(github.event.comment.body, '@droid review') &&
      (
        github.event.comment.user.login == 'vstakhov' ||
        github.event.comment.user.login == 'moisseev' ||
        github.event.comment.user.login == 'fatalbanana'
      )

    steps:
      - name: Acknowledge review start with reaction
        run: |
          gh api \
            --method POST \
            -H "Accept: application/vnd.github+json" \
            /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
            -f content='eyes'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Checkout PR branch
        run: |
          gh pr checkout ${{ github.event.issue.number }}
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Install Droid CLI
        run: |
          curl -fsSL https://app.factory.ai/cli | sh
          echo "$HOME/.local/bin" >> $GITHUB_PATH
          "$HOME/.local/bin/droid" --version

      - name: Configure git identity
        run: |
          git config user.name "Droid Code Reviewer"
          git config user.email "droid@factory.ai"

      - name: Perform automated code review
        env:
          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          cat > prompt.txt << 'EOF'
          You are a conservative automated code reviewer for the Rspamd mail filtering project.
          Your goal is high-precision, low-noise feedback: report only defects you are certain about.

          ## Step 1 — Gather full context before writing any comment

          Fetch the complete PR context from:
          https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number }}

          Read carefully:
          - PR title, description, and the author's stated intent
          - The complete unified diff with diff-position line numbers
          - All existing review threads, including every reply
          - Any maintainer explanations (vstakhov, moisseev, fatalbanana)

          Do NOT write any comment until you have read all existing threads.

          ## Step 2 — Duplicate and dismissed-feedback filter (mandatory)

          Before drafting a comment, check:
          1. Has this exact issue already been raised in an existing thread?
          2. Has a maintainer replied to that thread with an explanation, marked it resolved,
             or indicated the behaviour is intentional or by design?

          If either condition is true, SKIP the issue entirely. Do not re-raise dismissed or
          accepted concerns under any circumstances, even if you still think there is a problem.

          ## Step 3 — Defect qualification (only report findings that pass all criteria)

          Only report a finding if ALL of the following are true:
          - It is a concrete, demonstrable defect in the changed lines, or a pre-existing bug
            that the new code newly exposes or makes reachable
          - You can explain exactly why it is wrong (not merely unusual or unfamiliar)
          - It has a clear, identifiable impact: crash, data loss, memory leak, security
            vulnerability, silent incorrect behaviour, or definite API misuse
          - A specific, actionable fix exists

          Reject any finding that is:
          - A style, formatting, or naming preference (clang-format / luacheck own this)
          - A subjective architectural opinion
          - Speculative ("this might cause…" or "could potentially…")
          - About test coverage, documentation, or minor performance
          - Based on an assumption that has not been verified in the diff

          ## Step 4 — Rspamd-specific rules to check (only on changed lines)

          ### Memory management
          - C code must use rspamd_mempool or g_malloc/g_free — never raw malloc/free
          - Every allocation must have a matching free; check error-path cleanup
          - Verify no use-after-free when mempools are destroyed

          ### C++ containers
          - Never use std::unordered_map or std::unordered_set — use ankerl::unordered_dense

          ### Logging format strings
          - rspamd uses its own printf implementation (see src/libutil/printf.h for the
            full list of format specifiers) — it is NOT standard printf
          - Lua rspamd_logger uses %s for every argument type
          - Format strings must never come from untrusted input

          ### Lua stack discipline
          - Every lua_push* must be balanced; check stack cleanup on all exit paths
          - Validate lua_check_* return values before dereferencing

          ### Security (high-confidence only)
          - Buffer overflows with verifiable out-of-bounds access
          - Integer overflows in size/offset arithmetic feeding an allocation or copy
          - NULL pointer dereferences on unchecked return values
          - Confirmed command/SQL injection in external calls

          ### Error handling
          - Unchecked return values from functions that can fail, where the failure is silently ignored

          ## Step 5 — Comment format (strict)

          Each comment MUST contain exactly three parts:
          1. **Defect**: one sentence describing what is wrong and where
          2. **Impact**: one sentence stating the concrete consequence (crash / leak / wrong result / …)
          3. **Fix**: a specific code-level suggestion

          No emojis. No hedging language ("might", "could", "consider"). Plain technical prose.

          ## Step 6 — Positioning rules

          - Inline comments MUST target a line that is part of the changed diff (added or context lines within the diff hunk).
          - If you cannot identify the exact changed line with certainty, do NOT post an inline comment.
            Post a single top-level summary comment for that finding instead.
          - When in doubt about positioning, always prefer a summary comment over a mispositioned inline comment.

          ## Step 7 — Submission

          Use the GitHub CLI to submit. The PR number is ${{ github.event.issue.number }} and
          repository is ${{ github.repository }}.

          Commands:
          - Summary / approval only:
            gh pr review ${{ github.event.issue.number }} --comment --body "..."
          - Request changes with summary (no inline):
            gh pr review ${{ github.event.issue.number }} --request-changes --body "..."
          - Approve:
            gh pr review ${{ github.event.issue.number }} --approve --body "No actionable defects found."

          Limits:
          - Post at most 5 comments in total; choose the highest-severity findings.
          - If all findings are low-severity (no crash, leak, or security issue) and you have
            fewer than 2, post only an approval comment.
          - A single finding that involves a crash, memory leak, or security vulnerability is
            always worth reporting — do not suppress it.
          - Never post test or exploratory API calls — every comment is immediately visible.
          EOF

          # Run droid exec with the prompt
          echo "Running code review analysis and submitting results..."
          droid exec --auto high --model gpt-5.4 -f prompt.txt

      - name: Post failure notice
        if: ${{ failure() }}
        run: |
          gh pr comment ${{ github.event.issue.number }} --body "⚠️ Automated code review failed to complete. Check the [workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details."
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Dismiss previous change requests if approved
        if: ${{ success() }}
        run: |
          # Get all reviews for this PR
          REVIEWS=$(gh api "/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/reviews" --jq '.[] | select(.state == "CHANGES_REQUESTED" and .user.login == "github-actions[bot]") | .id')

          # Dismiss each change request review
          for REVIEW_ID in $REVIEWS; do
            echo "Dismissing review $REVIEW_ID"
            gh api \
              --method PUT \
              -H "Accept: application/vnd.github+json" \
              "/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/reviews/$REVIEW_ID/dismissals" \
              -f message="Dismissed after new approval" || echo "Failed to dismiss review $REVIEW_ID (may already be dismissed)"
          done
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload debug artifacts on failure
        if: ${{ failure() }}
        uses: actions/upload-artifact@v4
        with:
          name: droid-review-debug-${{ github.run_id }}
          path: |
            prompt.txt
            ${{ runner.home }}/.factory/logs/droid-log-single.log
            ${{ runner.home }}/.factory/logs/console.log
          if-no-files-found: ignore
          retention-days: 7
