name: Aider Common Steps

on:
  workflow_call:
    inputs:
      issue_title:
        description: "Title of the issue or PR"
        required: false
        type: string
      issue_body:
        description: "Body of the issue or PR"
        required: false
        type: string
      instruction:
        description: "Instruction for Aider"
        required: false
        type: string
      issue_id:
        description: "ID of the issue or PR"
        required: false
        type: string
      needs_processing:
        description: "Whether the issue needs to be processed by the external API"
        required: false
        type: boolean
        default: true
      base_prompt:
        description: "Base prompt for Aider"
        required: false
        type: string
        default: "Try to fix the following issue based on the instruction given by the user. The issue is prepended with the word ISSUE. The instruction is prepended with the word INSTRUCTION. The issue and instruction are separated by a blank line."
      probe_prompt:
        description: "Prompt for probe-chat"
        required: false
        type: string
        default: 'I''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST. Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: ["file1.py", "file2.py"]'
      rules_files:
        description: "Rules files for Aider"
        required: false
        type: string
    outputs:
      files_to_edit:
        description: "Files identified by probe-chat for editing"
        value: ${{ jobs.common-steps.outputs.files_to_edit }}
      final_prompt:
        description: "Final prompt for Aider"
        value: ${{ jobs.common-steps.outputs.final_prompt }}
      pr_branch_name:
        description: "Name of the branch used for PR"
        value: ${{ jobs.common-steps.outputs.pr_branch_name }}
      changes_applied_message:
        description: "Message indicating changes were applied"
        value: ${{ jobs.common-steps.outputs.changes_applied_message }}
      changes_applied:
        description: "Boolean indicating if changes were successfully applied"
        value: ${{ jobs.common-steps.outputs.changes_applied }}

