name: Documentation Suggestions

# Stable release callout stripping plan (not wired yet):
# 1. Add a separate stable-only workflow trigger on `release.published`
#    with `github.event.release.prerelease == false`.
# 2. In that workflow, run `script/docs-strip-preview-callouts` on `main`.
# 3. Open a PR with stripped preview callouts for human review.
# 4. Fail loudly on script errors or when no callout changes are produced.
# 5. Keep this workflow focused on suggestions only until that stable workflow is added.

on:
  # Run when PRs are merged to main
  pull_request:
    types: [closed]
    branches: [main]
    paths:
      - 'crates/**/*.rs'
      - '!crates/**/*_test.rs'
      - '!crates/**/tests/**'

  # Run on cherry-picks to release branches
  pull_request_target:
    types: [opened, synchronize]
    branches:
      - 'v0.*'
    paths:
      - 'crates/**/*.rs'

  # Manual trigger for testing
  workflow_dispatch:
    inputs:
      pr_number:
        description: 'PR number to analyze'
        required: true
        type: string
      mode:
        description: 'Output mode'
        required: true
        type: choice
        options:
          - batch
          - immediate
        default: batch

env:
  DROID_MODEL: claude-sonnet-4-5-20250929
  SUGGESTIONS_BRANCH: docs/suggestions-pending

