import { Octokit } from '@octokit/rest';
import { minimatch } from 'minimatch';
import pLimit from 'p-limit';

const limit = pLimit(50);

/** Repository-specific configuration for points calculation */
const REPOSITORY_CONFIG = new Map([
  [
    'actual',
    {
      POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
      POINTS_PER_ISSUE_CLOSING_ACTION: 1,
      POINTS_PER_RELEASE_PR: 0,
      PR_REVIEW_POINT_TIERS: [
        { minChanges: 500, points: 8 },
        { minChanges: 100, points: 6 },
        { minChanges: 10, points: 2 },
        { minChanges: 0, points: 1 },
      ],
      EXCLUDED_FILES: [
        'yarn.lock',
        '.yarn/**/*',
        'packages/component-library/src/icons/**/*',
        'release-notes/**/*',
      ],
    },
  ],
  [
    'docs',
    {
      POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
      POINTS_PER_ISSUE_CLOSING_ACTION: 1,
      POINTS_PER_RELEASE_PR: 4,
      PR_REVIEW_POINT_TIERS: [
        { minChanges: 2000, points: 6 },
        { minChanges: 200, points: 4 },
        { minChanges: 0, points: 2 },
      ],
      EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
    },
  ],
]);

/**
 * Get the start and end dates for the last month.
 * @returns {Object} An object containing the start and end dates.
 */
function getLastMonthDates() {
  // Get data relating to the last month
  const now = new Date();
  // Always use UTC for calculations
  const firstDayOfLastMonth = new Date(
    Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1, 0, 0, 0, 0),
  );
  const since = process.env.START_DATE
    ? new Date(Date.parse(process.env.START_DATE))
    : firstDayOfLastMonth;

  // Calculate the end of the month for the since date in UTC
  const until = new Date(
    Date.UTC(
      since.getUTCFullYear(),
      since.getUTCMonth() + 1,
      0,
      23,
      59,
      59,
      999,
    ),
  );

  return { since, until };
}

/**
 * Used for calculating the monthly points each core contributor has earned.
 * These are used for payouts depending.
 * @param {string} repo - The repository to analyze ('actual' or 'docs')
 * @returns {number} The total points earned for the repository
 */