jobs:
  common-steps:
    runs-on: ubicloud-standard-8
    outputs:
      files_to_edit: ${{ steps.probe_files.outputs.files_to_edit }}
      final_prompt: ${{ steps.create_prompt.outputs.final_prompt }}
      pr_branch_name: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }}
      changes_applied_message: ${{ steps.commit_and_push.outputs.CHANGES_APPLIED_MESSAGE }}
      changes_applied: ${{ steps.commit_and_push.outputs.CHANGES_APPLIED }}
    env:
      GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
      GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }}
      LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
      DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit

      - name: Check out code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Checkout PR Branch
        id: checkout_pr
        if: (github.event_name == 'issue_comment' && github.event.issue.pull_request) || (github.event_name == 'pull_request_review')
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "Issue comment trigger: Checking out PR branch..."
          PR_NUMBER=""
          if [ -n "${{ github.event.issue.number }}" ]; then
            PR_NUMBER="${{ github.event.issue.number }}"
          elif [ -n "${{ github.event.pull_request.number }}" ]; then
            PR_NUMBER="${{ github.event.pull_request.number }}"
          else
            echo "::error::Could not determine PR number."
            exit 1
          fi
          PR_HEAD_REF=$(gh pr view $PR_NUMBER --json headRefName -q .headRefName --repo $GITHUB_REPOSITORY)
          if [[ -z "$PR_HEAD_REF" || "$PR_HEAD_REF" == "null" ]]; then
             echo "::error::Could not determine PR head branch for PR #$PR_NUMBER via gh CLI."
             exit 1
          fi
          echo "Checking out PR head branch: $PR_HEAD_REF for PR #$PR_NUMBER"
          git fetch origin "refs/heads/${PR_HEAD_REF}:refs/remotes/origin/${PR_HEAD_REF}" --no-tags
          git checkout "$PR_HEAD_REF"
          echo "Successfully checked out branch $(git rev-parse --abbrev-ref HEAD)"
          echo "PR_BRANCH=$PR_HEAD_REF" >> $GITHUB_OUTPUT

      - name: Configure Git User
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Cache Python dependencies
        uses: actions/cache@v3
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/setup.py') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install Aider and Dependencies
        run: |
          echo "Installing Aider..."
          python -m pip install uv
          python -m venv ~/uv-env
          source ~/uv-env/bin/activate
          uv pip install configargparse==1.7
          uv pip install aider-chat==0.83.1
          uv pip install -U google-generativeai
          sudo apt-get update && sudo apt-get install -y jq
          echo "$HOME/.local/bin" >> $GITHUB_PATH
          echo "VIRTUAL_ENV_PATH=$HOME/uv-env" >> $GITHUB_ENV

      - name: Create Prompt for Aider
        id: create_prompt
        shell: bash
        env:
          BASE_PROMPT_ENV: ${{ inputs.base_prompt }}
          ISSUE_TITLE_ENV: ${{ inputs.issue_title }}
          ISSUE_BODY_ENV: ${{ inputs.issue_body }}
          INSTRUCTION_ENV: ${{ inputs.instruction }}
          NEEDS_PROCESSING_ENV: ${{ inputs.needs_processing }}
          WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }}
        run: |
          set -e
          FINAL_PROMPT_CONTENT=""

          if [[ "$ISSUE_TITLE_ENV" != "" && "$ISSUE_BODY_ENV" != "" ]]; then
            echo "Processing issue with title: $ISSUE_TITLE_ENV"
            if [[ "$NEEDS_PROCESSING_ENV" == "true" ]]; then
              echo "Needs processing is true. Calling Windmill API..."
              JSON_PAYLOAD=$(jq -n \
                --arg title "$ISSUE_TITLE_ENV" \
                --arg body "$ISSUE_BODY_ENV" \
                '{"body":{"issue_title":$title,"issue_body":$body}}')

              echo "Windmill JSON Payload: $JSON_PAYLOAD"

              API_RESULT_FILE=$(mktemp)
              HTTP_CODE=$(curl -s -o "$API_RESULT_FILE" -w "%{http_code}" \
                -X POST "https://app.windmill.dev/api/w/windmill-labs/jobs/run_wait_result/p/f/ai/quiet_script" \
                -H "Content-Type: application/json" \
                -H "Authorization: Bearer $WINDMILL_TOKEN" \
                --data-binary "$JSON_PAYLOAD" \
                --max-time 90)

              BODY_CONTENT=$(cat "$API_RESULT_FILE")
              rm -f "$API_RESULT_FILE" # Clean up temp file

              echo "Windmill API HTTP Code: $HTTP_CODE"
              if [[ "$HTTP_CODE" -eq 200 ]]; then
                PROCESSED_ISSUE_PROMPT=$(echo "$BODY_CONTENT" | jq -r '.effective_body // empty')
                if [[ -z "$PROCESSED_ISSUE_PROMPT" || "$PROCESSED_ISSUE_PROMPT" == "null" ]]; then
                  echo "::warning::Windmill API returned 200 but effective_body was empty or null."
                  EFFECTIVE_ISSUE_CONTENT_FOR_PROMPT="$ISSUE_BODY_ENV"
                else
                  echo "Successfully processed issue via Windmill API."
                  EFFECTIVE_ISSUE_CONTENT_FOR_PROMPT="$PROCESSED_ISSUE_PROMPT"
                fi
                FINAL_PROMPT_CONTENT=$(printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
                  "$BASE_PROMPT_ENV" "$EFFECTIVE_ISSUE_CONTENT_FOR_PROMPT" "$INSTRUCTION_ENV")
              else
                echo "::error::Windmill API call failed (HTTP $HTTP_CODE). Using raw issue content for prompt."
                FINAL_PROMPT_CONTENT=$(printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
                  "$BASE_PROMPT_ENV" "$ISSUE_BODY_ENV" "$INSTRUCTION_ENV")
              fi
            else
              echo "Needs processing is false. Using raw issue content for prompt."
              FINAL_PROMPT_CONTENT=$(printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
                "$BASE_PROMPT_ENV" "$ISSUE_BODY_ENV" "$INSTRUCTION_ENV")
            fi
          else
            echo "No issue title or body given. Using base prompt."
            FINAL_PROMPT_CONTENT=$(printf "%s\nINSTRUCTION:\n%s" "$BASE_PROMPT_ENV" "$INSTRUCTION_ENV")
          fi

          echo "Final prompt: $FINAL_PROMPT_CONTENT"
          echo "final_prompt<<EOF_AIDER_PROMPT" >> "$GITHUB_OUTPUT"
          echo "$FINAL_PROMPT_CONTENT" >> "$GITHUB_OUTPUT"
          echo "EOF_AIDER_PROMPT" >> "$GITHUB_OUTPUT"

      - name: Probe Chat for Relevant Files
        id: probe_files
        shell: bash
        env:
          FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }}
          PROBE_PROMPT: ${{ inputs.probe_prompt }}
        run: |
          echo "Running probe-chat to find relevant files..."

          MESSAGE_FOR_PROBE=$(printf "%s\nREQUEST:\n%s" "$PROBE_PROMPT" "$FINAL_PROMPT")

          set -o pipefail
          PROBE_OUTPUT=$(npx --yes @buger/probe-chat@latest --max-iterations 50 --model-name gemini-2.5-pro-preview-05-06 --message "$MESSAGE_FOR_PROBE") || {
            echo "::error::probe-chat command failed. Output:"
            echo "$PROBE_OUTPUT"
            exit 1
          }
          set +o pipefail
          echo "Probe-chat raw output:"
          echo "$PROBE_OUTPUT"

          JSON_FILES=$(echo "$PROBE_OUTPUT" | sed -n '/^\s*\[/,$p' | sed '/^\s*\]/q')
          echo "Extracted JSON block:"
          echo "$JSON_FILES"

          FILES_LIST=$(echo "$JSON_FILES" | jq -e -r '[.[] | select(type == "string" and . != "" and . != null and (endswith("/") | not))] | join(" ")' || echo "")

          if [[ -z "$FILES_LIST" ]]; then
             echo "::warning::probe-chat did not identify any relevant files."
          fi

          echo "Formatted files list for aider: $FILES_LIST"
          echo "files_to_edit=$FILES_LIST" >> $GITHUB_OUTPUT

      - name: Cache Aider tags
        uses: actions/cache@v3
        with:
          path: .aider.tags.cache.v4
          key: ${{ runner.os }}-aider-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-aider-

      - name: Prepare branch for Aider
        id: prepare_branch
        env:
          ISSUE_ID: ${{ inputs.issue_id }}
        run: |
          if [[ "$ISSUE_ID" != "" ]]; then
            BRANCH_NAME="aider-fix-issue-${ISSUE_ID}"

            # Check if branch exists remotely
            if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then
              echo "Branch $BRANCH_NAME already exists remotely, fetching it"
              git fetch origin $BRANCH_NAME
              git checkout $BRANCH_NAME
              git pull origin $BRANCH_NAME
            else
              echo "Creating new branch $BRANCH_NAME"
              git checkout -b $BRANCH_NAME
            fi
            echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
          else
            # We're in a pull_request_review event
            PR_NUMBER="${{ github.event.pull_request.number }}"
            PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
            
            echo "Handling pull_request_review for PR #$PR_NUMBER on branch $PR_HEAD_REF"
            
            # Ensure we're on the correct branch
            git config pull.rebase true
            git fetch origin $PR_HEAD_REF
            git checkout $PR_HEAD_REF
            git pull origin $PR_HEAD_REF
            
            echo "Using PR branch $PR_HEAD_REF for PR #$PR_NUMBER"
            echo "BRANCH_NAME=$PR_HEAD_REF" >> $GITHUB_OUTPUT
          fi

      - name: Run Aider
        id: run_aider
        shell: bash
        env:
          FILES_TO_EDIT: ${{ steps.probe_files.outputs.files_to_edit }}
          FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }}
          RULES_FILES: ${{ inputs.rules_files }}
        run: |
          source $VIRTUAL_ENV_PATH/bin/activate
          echo "$FINAL_PROMPT" > .aider_final_prompt.txt
          echo "FILES_TO_EDIT: $FILES_TO_EDIT"

          RULES=""
          if [ -n "$RULES_FILES" ]; then
            for rule in $RULES_FILES; do
              RULES="$RULES --read $rule"
            done
          fi

          aider \
            $RULES \
            $FILES_TO_EDIT \
            --model gemini/gemini-2.5-pro-preview-05-06 \
            --message-file .aider_final_prompt.txt \
            --yes \
            --no-check-update \
            --auto-commits \
            --no-analytics \
            --no-gitignore \
            | tee .aider_output.txt || true

          echo "Aider command completed. Output saved to .aider_output.txt"

      - name: Cache Node.js dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Commit and Push Changes
        id: commit_and_push
        env:
          ISSUE_ID: ${{ inputs.issue_id }}
          BRANCH_NAME: ${{ steps.prepare_branch.outputs.BRANCH_NAME }}
        run: |
          if [[ "$ISSUE_ID" != "" ]]; then
            # Check if there are any uncommitted changes
            if [[ -n $(git status --porcelain) ]]; then
              echo "Found uncommitted changes, committing them"
              git add .
              git commit -m "Aider changes"
            fi

            # Push changes to the branch
            if git push origin $BRANCH_NAME; then
              echo "Pushed to branch $BRANCH_NAME"
              echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
              echo "CHANGES_APPLIED_MESSAGE=Aider changes pushed to branch $BRANCH_NAME." >> $GITHUB_OUTPUT
              echo "CHANGES_APPLIED=true" >> $GITHUB_OUTPUT
            else
              echo "::warning::Push to PR branch $BRANCH_NAME failed."
              echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $BRANCH_NAME." >> $GITHUB_OUTPUT
              echo "CHANGES_APPLIED=false" >> $GITHUB_OUTPUT
            fi
          else
            # We're in a pull_request_review event
            PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
            echo "Attempting to push changes to PR branch $PR_HEAD_REF"
            if git push origin $PR_HEAD_REF; then
              echo "Push to $PR_HEAD_REF successful (or no new changes to push)."
              echo "CHANGES_APPLIED_MESSAGE=Aider changes (if any) pushed to PR branch $PR_HEAD_REF." >> $GITHUB_OUTPUT
              echo "PR_BRANCH_NAME=$PR_HEAD_REF" >> $GITHUB_OUTPUT
              echo "CHANGES_APPLIED=true" >> $GITHUB_OUTPUT
            else
              echo "::warning::Push to PR branch $PR_HEAD_REF failed."
              echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $PR_HEAD_REF." >> $GITHUB_OUTPUT
              echo "CHANGES_APPLIED=false" >> $GITHUB_OUTPUT
            fi
          fi

      - name: Create Pull Request
        if: always() && (github.event_name == 'issue_comment' || github.event_name == 'repository_dispatch') && !github.event.issue.pull_request && steps.commit_and_push.outputs.PR_BRANCH_NAME != ''
        id: create_pr
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_BRANCH: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }}
          ISSUE_NUM: ${{ inputs.issue_id }}
          ISSUE_TITLE: ${{ inputs.issue_title }}
          GITHUB_EVENT_NAME: ${{ github.event_name }}
        run: |
          # Create PR description in a temporary file to avoid command line length limits and ensure it stays under 40k chars
          HEADER="This PR was created automatically by Aider to fix issue #${ISSUE_NUM}."
          # if event is repository_dispatch, add the issue title to the header
          if [ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]; then
            if [[ "${{ github.event.client_payload.source }}" == "linear" ]]; then
              HEADER="This PR was created automatically by Aider to fix issue #linear:${ISSUE_NUM}."
            elif [[ "${{ github.event.client_payload.source }}" == "discord" ]]; then
              HEADER="This PR was created automatically by Aider to fix issue #discord:${ISSUE_NUM}."
            fi
          fi
          cat > /tmp/pr-description.md << EOL | head -c 40000
          $HEADER

          ## Aider Output
          \`\`\`
          $(cat .aider_output.txt || echo "No output available")
          \`\`\`
          EOL

          # Create PR using the file for the body content, handle errors gracefully
          set +e  # Don't exit on error
          PR_TITLE="[Aider PR] Fix: ${ISSUE_TITLE}"
          if [ -z "$ISSUE_TITLE" ]; then
            PR_TITLE="[Aider PR] AI changes after request"
          fi
          gh pr create \
            --title "$PR_TITLE" \
            --body-file /tmp/pr-description.md \
            --head "$PR_BRANCH" \
            --base main \
            --draft
          PR_CREATE_EXIT_CODE=$?
          set -e  # Re-enable exit on error

          if [ $PR_CREATE_EXIT_CODE -eq 0 ]; then
            echo "PR created successfully"
            PR_URL=$(gh pr view $PR_BRANCH --json url --jq .url)
            echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT
            echo "PR_CREATED=true" >> $GITHUB_OUTPUT
          else
            echo "Warning: Failed to create PR. Exit code: $PR_CREATE_EXIT_CODE"
            echo "PR_CREATED=false" >> $GITHUB_OUTPUT
            # Continue workflow despite PR creation failure
          fi

      - name: Comment on PR with Aider Output
        if: always() && github.event_name == 'pull_request_review' && steps.commit_and_push.outputs.CHANGES_APPLIED != ''
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUM: ${{ github.event.pull_request.number }}
          JOB_STATUS: ${{ job.status }}
        run: |
          # Create comment body in a temporary file to avoid command line length limits
          if [[ "${{ steps.commit_and_push.outputs.CHANGES_APPLIED }}" == "true" ]]; then
            if [[ "$JOB_STATUS" == "success" ]]; then
              STATUS_PREFIX="🤖 I've automatically addressed the feedback based on the review."
            else
              STATUS_PREFIX="⚠️ I attempted to address the feedback, but encountered some issues."
            fi
          else
            if [[ "$JOB_STATUS" == "success" ]]; then
              STATUS_PREFIX="🤖 I attempted to address the review feedback, but no modifications were made."
            else
              STATUS_PREFIX="⚠️ I encountered issues while attempting to address the feedback, and no modifications were made."
            fi
          fi

          cat > /tmp/pr-comment.md << EOL
          ${STATUS_PREFIX}

          ## Aider Output
          \`\`\`
          $(cat .aider_output.txt || echo 'No output available')
          \`\`\`

          Please review the output and provide additional guidance if needed.
          EOL

          # Use the file for comment body
          gh pr comment $PR_NUM --body-file /tmp/pr-comment.md

      - name: Comment on issue/PR to let the user know Aider has finished working on the request
        if: always() && github.event_name == 'issue_comment'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          JOB_STATUS: ${{ job.status }}
          PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }}
          PR_URL: ${{ steps.create_pr.outputs.PR_URL }}
        run: |
          echo "Commenting on issue/PR #${{ github.event.issue.number }} to let the user know Aider has finished working on the request."

          if [[ "$JOB_STATUS" == "success" ]]; then
            if [[ "$PR_CREATED" == "true" ]]; then
              COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created. $PR_URL"
            else
              COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR."
            fi
          else
            COMMENT_BODY="⚠️ Aider encountered issues while working on your request. Please check the workflow logs for details."
          fi

          gh issue comment ${{ github.event.issue.number }} --body "$COMMENT_BODY" --repo $GITHUB_REPOSITORY

      - name: Comment on linear issue to let the user know Aider has finished working on the request
        if: always() && github.event_name == 'repository_dispatch'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          JOB_STATUS: ${{ job.status }}
          LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
          PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }}
          PR_URL: ${{ steps.create_pr.outputs.PR_URL }}
          DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
          SOURCE: ${{ github.event.client_payload.source }}
        run: |
          echo "Notifying user about Aider completion status for $SOURCE request #${{ github.event.client_payload.issue_id }}"
          if [[ "$JOB_STATUS" == "success" ]]; then
            if [[ "$PR_CREATED" == "true" ]]; then
              COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created. $PR_URL"
            else
              COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR."
            fi
          else
            COMMENT_BODY="⚠️ Aider encountered issues while working on your request. Please check the workflow logs for details."
          fi

          if [[ "$SOURCE" == "discord" ]]; then
            curl -X POST \
              -H "Authorization: Bot $DISCORD_BOT_TOKEN" \
              -H "Content-Type: application/json" \
              "https://discord.com/api/v10/channels/${{ github.event.client_payload.channel_id }}/messages" \
              -d "{\"content\":\"${COMMENT_BODY}\"}"
          else
            curl -X POST \
              -H "Authorization: $LINEAR_API_KEY" \
              -H "Content-Type: application/json" \
              "https://api.linear.app/graphql" \
              -d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"${COMMENT_BODY}\\\" }) { success } }\"}"
          fi
