name: AI Bot

on:
  issues:
    types: [opened]
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  pull_request:
    types: [opened, edited, synchronize]

jobs:
  respond-to-commands:
    runs-on: ubuntu-latest
    if: |
      (github.actor == 'f') &&
      ((github.event_name == 'issues' && contains(github.event.issue.body, '/ai')) ||
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai')) ||
      (github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/ai')))
    permissions:
      contents: write
      pull-requests: write
      issues: write

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
          token: ${{ secrets.PAT_TOKEN }}

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install openai@^4.0.0 @octokit/rest@^19.0.0

      - name: Process command
        id: process
        env:
          GH_TOKEN: ${{ secrets.PAT_TOKEN }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          node << 'EOF'
          const OpenAI = require('openai');
          const { Octokit } = require('@octokit/rest');

          async function main() {
            const openai = new OpenAI({
              apiKey: process.env.OPENAI_API_KEY
            });
            
            const octokit = new Octokit({
              auth: process.env.GH_TOKEN
            });

            const eventName = process.env.GITHUB_EVENT_NAME;
            const eventPath = process.env.GITHUB_EVENT_PATH;
            const event = require(eventPath);
            
            // Double check user authorization
            const actor = event.sender?.login || event.pull_request?.user?.login || event.issue?.user?.login;
            if (actor !== 'f') {
              console.log('Unauthorized user attempted to use the bot:', actor);
              return;
            }

            // Get command and context
            let command = '';
            let issueNumber = null;
            let isPullRequest = false;
            
            if (eventName === 'issues') {
              command = event.issue.body;
              issueNumber = event.issue.number;
            } else if (eventName === 'issue_comment') {
              command = event.comment.body;
              issueNumber = event.issue.number;
              isPullRequest = !!event.issue.pull_request;
            } else if (eventName === 'pull_request_review_comment') {
              command = event.comment.body;
              issueNumber = event.pull_request.number;
              isPullRequest = true;
            } else if (eventName === 'pull_request') {
              command = event.pull_request.body;
              issueNumber = event.pull_request.number;
              isPullRequest = true;
            }

            if (!command.startsWith('/ai')) {
              return;
            }

            // Extract the actual command after /ai
            const aiCommand = command.substring(3).trim();
            
            // Handle resolve conflicts command
            if (aiCommand === 'resolve' || aiCommand === 'fix conflicts') {
              if (!isPullRequest) {
                console.log('Command rejected: Not a pull request');
                await octokit.issues.createComment({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  issue_number: issueNumber,
                  body: '❌ The resolve command can only be used on pull requests.'
                });
                return;
              }

              try {
                console.log('Starting resolve command execution...');
                
                // Get PR details
                console.log('Fetching PR details...');
                const { data: pr } = await octokit.pulls.get({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  pull_number: issueNumber
                });
                console.log(`Original PR found: #${issueNumber} from ${pr.user.login}`);

                // Get the PR diff to extract the new prompt
                console.log('Fetching PR file changes...');
                const { data: files } = await octokit.pulls.listFiles({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  pull_number: issueNumber
                });
                console.log(`Found ${files.length} changed files`);

                // Extract prompt from changes
                console.log('Analyzing changes to extract prompt information...');
                const prompts = new Map(); // Use Map to deduplicate prompts by title

                // Helper function to normalize prompt titles
                const normalizeTitle = (title) => {
                  title = title.trim();
                  // Remove "Act as" or "Act as a" or "Act as an" from start if present
                  title = title.replace(/^Act as (?:a |an )?/i, '');
                  // Add "Act as" prefix
                  return `Act as ${title}`;
                };

                for (const file of files) {
                  console.log(`Processing file: ${file.filename}`);
                  if (file.filename === 'README.md') {
                    const patch = file.patch || '';
                    const addedLines = patch.split('\n')
                      .filter(line => line.startsWith('+'))
                      .map(line => line.substring(1))
                      .join('\n');

                    console.log('Attempting to extract prompts from README changes...');
                    const promptMatches = [...addedLines.matchAll(/## (?:Act as (?:a |an )?)?([^\n]+)\n(?:Contributed by:[^\n]*\n)?(?:> )?([^#]+?)(?=\n##|\n\n##|$)/ig)];
                    
                    for (const match of promptMatches) {
                      const actName = normalizeTitle(match[1]);
                      const promptText = match[2].trim();
                      const contributorLine = addedLines.match(/Contributed by: \[@([^\]]+)\]\(https:\/\/github\.com\/([^\)]+)\)/);
                      const contributorInfo = contributorLine 
                        ? `Contributed by: [@${contributorLine[1]}](https://github.com/${contributorLine[2]})`
                        : `Contributed by: [@${pr.user.login}](https://github.com/${pr.user.login})`;
                      
                      prompts.set(actName.toLowerCase(), { actName, promptText, contributorInfo });
                      console.log(`Found prompt in README: "${actName}"`);
                    }
                  } else if (file.filename === 'prompts.csv') {
                    const patch = file.patch || '';
                    const addedLines = patch.split('\n')
                      .filter(line => line.startsWith('+'))
                      .map(line => line.substring(1))
                      .filter(line => line.trim()); // Remove empty lines

                    console.log('Attempting to extract prompts from CSV changes...');
                    for (const line of addedLines) {
                      // Parse CSV line considering escaped quotes
                      const matches = [...line.matchAll(/"([^"]*(?:""[^"]*)*)"/g)];
                      if (matches.length >= 2) {
                        const actName = normalizeTitle(matches[0][1].replace(/""/g, '"').trim());
                        const promptText = matches[1][1].replace(/""/g, '"').trim();
                        
                        // Only add if not already found in README
                        if (!prompts.has(actName.toLowerCase())) {
                          const contributorInfo = `Contributed by: [@${pr.user.login}](https://github.com/${pr.user.login})`;
                          prompts.set(actName.toLowerCase(), { actName, promptText, contributorInfo });
                          console.log(`Found prompt in CSV: "${actName}"`);
                        }
                      }
                    }
                  }
                }

                if (prompts.size === 0) {
                  console.log('Failed to extract prompt information');
                  await octokit.issues.createComment({
                    owner: event.repository.owner.login,
                    repo: event.repository.name,
                    issue_number: issueNumber,
                    body: '❌ Could not extract prompt information from changes'
                  });
                  return;
                }

                // Get content from main branch
                console.log('Fetching current content from main branch...');
                const { data: readmeFile } = await octokit.repos.getContent({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  path: 'README.md',
                  ref: 'main'
                });

                const { data: csvFile } = await octokit.repos.getContent({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  path: 'prompts.csv',
                  ref: 'main'
                });

                // Prepare new content
                console.log('Preparing content updates...');
                let readmeContent = Buffer.from(readmeFile.content, 'base64').toString('utf-8');
                let csvContent = Buffer.from(csvFile.content, 'base64').toString('utf-8');
                if (!csvContent.endsWith('\n')) csvContent += '\n';

                // Convert Map to array for processing
                const promptsArray = Array.from(prompts.values());

                // Process each prompt
                for (const { actName, promptText, contributorInfo } of promptsArray) {
                  // Remove markdown quote character and trim whitespace
                  const cleanPrompt = promptText.replace(/^>\s*/gm, '').trim();
                  
                  // For README: Add quote to each line
                  const readmePrompt = cleanPrompt.split('\n')
                    .map(line => `> ${line.trim()}`)
                    .join('\n');
                  const newSection = `## ${actName}\n${contributorInfo}\n\n${readmePrompt}\n\n`;
                  
                  // For CSV: Convert to single paragraph
                  const csvPrompt = cleanPrompt.replace(/\n+/g, ' ').trim();
                  
                  // Insert the new section before Contributors in README
                  const contributorsIndex = readmeContent.indexOf('## Contributors');
                  if (contributorsIndex === -1) {
                    console.log('Contributors section not found, appending to end');
                    readmeContent += newSection;
                  } else {
                    console.log('Inserting before Contributors section');
                    readmeContent = readmeContent.slice(0, contributorsIndex) + newSection + readmeContent.slice(contributorsIndex);
                  }

                  // Add to CSV content
                  csvContent += `"${actName.replace(/"/g, '""')}","${csvPrompt.replace(/"/g, '""')}"\n`;
                }

                // Create new branch
                const branchName = `prompt/${promptsArray.map(p => p.actName.toLowerCase().replace(/[^a-z0-9]+/g, '-')).join('-')}`;
                console.log(`Creating new branch: ${branchName}`);

                // Check if branch exists and delete it
                try {
                  console.log('Checking if branch already exists...');
                  const { data: existingRef } = await octokit.git.getRef({
                    owner: event.repository.owner.login,
                    repo: event.repository.name,
                    ref: `heads/${branchName}`
                  });

                  if (existingRef) {
                    // Check for existing PRs from this branch
                    console.log('Checking for existing PRs from this branch...');
                    const { data: existingPRs } = await octokit.pulls.list({
                      owner: event.repository.owner.login,
                      repo: event.repository.name,
                      head: `${event.repository.owner.login}:${branchName}`,
                      state: 'open'
                    });

                    // Close any existing PRs
                    for (const pr of existingPRs) {
                      console.log(`Closing existing PR #${pr.number}...`);
                      await octokit.pulls.update({
                        owner: event.repository.owner.login,
                        repo: event.repository.name,
                        pull_number: pr.number,
                        state: 'closed'
                      });
                    }

                    console.log('Branch exists, deleting it...');
                    await octokit.git.deleteRef({
                      owner: event.repository.owner.login,
                      repo: event.repository.name,
                      ref: `heads/${branchName}`
                    });
                    console.log('Existing branch deleted');
                  }
                } catch (error) {
                  // 404 means branch doesn't exist, which is fine
                  if (error.status !== 404) {
                    throw error;
                  }
                  console.log('Branch does not exist, proceeding with creation');
                }

                // Get main branch ref
                const { data: mainRef } = await octokit.git.getRef({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  ref: 'heads/main'
                });

                // Create new branch
                await octokit.git.createRef({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  ref: `refs/heads/${branchName}`,
                  sha: mainRef.object.sha
                });

                // Get current files from the new branch
                console.log('Getting current file SHAs...');
                const { data: currentReadme } = await octokit.repos.getContent({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  path: 'README.md',
                  ref: branchName
                });

                const { data: currentCsv } = await octokit.repos.getContent({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  path: 'prompts.csv',
                  ref: branchName
                });

                // Update files with correct author
                console.log('Updating README.md...');
                await octokit.repos.createOrUpdateFileContents({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  path: 'README.md',
                  message: promptsArray.length === 1
                    ? `feat: Add "${promptsArray[0].actName}" to README`
                    : `feat: Add multiple prompts to README`,
                  content: Buffer.from(readmeContent).toString('base64'),
                  branch: branchName,
                  sha: currentReadme.sha,
                  committer: {
                    name: pr.user.login,
                    email: `${pr.user.login}@users.noreply.github.com`
                  },
                  author: {
                    name: pr.user.login,
                    email: `${pr.user.login}@users.noreply.github.com`
                  }
                });

                console.log('Updating prompts.csv...');
                await octokit.repos.createOrUpdateFileContents({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  path: 'prompts.csv',
                  message: promptsArray.length === 1
                    ? `feat: Add "${promptsArray[0].actName}" to prompts.csv`
                    : `feat: Add multiple prompts to prompts.csv`,
                  content: Buffer.from(csvContent).toString('base64'),
                  branch: branchName,
                  sha: currentCsv.sha,
                  committer: {
                    name: pr.user.login,
                    email: `${pr.user.login}@users.noreply.github.com`
                  },
                  author: {
                    name: pr.user.login,
                    email: `${pr.user.login}@users.noreply.github.com`
                  }
                });

                // Create new PR
                const prTitle = promptsArray.length === 1
                  ? `feat: Add "${promptsArray[0].actName}"`
                  : `feat: Add multiple prompts (${promptsArray.map(p => `"${p.actName}"`).join(', ')})`;

                const prBody = promptsArray.length === 1
                  ? `This PR supersedes #${issueNumber} with proper formatting. Original PR by @${pr.user.login}. Added "${promptsArray[0].actName}" to README.md and prompts.csv, preserving original attribution.`
                  : `This PR supersedes #${issueNumber} with proper formatting. Original PR by @${pr.user.login}.\n\nAdded the following prompts:\n${promptsArray.map(p => `- "${p.actName}"`).join('\n')}\n\nAll prompts have been added to README.md and prompts.csv, preserving original attribution.`;

                const { data: newPr } = await octokit.pulls.create({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  title: prTitle,
                  head: branchName,
                  base: 'main',
                  body: prBody
                });

                // Comment on original PR
                await octokit.issues.createComment({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  issue_number: issueNumber,
                  body: `I've created a new PR #${newPr.number} with your contribution properly formatted. This PR will be closed in favor of the new one.`
                });

                // Close original PR
                await octokit.pulls.update({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  pull_number: issueNumber,
                  state: 'closed'
                });

                console.log(`Created new PR #${newPr.number} and closed original PR #${issueNumber}`);

              } catch (error) {
                console.error('Error details:', error);
                await octokit.issues.createComment({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  issue_number: issueNumber,
                  body: `❌ Error while trying to create new PR:\n\`\`\`\n${error.message}\n\`\`\``
                });
              }
              return;
            }

            // Handle rename command specifically
            if (aiCommand.startsWith('rename') || aiCommand === 'suggest title') {
              if (!isPullRequest) {
                await octokit.issues.createComment({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  issue_number: issueNumber,
                  body: '❌ The rename command can only be used on pull requests.'
                });
                return;
              }

              // Get PR details for context
              const { data: pr } = await octokit.pulls.get({
                owner: event.repository.owner.login,
                repo: event.repository.name,
                pull_number: issueNumber
              });

              // Get the list of files changed in the PR
              const { data: files } = await octokit.pulls.listFiles({
                owner: event.repository.owner.login,
                repo: event.repository.name,
                pull_number: issueNumber
              });

              // Process file changes
              const fileChanges = await Promise.all(files.map(async file => {
                if (file.status === 'removed') {
                  return `Deleted: ${file.filename}`;
                }
                
                // Get file content for added or modified files
                if (file.status === 'added' || file.status === 'modified') {
                  const patch = file.patch || '';
                  return `${file.status === 'added' ? 'Added' : 'Modified'}: ${file.filename}\nChanges:\n${patch}`;
                }
                
                return `${file.status}: ${file.filename}`;
              }));

              const completion = await openai.chat.completions.create({
                model: "gpt-3.5-turbo",
                messages: [
                  {
                    role: "system",
                    content: "You are a helpful assistant that generates clear and concise pull request titles. Follow these rules:\n1. Use conventional commit style (feat:, fix:, docs:, etc.)\n2. Focus on WHAT changed, not HOW or WHERE\n3. Keep it short and meaningful\n4. Don't mention file names or technical implementation details\n5. Return ONLY the new title, nothing else\n\nGood examples:\n- feat: Add \"Act as a Career Coach\"\n- fix: Correct typo in Linux Terminal prompt\n- docs: Update installation instructions\n- refactor: Improve error handling"
                  },
                  {
                    role: "user",
                    content: `Based on these file changes, generate a concise PR title:\n\n${fileChanges.join('\n\n')}`
                  }
                ],
                temperature: 0.5,
                max_tokens: 60
              });

              const newTitle = completion.choices[0].message.content.trim();

              // Update PR title
              await octokit.pulls.update({
                owner: event.repository.owner.login,
                repo: event.repository.name,
                pull_number: issueNumber,
                title: newTitle
              });

              // Add comment about the rename
              await octokit.issues.createComment({
                owner: event.repository.owner.login,
                repo: event.repository.name,
                issue_number: issueNumber,
                body: `✨ Updated PR title to: "${newTitle}"\n\nBased on the following changes:\n\`\`\`diff\n${fileChanges.join('\n')}\n\`\`\``
              });
              return;
            }

            // Handle other commands
            const completion = await openai.chat.completions.create({
              model: "gpt-3.5-turbo",
              messages: [
                {
                  role: "system",
                  content: "You are a helpful AI assistant that helps with GitHub repositories. You can suggest code changes, fix issues, and improve code quality."
                },
                {
                  role: "user",
                  content: aiCommand
                }
              ],
              temperature: 0.7,
              max_tokens: 2000
            });

            const response = completion.choices[0].message.content;

            // If response contains code changes, create a new branch and PR
            if (response.includes('```')) {
              const branchName = `ai-bot/fix-${issueNumber}`;
              
              // Create new branch
              const defaultBranch = event.repository.default_branch;
              const ref = await octokit.git.getRef({
                owner: event.repository.owner.login,
                repo: event.repository.name,
                ref: `heads/${defaultBranch}`
              });
              
              await octokit.git.createRef({
                owner: event.repository.owner.login,
                repo: event.repository.name,
                ref: `refs/heads/${branchName}`,
                sha: ref.data.object.sha
              });

              // Extract code changes and file paths from response
              const codeBlocks = response.match(/```[\s\S]*?```/g);
              for (const block of codeBlocks) {
                const [_, filePath, ...codeLines] = block.split('\n');
                const content = Buffer.from(codeLines.join('\n')).toString('base64');
                
                await octokit.repos.createOrUpdateFileContents({
                  owner: event.repository.owner.login,
                  repo: event.repository.name,
                  path: filePath,
                  message: `AI Bot: Apply suggested changes for #${issueNumber}`,
                  content,
                  branch: branchName
                });
              }

              // Create PR
              await octokit.pulls.create({
                owner: event.repository.owner.login,
                repo: event.repository.name,
                title: `AI Bot: Fix for #${issueNumber}`,
                body: `This PR was automatically generated in response to #${issueNumber}\n\nChanges proposed:\n${response}`,
                head: branchName,
                base: defaultBranch
              });
            }

            // Add comment with response
            await octokit.issues.createComment({
              owner: event.repository.owner.login,
              repo: event.repository.name,
              issue_number: issueNumber,
              body: `AI Bot Response:\n\n${response}`
            });
          }

          main().catch(error => {
            console.error('Error:', error);
            process.exit(1);
          });
          EOF

      - name: Handle errors
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            const issueNumber = context.issue.number || context.payload.pull_request.number;
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
              body: '❌ Sorry, there was an error processing your command. Please try again or contact the repository maintainers.'
            });