jobs:
  # Job for PRs merged to main - batch suggestions to branch
  # Only runs for PRs from the same repo (not forks) since secrets aren't available for fork PRs
  batch-suggestions:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: write
      pull-requests: read
    if: |
      (github.event_name == 'pull_request' &&
       github.event.pull_request.merged == true &&
       github.event.pull_request.base.ref == 'main' &&
       github.event.pull_request.head.repo.full_name == github.repository) ||
      (github.event_name == 'workflow_dispatch' && inputs.mode == 'batch')

    steps:
      - name: Checkout repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install Droid CLI
        run: |
          # Retry with exponential backoff for transient network/auth issues
          MAX_RETRIES=3
          for i in $(seq 1 "$MAX_RETRIES"); do
            echo "Attempt $i of $MAX_RETRIES to install Droid CLI..."
            if curl -fsSL https://app.factory.ai/cli | sh; then
              echo "Droid CLI installed successfully"
              break
            fi
            if [ "$i" -eq "$MAX_RETRIES" ]; then
              echo "Failed to install Droid CLI after $MAX_RETRIES attempts"
              exit 1
            fi
            sleep $((i * 5))
          done
          echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
        env:
          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}

      - name: Get PR info
        id: pr
        env:
          INPUT_PR_NUMBER: ${{ inputs.pr_number }}
          EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
          GH_TOKEN: ${{ github.token }}
        run: |
          if [ -n "$INPUT_PR_NUMBER" ]; then
            PR_NUM="$INPUT_PR_NUMBER"
          else
            PR_NUM="$EVENT_PR_NUMBER"
          fi
          if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
            echo "::error::Invalid PR number: $PR_NUM"
            exit 1
          fi
          echo "number=$PR_NUM" >> "$GITHUB_OUTPUT"
          PR_TITLE=$(gh pr view "$PR_NUM" --json title --jq '.title' | tr -d '\n\r' | head -c 200)
          EOF_MARKER="EOF_$(openssl rand -hex 8)"
          {
            echo "title<<$EOF_MARKER"
            echo "$PR_TITLE"
            echo "$EOF_MARKER"
          } >> "$GITHUB_OUTPUT"

      - name: Analyze PR for documentation needs
        id: analyze
        env:
          GH_TOKEN: ${{ github.token }}
          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
          PR_NUMBER: ${{ steps.pr.outputs.number }}
        run: |
          # Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected)
          # Unset GH_TOKEN first to allow gh auth login to store credentials
          echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token)

          OUTPUT_FILE=$(mktemp)

          # Retry with exponential backoff for transient Factory API failures
          MAX_RETRIES=3
          for i in $(seq 1 "$MAX_RETRIES"); do
            echo "Attempt $i of $MAX_RETRIES to analyze PR..."
            if ./script/docs-suggest \
              --pr "$PR_NUMBER" \
              --immediate \
              --preview \
              --output "$OUTPUT_FILE" \
              --verbose; then
              echo "Analysis completed successfully"
              break
            fi
            if [ "$i" -eq "$MAX_RETRIES" ]; then
              echo "Analysis failed after $MAX_RETRIES attempts"
              exit 1
            fi
            echo "Retrying in $((i * 5)) seconds..."
            sleep $((i * 5))
          done

          # Check if we got actionable suggestions (not "no updates needed")
          if grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
             ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
            echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
            echo "output_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
          else
            echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
            echo "No actionable documentation suggestions for this PR"
            cat "$OUTPUT_FILE"
          fi

      - name: Commit suggestions to queue branch
        if: steps.analyze.outputs.has_suggestions == 'true'
        env:
          PR_NUM: ${{ steps.pr.outputs.number }}
          PR_TITLE: ${{ steps.pr.outputs.title }}
          OUTPUT_FILE: ${{ steps.analyze.outputs.output_file }}
          REPO: ${{ github.repository }}
        run: |
          set -euo pipefail

          # Configure git
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

          # Retry loop for handling concurrent pushes
          MAX_RETRIES=3
          for i in $(seq 1 "$MAX_RETRIES"); do
            echo "Attempt $i of $MAX_RETRIES"

            # Fetch and checkout suggestions branch (create if doesn't exist)
            if git ls-remote --exit-code --heads origin "$SUGGESTIONS_BRANCH" > /dev/null 2>&1; then
              git fetch origin "$SUGGESTIONS_BRANCH"
              git checkout -B "$SUGGESTIONS_BRANCH" "origin/$SUGGESTIONS_BRANCH"
            else
              # Create orphan branch for clean history
              git checkout --orphan "$SUGGESTIONS_BRANCH"
              git rm -rf . > /dev/null 2>&1 || true

              # Initialize with README
              cat > README.md << 'EOF'
          # Documentation Suggestions Queue

          This branch contains batched documentation suggestions for the next Preview release.

          Each file represents suggestions from a merged PR. At preview branch cut time,
          run `script/docs-suggest-publish` to create a documentation PR from these suggestions.

          ## Structure

          - `suggestions/PR-XXXXX.md` - Suggestions for PR #XXXXX
          - `manifest.json` - Index of all pending suggestions

          ## Workflow

          1. PRs merged to main trigger documentation analysis
          2. Suggestions are committed here as individual files
          3. At preview release, suggestions are collected into a docs PR
          4. After docs PR is created, this branch is reset
          EOF

              mkdir -p suggestions
              echo '{"suggestions":[]}' > manifest.json
              git add README.md suggestions manifest.json
              git commit -m "Initialize documentation suggestions queue"
            fi

            # Create suggestion file
            SUGGESTION_FILE="suggestions/PR-${PR_NUM}.md"

            {
              echo "# PR #${PR_NUM}: ${PR_TITLE}"
              echo ""
              echo "_Merged: $(date -u +%Y-%m-%dT%H:%M:%SZ)_"
              echo "_PR: https://github.com/${REPO}/pull/${PR_NUM}_"
              echo ""
              cat "$OUTPUT_FILE"
            } > "$SUGGESTION_FILE"

            # Update manifest
            MANIFEST=$(cat manifest.json)
            NEW_ENTRY="{\"pr\":${PR_NUM},\"title\":$(echo "$PR_TITLE" | jq -R .),\"file\":\"$SUGGESTION_FILE\",\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"

            # Add to manifest if not already present
            if ! echo "$MANIFEST" | jq -e ".suggestions[] | select(.pr == $PR_NUM)" > /dev/null 2>&1; then
              echo "$MANIFEST" | jq ".suggestions += [$NEW_ENTRY]" > manifest.json
            fi

            # Commit
            git add "$SUGGESTION_FILE" manifest.json
            git commit -m "docs: Add suggestions for PR #${PR_NUM}

          ${PR_TITLE}

          Auto-generated documentation suggestions for review at next preview release."

            # Try to push
            if git push origin "$SUGGESTIONS_BRANCH"; then
              echo "Successfully pushed suggestions"
              break
            else
              echo "Push failed, retrying..."
              if [ "$i" -eq "$MAX_RETRIES" ]; then
                echo "Failed after $MAX_RETRIES attempts"
                exit 1
              fi
              sleep $((i * 2))
            fi
          done

      - name: Summary
        if: always()
        env:
          HAS_SUGGESTIONS: ${{ steps.analyze.outputs.has_suggestions }}
          PR_NUM: ${{ steps.pr.outputs.number }}
          REPO: ${{ github.repository }}
        run: |
          {
            echo "## Documentation Suggestions"
            echo ""
            if [ "$HAS_SUGGESTIONS" == "true" ]; then
              echo "✅ Suggestions queued for PR #${PR_NUM}"
              echo ""
              echo "View pending suggestions: [docs/suggestions-pending branch](https://github.com/${REPO}/tree/${SUGGESTIONS_BRANCH})"
            else
              echo "No documentation updates needed for this PR."
            fi
          } >> "$GITHUB_STEP_SUMMARY"

  # Job for cherry-picks to release branches - immediate output as PR comment
  cherry-pick-suggestions:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: read
      pull-requests: write
    concurrency:
      group: docs-suggestions-${{ github.event.pull_request.number || inputs.pr_number || 'manual' }}
      cancel-in-progress: true
    if: |
      (github.event_name == 'pull_request_target' &&
       startsWith(github.event.pull_request.base.ref, 'v0.') &&
       contains(fromJSON('["MEMBER","OWNER"]'),
                github.event.pull_request.author_association)) ||
      (github.event_name == 'workflow_dispatch' && inputs.mode == 'immediate')

    steps:
      - name: Checkout repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.ref || '' }}
          persist-credentials: false

      - name: Install Droid CLI
        run: |
          # Retry with exponential backoff for transient network/auth issues
          MAX_RETRIES=3
          for i in $(seq 1 "$MAX_RETRIES"); do
            echo "Attempt $i of $MAX_RETRIES to install Droid CLI..."
            if curl -fsSL https://app.factory.ai/cli | sh; then
              echo "Droid CLI installed successfully"
              break
            fi
            if [ "$i" -eq "$MAX_RETRIES" ]; then
              echo "Failed to install Droid CLI after $MAX_RETRIES attempts"
              exit 1
            fi
            sleep $((i * 5))
          done
          echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
        env:
          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}

      - name: Get PR number
        id: pr
        env:
          INPUT_PR_NUMBER: ${{ inputs.pr_number }}
          EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          if [ -n "$INPUT_PR_NUMBER" ]; then
            PR_NUM="$INPUT_PR_NUMBER"
          else
            PR_NUM="$EVENT_PR_NUMBER"
          fi
          if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
            echo "::error::Invalid PR number: $PR_NUM"
            exit 1
          fi
          echo "number=$PR_NUM" >> "$GITHUB_OUTPUT"

      - name: Analyze PR for documentation needs
        id: analyze
        env:
          GH_TOKEN: ${{ github.token }}
          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
          PR_NUMBER: ${{ steps.pr.outputs.number }}
        run: |
          # Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected)
          # Unset GH_TOKEN first to allow gh auth login to store credentials
          echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token)

          OUTPUT_FILE="${RUNNER_TEMP}/suggestions.md"

          # Cherry-picks don't get preview callout
          # Retry with exponential backoff for transient Factory API failures
          MAX_RETRIES=3
          for i in $(seq 1 "$MAX_RETRIES"); do
            echo "Attempt $i of $MAX_RETRIES to analyze PR..."
            if ./script/docs-suggest \
              --pr "$PR_NUMBER" \
              --immediate \
              --no-preview \
              --output "$OUTPUT_FILE" \
              --verbose; then
              echo "Analysis completed successfully"
              break
            fi
            if [ "$i" -eq "$MAX_RETRIES" ]; then
              echo "Analysis failed after $MAX_RETRIES attempts"
              exit 1
            fi
            echo "Retrying in $((i * 5)) seconds..."
            sleep $((i * 5))
          done

          # Check if we got actionable suggestions
          if [ -s "$OUTPUT_FILE" ] && \
             grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
             ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
            echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
            echo "suggestions_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
          else
            echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Post suggestions as PR comment
        if: steps.analyze.outputs.has_suggestions == 'true'
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
        env:
          SUGGESTIONS_FILE: ${{ steps.analyze.outputs.suggestions_file }}
          PR_NUMBER: ${{ steps.pr.outputs.number }}
        with:
          script: |
            const fs = require('fs');

            // Read suggestions from file
            const suggestionsRaw = fs.readFileSync(process.env.SUGGESTIONS_FILE, 'utf8');

            // Sanitize AI-generated content
            let sanitized = suggestionsRaw
              // Strip HTML tags
              .replace(/<[^>]*>/g, '')
              // Strip markdown links but keep display text
              .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
              // Strip raw URLs
              .replace(/https?:\/\/[^\s)>\]]+/g, '[link removed]')
              // Strip protocol-relative URLs
              .replace(/\/\/[^\s)>\]]+\.[^\s)>\]]+/g, '[link removed]')
              // Neutralize @-mentions (preserve JSDoc-style annotations)
              .replace(/@(?!param\b|returns?\b|throws?\b|typedef\b|type\b|see\b|example\b|since\b|deprecated\b|default\b)(\w+)/g, '`@$1`')
              // Strip cross-repo references that could be confused with real links
              .replace(/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+#\d+/g, '[ref removed]');

            // Truncate to 20,000 characters
            if (sanitized.length > 20000) {
              sanitized = sanitized.substring(0, 20000) + '\n\n…(truncated)';
            }

            // Parse and validate PR number
            const prNumber = parseInt(process.env.PR_NUMBER, 10);
            if (isNaN(prNumber) || prNumber <= 0) {
              core.setFailed(`Invalid PR number: ${process.env.PR_NUMBER}`);
              return;
            }

            const body = `## 📚 Documentation Suggestions

            This cherry-pick contains changes that may need documentation updates.

            ${sanitized}

            ---
            > **Note:** This comment was generated automatically by an AI model analyzing
            > code changes. Suggestions may contain inaccuracies — please verify before acting.

            <details>
            <summary>About this comment</summary>

            This comment was generated automatically by analyzing code changes in this cherry-pick.
            Cherry-picks typically don't need new documentation since the feature was already
            documented when merged to main, but please verify.

            </details>`;

            // Find existing comment to update (avoid spam)
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNumber
            });

            const botComment = comments.find(c =>
              c.user.type === 'Bot' &&
              c.body.includes('Documentation Suggestions')
            );

            if (botComment) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: botComment.id,
                body: body
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                body: body
              });
            }

      - name: Summary
        if: always()
        env:
          HAS_SUGGESTIONS: ${{ steps.analyze.outputs.has_suggestions }}
          PR_NUM: ${{ steps.pr.outputs.number }}
        run: |
          {
            echo "## 📚 Documentation Suggestions (Cherry-pick)"
            echo ""
            if [ "$HAS_SUGGESTIONS" == "true" ]; then
              echo "Suggestions posted as PR comment on #${PR_NUM}."
            else
              echo "No documentation suggestions for this cherry-pick."
            fi
          } >> "$GITHUB_STEP_SUMMARY"