async function countContributorPoints(repo) {
  const octokit = new Octokit({
    auth: process.env.GITHUB_TOKEN,
  });
  const owner = 'actualbudget';
  const config = REPOSITORY_CONFIG.get(repo);

  const { since, until } = getLastMonthDates();

  // Get organization members
  const { data: orgMembers } = await octokit.orgs.listMembers({
    org: owner,
  });
  const orgMemberLogins = new Set(orgMembers.map(member => member.login));

  // Initialize stats map with all org members
  const stats = new Map(
    Array.from(orgMemberLogins).map(login => [
      login,
      {
        reviews: [], // Will store objects with PR number and points
        labelRemovals: [],
        issueClosings: [],
        points: 0,
      },
    ]),
  );

  // Helper function to print statistics
  const printStats = (title, getValue, formatLine) => {
    console.log(`\n${title}:`);
    console.log('='.repeat(title.length + 1));

    const entries = Array.from(stats.entries())
      .map(([user, userStats]) => [user, getValue(userStats)])
      .filter(([, count]) => count > 0)
      .sort((a, b) => b[1] - a[1]);

    if (entries.length === 0) {
      console.log(`No ${title.toLowerCase()} found in the last month.`);
    } else {
      entries.forEach(([user, count]) => {
        console.log(formatLine(user, count));
      });
    }
  };

  // Get all PRs using search
  const searchQuery = `repo:${owner}/${repo} is:pr is:merged merged:${since.toISOString()}..${until.toISOString()}`;
  const recentPRs = await octokit.paginate(
    'GET /search/issues',
    {
      q: searchQuery,
      per_page: 100,
      advanced_search: true,
    },
    response => response.data.filter(pr => pr.number),
  );

  // Get reviews and PR details for each PR
  await Promise.all(
    recentPRs.map(pr =>
      limit(async () => {
        const [reviews, modifiedFiles] = await Promise.all([
          octokit.pulls.listReviews({ owner, repo, pull_number: pr.number }),
          octokit.paginate(
            octokit.pulls.listFiles,
            {
              owner,
              repo,
              pull_number: pr.number,
              per_page: 100,
            },
            res => res.data,
          ),
        ]);

        const totalChanges = modifiedFiles
          .filter(
            file =>
              !config.EXCLUDED_FILES.some(pattern =>
                minimatch(file.filename, pattern),
              ),
          )
          .reduce((sum, file) => sum + file.additions + file.deletions, 0);

        const isReleasePR = pr.title.match(/🔖.*\d+\.\d+\.\d+/);
        const prPoints =
          config.PR_REVIEW_POINT_TIERS.find(t => totalChanges >= t.minChanges)
            ?.points ?? 0;

        if (isReleasePR) {
          if (stats.has(pr.user.login)) {
            const creatorStats = stats.get(pr.user.login);
            creatorStats.reviews.push({
              pr: pr.number.toString(),
              points: config.POINTS_PER_RELEASE_PR,
              isReleaseCreator: true,
            });
            creatorStats.points += config.POINTS_PER_RELEASE_PR;
          }
        } else {
          const uniqueReviewers = new Set();
          reviews.data
            .filter(
              review =>
                stats.has(review.user?.login) &&
                review.state === 'APPROVED' &&
                !uniqueReviewers.has(review.user?.login),
            )
            .forEach(({ user: { login: reviewer } }) => {
              uniqueReviewers.add(reviewer);
              const userStats = stats.get(reviewer);
              userStats.reviews.push({
                pr: pr.number.toString(),
                points: prPoints,
              });
              userStats.points += prPoints;
            });
        }
      }),
    ),
  );

  // Get all issues with label events in the last month
  const issues = await octokit.paginate(octokit.issues.listForRepo, {
    owner,
    repo,
    state: 'all',
    sort: 'updated',
    direction: 'desc',
    per_page: 100,
    since: since.toISOString(),
  });

  // Get label events for each issue
  await Promise.all(
    issues.map(issue =>
      limit(async () => {
        const { data: events } = await octokit.issues.listEventsForTimeline({
          owner,
          repo,
          issue_number: issue.number,
        });

        events
          .filter(event => {
            const createdAt = new Date(event.created_at);
            return (
              createdAt.getTime() > since.getTime() &&
              createdAt.getTime() <= until.getTime() &&
              stats.has(event.actor?.login)
            );
          })
          .forEach(event => {
            if (
              event.event === 'unlabeled' &&
              event.label?.name.toLowerCase() === 'needs triage'
            ) {
              const remover = event.actor.login;
              const userStats = stats.get(remover);
              userStats.labelRemovals.push(issue.number.toString());
              userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
            }

            if (
              event.event === 'closed' &&
              event.state_reason === 'not_planned'
            ) {
              const closer = event.actor.login;
              const userStats = stats.get(closer);
              userStats.issueClosings.push(issue.number.toString());
              userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
            }
          });
      }),
    ),
  );

  // Print all statistics
  printStats(
    `PR Review Statistics (${repo})`,
    stats => stats.reviews.length,
    (user, count) =>
      `${user}: ${count} (PRs: ${stats
        .get(user)
        .reviews.map(r => {
          if (r.isReleaseCreator) {
            return `#${r.pr} (${r.points}pts - Release Creator)`;
          }
          return `#${r.pr} (${r.points}pts)`;
        })
        .join(', ')})`,
  );
  printStats(
    `"Needs Triage" Label Removal Statistics (${repo})`,
    stats => stats.labelRemovals.length,
    (user, count) =>
      `${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
  );
  printStats(
    `Issue Closing Statistics (${repo})`,
    stats => stats.issueClosings.length,
    (user, count) =>
      `${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
  );

  // Print points summary
  printStats(
    `Points Summary (${repo})`,
    stats => stats.points,
    (user, userPoints) => `${user}: ${userPoints}`,
  );

  // Calculate and print total points
  const totalPoints = Array.from(stats.values()).reduce(
    (sum, userStats) => sum + userStats.points,
    0,
  );
  console.log(`\nTotal points earned for ${repo}: ${totalPoints}`);

  // Return the points
  return new Map(
    Array.from(stats.entries()).map(([login, userStats]) => [
      login,
      userStats.points,
    ]),
  );
}

/**
 * Calculate the points for both repositories and print cumulative results
 */
async function calculateCumulativePoints() {
  // Get stats for each repository
  const repoPointsResults = await Promise.all(
    Array.from(REPOSITORY_CONFIG.keys()).map(countContributorPoints),
  );

  // Calculate cumulative stats
  const cumulativeStats = new Map(repoPointsResults[0]);

  // Combine stats from all repositories
  for (let i = 1; i < repoPointsResults.length; i++) {
    for (const [login, points] of repoPointsResults[i].entries()) {
      if (!cumulativeStats.has(login)) {
        cumulativeStats.set(login, 0);
      }

      cumulativeStats.set(login, cumulativeStats.get(login) + points);
    }
  }

  // Print cumulative statistics
  console.log('\n\nCUMULATIVE STATISTICS ACROSS ALL REPOSITORIES');
  console.log('='.repeat(50));

  console.log('\nCumulative Points Summary:');
  console.log('='.repeat('Cumulative Points Summary'.length + 1));

  const entries = Array.from(cumulativeStats.entries())
    .filter(([, count]) => count > 0)
    .sort((a, b) => b[1] - a[1]);

  if (entries.length === 0) {
    console.log('No cumulative points summary found.');
  } else {
    entries.forEach(([user, points]) => {
      console.log(`${user}: ${points}`);
    });
  }

  // Calculate and print total cumulative points
  const totalCumulativePoints = Array.from(cumulativeStats.values()).reduce(
    (sum, points) => sum + points,
    0,
  );
  console.log('\nTotal cumulative points earned: ' + totalCumulativePoints);
}

// Run the calculations
calculateCumulativePoints().catch(console.error);
