name: background_agent_mvp

on:
  schedule:
    - cron: "0 16 * * 1-5"
  workflow_dispatch:
    inputs:
      crash_ids:
        description: "Optional comma-separated Sentry issue IDs (e.g. ZED-4VS,ZED-123)"
        required: false
        type: string
      reviewers:
        description: "Optional comma-separated GitHub reviewer handles"
        required: false
        type: string
      top:
        description: "Top N candidates when crash_ids is empty"
        required: false
        type: string
        default: "3"

permissions:
  contents: write
  pull-requests: write

env:
  FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
  DROID_MODEL: claude-opus-4-5-20251101
  SENTRY_ORG: zed-dev

jobs:
  run-mvp:
    runs-on: ubuntu-latest
    timeout-minutes: 180

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

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

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

      - name: Resolve reviewers
        id: reviewers
        env:
          INPUT_REVIEWERS: ${{ inputs.reviewers }}
          DEFAULT_REVIEWERS: ${{ vars.BACKGROUND_AGENT_REVIEWERS }}
        run: |
          set -euo pipefail
          if [ -z "$DEFAULT_REVIEWERS" ]; then
            DEFAULT_REVIEWERS="eholk,morgankrey,osiewicz,bennetbo"
          fi
          REVIEWERS="${INPUT_REVIEWERS:-$DEFAULT_REVIEWERS}"
          REVIEWERS="$(echo "$REVIEWERS" | tr -d '[:space:]')"
          echo "reviewers=$REVIEWERS" >> "$GITHUB_OUTPUT"

      - name: Select crash candidates
        id: candidates
        env:
          INPUT_CRASH_IDS: ${{ inputs.crash_ids }}
          INPUT_TOP: ${{ inputs.top }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_BACKGROUND_AGENT_MVP_TOKEN }}
        run: |
          set -euo pipefail

          PREFETCH_DIR="/tmp/crash-data"
          ARGS=(--select-only --prefetch-dir "$PREFETCH_DIR" --org "$SENTRY_ORG")
          if [ -n "$INPUT_CRASH_IDS" ]; then
            ARGS+=(--crash-ids "$INPUT_CRASH_IDS")
          else
            TARGET_DRAFT_PRS="${INPUT_TOP:-3}"
            if ! [[ "$TARGET_DRAFT_PRS" =~ ^[0-9]+$ ]] || [ "$TARGET_DRAFT_PRS" -lt 1 ]; then
              TARGET_DRAFT_PRS="3"
            fi
            CANDIDATE_TOP=$((TARGET_DRAFT_PRS * 5))
            if [ "$CANDIDATE_TOP" -gt 100 ]; then
              CANDIDATE_TOP=100
            fi
            ARGS+=(--top "$CANDIDATE_TOP" --sample-size 100)
          fi

          IDS="$(python3 script/run-background-agent-mvp-local "${ARGS[@]}")"

          if [ -z "$IDS" ]; then
            echo "No candidates selected"
            exit 1
          fi

          echo "Using crash IDs: $IDS"
          echo "ids=$IDS" >> "$GITHUB_OUTPUT"

      - name: Run background agent pipeline per crash
        id: pipeline
        env:
          GH_TOKEN: ${{ github.token }}
          REVIEWERS: ${{ steps.reviewers.outputs.reviewers }}
          CRASH_IDS: ${{ steps.candidates.outputs.ids }}
          TARGET_DRAFT_PRS_INPUT: ${{ inputs.top }}
        run: |
          set -euo pipefail

          git config user.name "factory-droid[bot]"
          git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"

          # Crash ID format validation regex
          CRASH_ID_PATTERN='^[A-Za-z0-9]+-[A-Za-z0-9]+$'
          TARGET_DRAFT_PRS="${TARGET_DRAFT_PRS_INPUT:-3}"
          if ! [[ "$TARGET_DRAFT_PRS" =~ ^[0-9]+$ ]] || [ "$TARGET_DRAFT_PRS" -lt 1 ]; then
            TARGET_DRAFT_PRS="3"
          fi
          CREATED_DRAFT_PRS=0

          IFS=',' read -r -a CRASH_ID_ARRAY <<< "$CRASH_IDS"

          for CRASH_ID in "${CRASH_ID_ARRAY[@]}"; do
            if [ "$CREATED_DRAFT_PRS" -ge "$TARGET_DRAFT_PRS" ]; then
              echo "Reached target draft PR count ($TARGET_DRAFT_PRS), stopping candidate processing"
              break
            fi

            CRASH_ID="$(echo "$CRASH_ID" | xargs)"
            [ -z "$CRASH_ID" ] && continue

            # Validate crash ID format to prevent injection via branch names or prompts
            if ! [[ "$CRASH_ID" =~ $CRASH_ID_PATTERN ]]; then
              echo "ERROR: Invalid crash ID format: '$CRASH_ID' — skipping"
              continue
            fi

            BRANCH="background-agent/mvp-${CRASH_ID,,}-$(date +%Y%m%d)"
            echo "Running crash pipeline for $CRASH_ID on $BRANCH"

            # Deduplication: skip if a draft PR already exists for this crash
            EXISTING_BRANCH_PR="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' || echo "")"
            if [ -n "$EXISTING_BRANCH_PR" ]; then
              echo "Draft PR #$EXISTING_BRANCH_PR already exists for $CRASH_ID — skipping"
              continue
            fi

            git config --local http.https://github.com/.extraheader "AUTHORIZATION: bearer ${GH_TOKEN}"

            if ! git fetch origin main; then
              echo "WARNING: Failed to fetch origin/main for $CRASH_ID — skipping"
              continue
            fi

            if ! git checkout -B "$BRANCH" origin/main; then
              echo "WARNING: Failed to create checkout branch $BRANCH for $CRASH_ID — skipping"
              continue
            fi

            CRASH_DATA_FILE="/tmp/crash-data/crash-${CRASH_ID}.md"
            if [ ! -f "$CRASH_DATA_FILE" ]; then
              echo "WARNING: No pre-fetched crash data for $CRASH_ID at $CRASH_DATA_FILE — skipping"
              continue
            fi

            python3 -c "
          import sys
          crash_id, data_file = sys.argv[1], sys.argv[2]
          prompt = f'''You are running the weekly background crash-fix MVP pipeline for crash {crash_id}.

          The crash report has been pre-fetched and is available at: {data_file}
          Read this file to get the crash data. Do not call script/sentry-fetch.

          Required workflow:
          1. Read the crash report from {data_file}
          2. Read and follow .rules.
          3. Follow .factory/prompts/crash/investigate.md and write ANALYSIS.md
          4. Follow .factory/prompts/crash/link-issues.md and write LINKED_ISSUES.md
          5. Follow .factory/prompts/crash/fix.md to implement a minimal fix with tests
          6. Run validators required by the fix prompt for the affected code paths
          7. Write PR_BODY.md with sections:
             - Crash Summary
             - Root Cause
             - Fix
             - Validation
             - Potentially Related Issues (High/Medium/Low from LINKED_ISSUES.md)
             - Reviewer Checklist
             - Release Notes (final section; format as Release Notes:, then a blank line, then one bullet like - N/A)

          Constraints:
          - Do not merge or auto-approve.
          - Keep changes narrowly scoped to this crash.
          - Do not modify files in .github/, .factory/, or script/ directories.
          - When investigating git history, limit your search to the last 2 weeks of commits. Do not traverse older history.
          - If the crash is not solvable with available context, write a clear blocker summary to PR_BODY.md.
          '''
          import textwrap
          with open('/tmp/background-agent-prompt.md', 'w') as f:
              f.write(textwrap.dedent(prompt))
          " "$CRASH_ID" "$CRASH_DATA_FILE"

            if ! "$DROID_BIN" exec --auto medium -m "$DROID_MODEL" -f /tmp/background-agent-prompt.md; then
              echo "Droid execution failed for $CRASH_ID, continuing to next candidate"
              continue
            fi

            for REPORT_FILE in ANALYSIS.md LINKED_ISSUES.md PR_BODY.md; do
              if [ -f "$REPORT_FILE" ]; then
                echo "::group::${CRASH_ID} ${REPORT_FILE}"
                cat "$REPORT_FILE"
                echo "::endgroup::"
              fi
            done

            if git diff --quiet; then
              echo "No code changes produced for $CRASH_ID"
              continue
            fi

            # Stage only expected file types — not git add -A
            git add -- '*.rs' '*.toml' 'Cargo.lock' 'ANALYSIS.md' 'LINKED_ISSUES.md' 'PR_BODY.md'

            # Reject changes to protected paths
            PROTECTED_CHANGES="$(git diff --cached --name-only | grep -E '^(\.github/|\.factory/|script/)' || true)"
            if [ -n "$PROTECTED_CHANGES" ]; then
              echo "ERROR: Agent modified protected paths — aborting commit for $CRASH_ID:"
              echo "$PROTECTED_CHANGES"
              git reset HEAD -- .
              continue
            fi

            if ! git diff --cached --quiet; then
              git commit -m "Fix crash ${CRASH_ID}"
            fi

            git push -u origin "$BRANCH"

            CRATE_PREFIX=""
            CHANGED_CRATES="$(git diff --cached --name-only | awk -F/ '/^crates\/[^/]+\// {print $2}' | sort -u)"
            if [ -n "$CHANGED_CRATES" ] && [ "$(printf "%s\n" "$CHANGED_CRATES" | wc -l | tr -d ' ')" -eq 1 ]; then
              CRATE_PREFIX="${CHANGED_CRATES}: "
            fi

            TITLE="${CRATE_PREFIX}Fix crash ${CRASH_ID}"
            BODY_FILE="PR_BODY.md"
            if [ ! -f "$BODY_FILE" ]; then
              BODY_FILE="/tmp/pr-body-${CRASH_ID}.md"
              printf "Automated draft crash-fix pipeline output for %s.\n\nNo PR_BODY.md was generated by the agent; please review commit and linked artifacts manually.\n" "$CRASH_ID" > "$BODY_FILE"
            fi

            python3 -c '
            import re
            import sys

            path = sys.argv[1]
            body = open(path, encoding="utf-8").read()
            pattern = re.compile(r"(^|\n)Release Notes:\r?\n(?:\r?\n)*(?P<bullets>(?:\s*-\s+.*(?:\r?\n|$))+)", re.MULTILINE)
            match = pattern.search(body)

            if match:
                bullets = [
                    re.sub(r"^\s*", "", bullet)
                    for bullet in re.findall(r"^\s*-\s+.*$", match.group("bullets"), re.MULTILINE)
                ]
                if not bullets:
                    bullets = ["- N/A"]
                section = "Release Notes:\n\n" + "\n".join(bullets)
                body_without_release_notes = (body[: match.start()] + body[match.end() :]).rstrip()
                if body_without_release_notes:
                    normalized_body = f"{body_without_release_notes}\n\n{section}\n"
                else:
                    normalized_body = f"{section}\n"
            else:
                normalized_body = body.rstrip() + "\n\nRelease Notes:\n\n- N/A\n"

            with open(path, "w", encoding="utf-8") as file:
                file.write(normalized_body)
            ' "$BODY_FILE"

            EXISTING_PR="$(gh pr list --head "$BRANCH" --json number --jq '.[0].number')"
            if [ -n "$EXISTING_PR" ]; then
              gh pr edit "$EXISTING_PR" --title "$TITLE" --body-file "$BODY_FILE"
              PR_NUMBER="$EXISTING_PR"
            else
              PR_URL="$(gh pr create --draft --base main --head "$BRANCH" --title "$TITLE" --body-file "$BODY_FILE")"
              PR_NUMBER="$(basename "$PR_URL")"
            fi

            if [ -n "$REVIEWERS" ]; then
              IFS=',' read -r -a REVIEWER_ARRAY <<< "$REVIEWERS"
              for REVIEWER in "${REVIEWER_ARRAY[@]}"; do
                [ -z "$REVIEWER" ] && continue
                gh pr edit "$PR_NUMBER" --add-reviewer "$REVIEWER" || true
              done
            fi

            CREATED_DRAFT_PRS=$((CREATED_DRAFT_PRS + 1))
            echo "Created/updated draft PRs this run: $CREATED_DRAFT_PRS/$TARGET_DRAFT_PRS"
          done

          echo "created_draft_prs=$CREATED_DRAFT_PRS" >> "$GITHUB_OUTPUT"
          echo "target_draft_prs=$TARGET_DRAFT_PRS" >> "$GITHUB_OUTPUT"

      - name: Cleanup pre-fetched crash data
        if: always()
        run: rm -rf /tmp/crash-data

      - name: Workflow summary
        if: always()
        env:
          SUMMARY_CRASH_IDS: ${{ steps.candidates.outputs.ids }}
          SUMMARY_REVIEWERS: ${{ steps.reviewers.outputs.reviewers }}
          SUMMARY_CREATED_DRAFT_PRS: ${{ steps.pipeline.outputs.created_draft_prs }}
          SUMMARY_TARGET_DRAFT_PRS: ${{ steps.pipeline.outputs.target_draft_prs }}
        run: |
          {
            echo "## Background Agent MVP"
            echo ""
            echo "- Crash IDs: ${SUMMARY_CRASH_IDS:-none}"
            echo "- Reviewer routing: ${SUMMARY_REVIEWERS:-NOT CONFIGURED}"
            echo "- Draft PRs created: ${SUMMARY_CREATED_DRAFT_PRS:-0}/${SUMMARY_TARGET_DRAFT_PRS:-3}"
            echo "- Pipeline: investigate -> link-issues -> fix -> draft PR"
          } >> "$GITHUB_STEP_SUMMARY"

concurrency:
  group: background-agent-mvp
  cancel-in-progress: false
