# This workflow automatically requests reviews from codeowners when:
# 1. A PR is opened, reopened, or synchronized (updated)
# 2. A PR is marked as ready for review
#
# It reads the CODEOWNERS file and matches all changed files in the PR against
# the codeowner patterns, then requests reviews from the appropriate owners
# while avoiding duplicate requests for users who have already been requested
# or have already reviewed the PR.

name: Request Codeowner Reviews

on:
  # Needs to be pull_request_target to get write permissions
  pull_request_target:
    types: [opened, reopened, synchronize, ready_for_review]

permissions:
  pull-requests: write
  contents: read

jobs:
  request-codeowner-reviews:
    name: Run
    if: ${{ !github.event.pull_request.draft }}
    runs-on: ubuntu-latest
    steps:
      - name: Request reviews from component codeowners
        uses: actions/github-script@v7.0.1
        with:
          script: |
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const pr_number = context.payload.pull_request.number;

            console.log(`Processing PR #${pr_number} for codeowner review requests`);

            // Hidden marker to identify bot comments from this workflow
            const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';

            try {
              // Get the list of changed files in this PR
              const { data: files } = await github.rest.pulls.listFiles({
                owner,
                repo,
                pull_number: pr_number
              });

              const changedFiles = files.map(file => file.filename);
              console.log(`Found ${changedFiles.length} changed files`);

              if (changedFiles.length === 0) {
                console.log('No changed files found, skipping codeowner review requests');
                return;
              }

              // Fetch CODEOWNERS file from root
              const { data: codeownersFile } = await github.rest.repos.getContent({
                owner,
                repo,
                path: 'CODEOWNERS',
                ref: context.payload.pull_request.base.sha
              });
              const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');

              // Parse CODEOWNERS file to extract all patterns and their owners
              const codeownersLines = codeownersContent.split('\n')
                .map(line => line.trim())
                .filter(line => line && !line.startsWith('#'));

              const codeownersPatterns = [];

              // Convert CODEOWNERS pattern to regex (robust glob handling)
              function globToRegex(pattern) {
                // Escape regex special characters except for glob wildcards
                let regexStr = pattern
                  .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
                  .replace(/\*\*/g, '.*') // globstar
                  .replace(/\*/g, '[^/]*') // single star
                  .replace(/\?/g, '.'); // question mark
                return new RegExp('^' + regexStr + '$');
              }

              // Helper function to create comment body
              function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
                const reviewerMentions = reviewersList.map(r => `@${r}`);
                const teamMentions = teamsList.map(t => `@${owner}/${t}`);
                const allMentions = [...reviewerMentions, ...teamMentions].join(', ');

                if (isSuccessful) {
                  return `${BOT_COMMENT_MARKER}\n👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`;
                } else {
                  return `${BOT_COMMENT_MARKER}\n👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`;
                }
              }

              for (const line of codeownersLines) {
                const parts = line.split(/\s+/);
                if (parts.length < 2) continue;

                const pattern = parts[0];
                const owners = parts.slice(1);

                // Use robust glob-to-regex conversion
                const regex = globToRegex(pattern);
                codeownersPatterns.push({ pattern, regex, owners });
              }

              console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);

              // Match changed files against CODEOWNERS patterns
              const matchedOwners = new Set();
              const matchedTeams = new Set();
              const fileMatches = new Map(); // Track which files matched which patterns

              for (const file of changedFiles) {
                for (const { pattern, regex, owners } of codeownersPatterns) {
                  if (regex.test(file)) {
                    console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);

                    if (!fileMatches.has(file)) {
                      fileMatches.set(file, []);
                    }
                    fileMatches.get(file).push({ pattern, owners });

                    // Add owners to the appropriate set (remove @ prefix)
                    for (const owner of owners) {
                      const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
                      if (cleanOwner.includes('/')) {
                        // Team mention (org/team-name)
                        const teamName = cleanOwner.split('/')[1];
                        matchedTeams.add(teamName);
                      } else {
                        // Individual user
                        matchedOwners.add(cleanOwner);
                      }
                    }
                  }
                }
              }

              if (matchedOwners.size === 0 && matchedTeams.size === 0) {
                console.log('No codeowners found for any changed files');
                return;
              }

              // Remove the PR author from reviewers
              const prAuthor = context.payload.pull_request.user.login;
              matchedOwners.delete(prAuthor);

              // Get current reviewers to avoid duplicate requests (but still mention them)
              const { data: prData } = await github.rest.pulls.get({
                owner,
                repo,
                pull_number: pr_number
              });

              const currentReviewers = new Set();
              const currentTeams = new Set();

              if (prData.requested_reviewers) {
                prData.requested_reviewers.forEach(reviewer => {
                  currentReviewers.add(reviewer.login);
                });
              }

              if (prData.requested_teams) {
                prData.requested_teams.forEach(team => {
                  currentTeams.add(team.slug);
                });
              }

              // Check for completed reviews to avoid re-requesting users who have already reviewed
              const { data: reviews } = await github.rest.pulls.listReviews({
                owner,
                repo,
                pull_number: pr_number
              });

              const reviewedUsers = new Set();
              reviews.forEach(review => {
                reviewedUsers.add(review.user.login);
              });

              // Check for previous comments from this workflow to avoid duplicate pings
              const comments = await github.paginate(
                github.rest.issues.listComments,
                {
                  owner,
                  repo,
                  issue_number: pr_number
                }
              );

              const previouslyPingedUsers = new Set();
              const previouslyPingedTeams = new Set();

              // Look for comments from github-actions bot that contain our bot marker
              const workflowComments = comments.filter(comment =>
                comment.user.type === 'Bot' &&
                comment.body.includes(BOT_COMMENT_MARKER)
              );

              // Extract previously mentioned users and teams from workflow comments
              for (const comment of workflowComments) {
                // Match @username patterns (not team mentions)
                const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
                userMentions.forEach(mention => {
                  const username = mention.slice(1); // remove @
                  previouslyPingedUsers.add(username);
                });

                // Match @org/team patterns
                const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || [];
                teamMentions.forEach(mention => {
                  const teamName = mention.split('/')[1];
                  previouslyPingedTeams.add(teamName);
                });
              }

              console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`);

              // Remove users who have already been pinged in previous workflow comments
              previouslyPingedUsers.forEach(user => {
                matchedOwners.delete(user);
              });

              previouslyPingedTeams.forEach(team => {
                matchedTeams.delete(team);
              });

              // Remove only users who have already submitted reviews (not just requested reviewers)
              reviewedUsers.forEach(reviewer => {
                matchedOwners.delete(reviewer);
              });

              // For teams, we'll still remove already requested teams to avoid API errors
              currentTeams.forEach(team => {
                matchedTeams.delete(team);
              });

              const reviewersList = Array.from(matchedOwners);
              const teamsList = Array.from(matchedTeams);

              if (reviewersList.length === 0 && teamsList.length === 0) {
                console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)');
                return;
              }

              const totalReviewers = reviewersList.length + teamsList.length;
              console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);

              // Request reviews
              try {
                const requestParams = {
                  owner,
                  repo,
                  pull_number: pr_number
                };

                // Filter out users who are already requested reviewers for the API call
                const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer));
                const newTeams = teamsList.filter(team => !currentTeams.has(team));

                if (newReviewers.length > 0) {
                  requestParams.reviewers = newReviewers;
                }

                if (newTeams.length > 0) {
                  requestParams.team_reviewers = newTeams;
                }

                // Only make the API call if there are new reviewers to request
                if (newReviewers.length > 0 || newTeams.length > 0) {
                  await github.rest.pulls.requestReviewers(requestParams);
                  console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`);
                } else {
                  console.log('All codeowners are already requested reviewers or have reviewed');
                }

                // Only add a comment if there are new codeowners to mention (not previously pinged)
                if (reviewersList.length > 0 || teamsList.length > 0) {
                  const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);

                  await github.rest.issues.createComment({
                    owner,
                    repo,
                    issue_number: pr_number,
                    body: commentBody
                  });
                  console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
                } else {
                  console.log('No new codeowners to mention in comment (all previously pinged)');
                }
              } catch (error) {
                if (error.status === 422) {
                  console.log('Some reviewers may already be requested or unavailable:', error.message);

                  // Only try to add a comment if there are new codeowners to mention
                  if (reviewersList.length > 0 || teamsList.length > 0) {
                    const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);

                    try {
                      await github.rest.issues.createComment({
                        owner,
                        repo,
                        issue_number: pr_number,
                        body: commentBody
                      });
                      console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
                    } catch (commentError) {
                      console.log('Failed to add comment:', commentError.message);
                    }
                  } else {
                    console.log('No new codeowners to mention in fallback comment');
                  }
                } else {
                  throw error;
                }
              }

            } catch (error) {
              console.log('Failed to process codeowner review requests:', error.message);
              console.error(error);
            }
