name: Label issues based on keywords
on:
  issues:
    types: [opened, edited, reopened]
permissions:
  issues: write          # needed so the workflow can add labels
  contents: read
concurrency:
  group: issue-labeler-${{ github.event.issue.number }}
  cancel-in-progress: true
jobs:
  add-labels:
    runs-on: ubuntu-latest
    steps:
      - name: Label issues based on keywords
        id: label-step
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd  # v8.0.0
        with:
          script: |
            // Configuration: Add new labels and keywords here
            const labelConfig = {
              rocm: {
                // Keyword search - matches whole words only (with word boundaries)
                keywords: [
                  {
                    term: "composable kernel",
                    searchIn: "both"
                  },
                  {
                    term: "rccl",
                    searchIn: "body"  // only search in body
                  },
                  {
                    term: "migraphx",
                    searchIn: "title"  // only search in title
                  },
                  {
                    term: "hipgraph",
                    searchIn: "both"
                  },
                  {
                    term: "ROCm System Management Interface",
                    searchIn: "body"
                  },
                ],
                // Substring search - matches anywhere in text (partial matches)
                substrings: [
                  {
                    term: "VLLM_ROCM_",
                    searchIn: "both"
                  },
                  {
                    term: "aiter",
                    searchIn: "title"
                  },
                  {
                    term: "rocm",
                    searchIn: "title"
                  },
                  {
                    term: "amd",
                    searchIn: "title"
                  },
                  {
                    term: "hip-",
                    searchIn: "both"
                  },
                  {
                    term: "gfx",
                    searchIn: "both"
                  },
                  {
                    term: "cdna",
                    searchIn: "both"
                  },
                  {
                    term: "rdna",
                    searchIn: "both"
                  },
                  {
                    term: "torch_hip",
                    searchIn: "body"  // only in body
                  },
                  {
                    term: "_hip",
                    searchIn: "both"
                  },
                  {
                    term: "hip_",
                    searchIn: "both"
                  },
                  // ROCm tools and libraries
                  {
                    term: "hipify",
                    searchIn: "both"
                  },
                ],
                // Regex patterns - for complex pattern matching
                regexPatterns: [
                  {
                    pattern: "\\bmi\\d{3}[a-z]*\\b",
                    description: "AMD GPU names (mi + 3 digits + optional letters)",
                    flags: "gi",
                    searchIn: "both"  // "title", "body", or "both"
                  }
                ],
              },
              cpu: {
                // Keyword search - matches whole words only (with word boundaries)
                keywords: [
                  {
                    term: "CPU Backend",
                    searchIn: "title"
                  },
                  {
                    term: "x86",
                    searchIn: "title"
                  },
                  {
                    term: "ARM",
                    searchIn: "title"
                  },
                  {
                    term: "Apple Silicon",
                    searchIn: "title"
                  },
                  {
                    term: "IBM Z",
                    searchIn: "title"
                  },
                ],
              },
              // Add more label configurations here as needed
              // example: {
              //   keywords: [...],
              //   substrings: [...],
              //   regexPatterns: [...]
              // },
            };
            // Helper function to create regex based on search type
            function createSearchRegex(term, type) {
              // Escape special regex characters in the term
              const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
              switch (type) {
                case 'keyword':
                  // Word boundary search - matches whole words only
                  return new RegExp(`\\b${escapedTerm}\\b`, "gi");
                case 'substring':
                  // Substring search - matches anywhere in the text
                  return new RegExp(escapedTerm, "gi");
                default:
                  throw new Error(`Unknown search type: ${type}`);
              }
            }
            // Helper function to find matching terms in text with line information
            function findMatchingTermsWithLines(text, searchTerms = [], searchType = 'keyword', searchLocation = '') {
              const matches = [];
              const lines = text.split('\n');
              for (const termConfig of searchTerms) {
                let regex;
                let term, searchIn, pattern, description, flags;
                // Handle different input formats (string or object)
                if (typeof termConfig === 'string') {
                  term = termConfig;
                  searchIn = 'both'; // default
                } else {
                  term = termConfig.term;
                  searchIn = termConfig.searchIn || 'both';
                  pattern = termConfig.pattern;
                  description = termConfig.description;
                  flags = termConfig.flags;
                }
                // Skip if this term shouldn't be searched in the current location
                if (searchIn !== 'both' && searchIn !== searchLocation) {
                  continue;
                }
                // Create appropriate regex
                if (searchType === 'regex') {
                  regex = new RegExp(pattern, flags || "gi");
                } else {
                  regex = createSearchRegex(term, searchType);
                }
                const termMatches = [];
                // Check each line for matches
                lines.forEach((line, lineIndex) => {
                  const lineMatches = line.match(regex);
                  if (lineMatches) {
                    lineMatches.forEach(match => {
                      termMatches.push({
                        match: match,
                        lineNumber: lineIndex + 1,
                        lineContent: line.trim(),
                        searchType: searchType,
                        searchLocation: searchLocation,
                        originalTerm: term || pattern,
                        description: description,
                        // Show context around the match in the line
                        context: line.length > 100 ?
                          line.substring(Math.max(0, line.toLowerCase().indexOf(match.toLowerCase()) - 30),
                                       line.toLowerCase().indexOf(match.toLowerCase()) + match.length + 30) + '...'
                          : line.trim()
                      });
                    });
                  }
                });
                if (termMatches.length > 0) {
                  matches.push({
                    term: term || (description || pattern),
                    searchType: searchType,
                    searchLocation: searchLocation,
                    searchIn: searchIn,
                    pattern: pattern,
                    matches: termMatches,
                    count: termMatches.length
                  });
                }
              }
              return matches;
            }
            // Helper function to check if label should be added
            async function processLabel(labelName, config) {
              const body = context.payload.issue.body || "";
              const title = context.payload.issue.title || "";
              core.notice(`Processing label: ${labelName}`);
              core.notice(`Issue Title: "${title}"`);
              core.notice(`Issue Body length: ${body.length} characters`);
              let shouldAddLabel = false;
              let allMatches = [];
              let reason = '';
              const keywords = config.keywords || [];
              const substrings = config.substrings || [];
              const regexPatterns = config.regexPatterns || [];
              core.notice(`Searching with ${keywords.length} keywords, ${substrings.length} substrings, and ${regexPatterns.length} regex patterns`);
              // Search in title
              if (title.trim()) {
                core.notice(`Searching in title: "${title}"`);
                const titleKeywordMatches = findMatchingTermsWithLines(title, keywords, 'keyword', 'title');
                const titleSubstringMatches = findMatchingTermsWithLines(title, substrings, 'substring', 'title');
                const titleRegexMatches = findMatchingTermsWithLines(title, regexPatterns, 'regex', 'title');
                allMatches.push(...titleKeywordMatches, ...titleSubstringMatches, ...titleRegexMatches);
              }
              // Search in body
              if (body.trim()) {
                core.notice(`Searching in body (${body.length} characters)`);
                const bodyKeywordMatches = findMatchingTermsWithLines(body, keywords, 'keyword', 'body');
                const bodySubstringMatches = findMatchingTermsWithLines(body, substrings, 'substring', 'body');
                const bodyRegexMatches = findMatchingTermsWithLines(body, regexPatterns, 'regex', 'body');
                allMatches.push(...bodyKeywordMatches, ...bodySubstringMatches, ...bodyRegexMatches);
              }
              if (allMatches.length > 0) {
                core.notice(`Found ${allMatches.length} matching term(s):`);
                for (const termMatch of allMatches) {
                  const locationText = termMatch.searchLocation === 'title' ? 'title' : 'body';
                  const searchInText = termMatch.searchIn === 'both' ? 'both' : termMatch.searchIn;
                  if (termMatch.searchType === 'regex') {
                    core.notice(`  📍 Regex: "${termMatch.term}" (pattern: ${termMatch.pattern}) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`);
                  } else {
                    core.notice(`  📍 Term: "${termMatch.term}" (${termMatch.searchType} search) found ${termMatch.count} time(s) in ${locationText} (configured to search in: ${searchInText}):`);
                  }
                  // Show details for each match
                  termMatch.matches.forEach((match, index) => {
                    core.notice(`    ${index + 1}. Line ${match.lineNumber} in ${match.searchLocation}: "${match.match}" [${match.searchType}]`);
                    if (match.description) {
                      core.notice(`       Description: ${match.description}`);
                    }
                    core.notice(`       Context: ${match.context}`);
                    if (match.lineContent !== match.context) {
                      core.notice(`       Full line: ${match.lineContent}`);
                    }
                  });
                }
                shouldAddLabel = true;
                const totalMatches = allMatches.reduce((sum, t) => sum + t.count, 0);
                const titleMatches = allMatches.filter(t => t.searchLocation === 'title').reduce((sum, t) => sum + t.count, 0);
                const bodyMatches = allMatches.filter(t => t.searchLocation === 'body').reduce((sum, t) => sum + t.count, 0);
                const keywordMatches = allMatches.filter(t => t.searchType === 'keyword').reduce((sum, t) => sum + t.count, 0);
                const substringMatches = allMatches.filter(t => t.searchType === 'substring').reduce((sum, t) => sum + t.count, 0);
                const regexMatches = allMatches.filter(t => t.searchType === 'regex').reduce((sum, t) => sum + t.count, 0);
                reason = `Found ${totalMatches} total matches (${titleMatches} in title, ${bodyMatches} in body) - ${keywordMatches} keyword matches, ${substringMatches} substring matches, ${regexMatches} regex matches`;
              }
              core.notice(`Final decision: ${shouldAddLabel ? 'ADD LABEL' : 'DO NOT ADD LABEL'}`);
              core.notice(`Reason: ${reason || 'No matching terms found'}`);
              if (shouldAddLabel) {
                const existingLabels = context.payload.issue.labels.map(l => l.name);
                if (!existingLabels.includes(labelName)) {
                  await github.rest.issues.addLabels({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: context.issue.number,
                    labels: [labelName],
                  });
                  core.notice(`Label "${labelName}" added. ${reason}`);
                  return true;
                }
                core.notice(`Label "${labelName}" already present.`);
                return false;
              }
              core.notice(`No matching terms found for label "${labelName}".`);
              return false;
            }
            // Process all configured labels
            const labelsAddedResults = await Promise.all(
              Object.entries(labelConfig).map(([labelName, config]) => 
                processLabel(labelName, config).then(added => ({ labelName, added }))
              )
            );
            
            const numLabelsAdded = labelsAddedResults.filter(r => r.added).length;
            core.notice(`Processing complete. ${numLabelsAdded} label(s) added.`);
            
            // Return which labels were added for the next step
            const addedLabels = labelsAddedResults.filter(r => r.added).map(r => r.labelName);
            core.setOutput('labels_added', JSON.stringify(addedLabels));
            return addedLabels;

      - name: CC users for labeled issues
        if: steps.label-step.outputs.labels_added != '[]'
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd  # v8.0.0
        with:
          script: |
            // Configuration: Map labels to GitHub users to CC
            // You can add multiple users per label, and multiple label configurations
            // {users} will be replaced with @mentions
            const ccConfig = {
              rocm: {
                users: ['hongxiayang', 'tjtanaa', 'vllmellm'],
                message: 'CC {users} for ROCm-related issue',
              },
              mistral: {
                users: ['patrickvonplaten', 'juliendenize', 'andylolu2'],
                message: 'CC {users} for Mistral-related issue',
              },
              // Add more label -> user mappings here
              // Example:
              // cuda: {
              //   users: ['user1', 'user2'],
              //   message: 'CC {users} for CUDA-related issue',
              // },
              // performance: {
              //   users: ['perfexpert'],
              //   message: 'CC {users} for performance issue',
              // },
            };
            
            const labelsAdded = JSON.parse('${{ steps.label-step.outputs.labels_added }}');
            core.notice(`Labels added: ${labelsAdded.join(', ')}`);
            
            // Get existing comments to check for already mentioned users
            const comments = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            
            const issueBody = context.payload.issue.body || '';
            const allExistingText = issueBody + '\n' + comments.data.map(c => c.body).join('\n');
            
            // Process each label that was added
            for (const label of labelsAdded) {
              if (ccConfig[label]) {
                const config = ccConfig[label];
                const usersToMention = [];
                
                // Check which users haven't been mentioned yet
                for (const user of config.users) {
                  const mentionPattern = new RegExp(`@${user}\\b`, 'i');
                  if (!mentionPattern.test(allExistingText)) {
                    usersToMention.push(user);
                  } else {
                    core.notice(`@${user} already mentioned for label "${label}", skipping`);
                  }
                }
                
                // Post comment if there are users to mention
                if (usersToMention.length > 0) {
                  const mentions = usersToMention.map(u => `@${u}`).join(' ');
                  const message = config.message.replace('{users}', mentions);
                  
                  await github.rest.issues.createComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: context.issue.number,
                    body: message
                  });
                  
                  core.notice(`CC comment added for label "${label}": ${mentions}`);
                } else {
                  core.notice(`All users for label "${label}" already mentioned, skipping comment`);
                }
              }
            }

      - name: Request missing ROCm info from issue author
        if: contains(steps.label-step.outputs.labels_added, 'rocm') && contains(toJSON(github.event.issue.labels.*.name), 'bug')
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd  # v8.0.0
        with:
          script: |
            const body = (context.payload.issue.body || '').toLowerCase();

            // Check for existing bot comments to avoid duplicate requests
            const comments = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            const botAlreadyAsked = comments.data.some(
              c => c.user.type === 'Bot' && c.body.includes('<!-- rocm-info-request -->')
            );
            if (botAlreadyAsked) {
              core.notice('ROCm info request already posted, skipping');
              return;
            }

            // Define required information and detection patterns
            const requiredInfo = [
              {
                name: 'Reproducer',
                patterns: [
                  /reproduc/i, /minimal.?example/i, /repro\b/i, /steps to reproduce/i,
                  /code.?snippet/i, /sample.?code/i,
                  /```python[\s\S]*?```/, /```bash[\s\S]*?```/, /```sh[\s\S]*?```/,
                ],
                ask: 'A minimal reproducer (code snippet or script that triggers the issue)',
              },
              {
                name: 'Error message',
                patterns: [
                  /error/i, /traceback/i, /exception/i, /fault/i, /crash/i,
                  /failed/i, /abort/i, /panic/i,
                ],
                ask: 'The full error message or traceback',
              },
              {
                name: 'Installation method',
                patterns: [
                  /docker/i, /rocm\/pytorch/i, /dockerfile/i, /from source/i,
                  /pip install/i, /build.?from/i, /container/i, /image/i,
                  /wheel/i, /\.whl/i, /nightly/i,
                ],
                ask: 'How you installed vLLM (Docker image name, pip install, or build from source steps)',
              },
              {
                name: 'Command',
                patterns: [
                  /vllm serve/i, /python\s+\S+\.py/i, /```bash[\s\S]*?```/,
                  /```sh[\s\S]*?```/, /command/i, /launch/i, /run\s/i,
                  /--model/i, /--tensor-parallel/i, /--gpu-memory/i,
                ],
                ask: 'The command you used to launch vLLM (e.g., `vllm serve ...` or the Python script)',
              },
              {
                name: 'GFX architecture',
                patterns: [
                  /gfx\d{3,4}/i, /mi\d{3}/i, /mi\d{2}\b/i, /radeon/i,
                  /gpu.?arch/i, /rocm-smi/i, /rocminfo/i, /navi/i,
                  /instinct/i,
                ],
                ask: 'Your GPU model and GFX architecture (e.g., MI300X / gfx942) — run `rocminfo | grep gfx`',
              },
            ];

            const issueBody = context.payload.issue.body || '';
            const missing = requiredInfo.filter(info =>
              !info.patterns.some(p => p.test(issueBody))
            );

            if (missing.length === 0) {
              core.notice('All required ROCm info appears to be present');
              return;
            }

            const author = context.payload.issue.user.login;
            const checklist = requiredInfo.map(info => {
              const found = !missing.includes(info);
              return `- [${found ? 'x' : ' '}] ${info.ask}`;
            }).join('\n');
            const message = [
              '<!-- rocm-info-request -->',
              `Hi @${author}, thanks for reporting this ROCm issue!`,
              '',
              'To help us investigate, please make sure the following information is included:',
              '',
              checklist,
              '',
              'Please provide any unchecked items above. This will help us reproduce and resolve the issue faster. Thank you!',
            ].join('\n');

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: message,
            });
            core.notice(`Requested missing ROCm info from @${author}: ${missing.map(m => m.name).join(', ')}`);