/**
 * @file Script that can be used to close stale theme PRs that have a `invalid` label.
 */
import * as dotenv from "dotenv";
dotenv.config();

import { debug, setFailed } from "@actions/core";
import github from "@actions/github";
import { RequestError } from "@octokit/request-error";
import { getGithubToken, getRepoInfo } from "./helpers.js";

const CLOSING_COMMENT = `
	\rThis theme PR has been automatically closed due to inactivity. Please reopen it if you want to continue working on it.\
	\rThank you for your contributions.
`;
const REVIEWER = "github-actions[bot]";

/**
 * Retrieve the review user.
 * @returns {string} review user.
 */
const getReviewer = () => {
  return process.env.REVIEWER ? process.env.REVIEWER : REVIEWER;
};

/**
 * Fetch open PRs from a given repository.
 *
 * @param {module:@actions/github.Octokit} octokit The octokit client.
 * @param {string} user The user name of the repository owner.
 * @param {string} repo The name of the repository.
 * @param {string} reviewer The reviewer to filter by.
 * @returns {Promise<Object[]>} The open PRs.
 */
export const fetchOpenPRs = async (octokit, user, repo, reviewer) => {
  const openPRs = [];
  let hasNextPage = true;
  let endCursor;
  while (hasNextPage) {
    try {
      const { repository } = await octokit.graphql(
        `
            {
              repository(owner: "${user}", name: "${repo}") {
                open_prs: pullRequests(${
                  endCursor ? `after: "${endCursor}", ` : ""
                }
                  first: 100, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) {
                  nodes {
                    number
										commits(last:1){
											nodes{
												commit{
													pushedDate
												}
											}
										}
                    labels(first: 100, orderBy:{field: CREATED_AT, direction: DESC}) {
                      nodes {
												name
                      }
                    }
                    reviews(first: 100, states: CHANGES_REQUESTED, author: "${reviewer}") {
											nodes {
                        submittedAt
											}
                    }
                  }
                  pageInfo {
                    endCursor
                    hasNextPage
                  }
                }
              }
            }
          `,
      );
      openPRs.push(...repository.open_prs.nodes);
      hasNextPage = repository.open_prs.pageInfo.hasNextPage;
      endCursor = repository.open_prs.pageInfo.endCursor;
    } catch (error) {
      if (error instanceof RequestError) {
        setFailed(`Could not retrieve top PRs using GraphQl: ${error.message}`);
      }
      throw error;
    }
  }
  return openPRs;
};

/**
 * Retrieve pull requests that have a given label.
 *
 * @param {Object[]} pulls The pull requests to check.
 * @param {string} label The label to check for.
 * @returns {Object[]} The pull requests that have the given label.
 */
export const pullsWithLabel = (pulls, label) => {
  return pulls.filter((pr) => {
    return pr.labels.nodes.some((lab) => lab.name === label);
  });
};

/**
 * Check if PR is stale. Meaning that it hasn't been updated in a given time.
 *
 * @param {Object} pullRequest request object.
 * @param {number} staleDays number of days.
 * @returns {boolean} indicating if PR is stale.
 */
const isStale = (pullRequest, staleDays) => {
  const lastCommitDate = new Date(
    pullRequest.commits.nodes[0].commit.pushedDate,
  );
  if (pullRequest.reviews.nodes[0]) {
    const lastReviewDate = new Date(
      pullRequest.reviews.nodes.sort((a, b) => (a < b ? 1 : -1))[0].submittedAt,
    );
    const lastUpdateDate =
      lastCommitDate >= lastReviewDate ? lastCommitDate : lastReviewDate;
    const now = new Date();
    return (now - lastUpdateDate) / (1000 * 60 * 60 * 24) >= staleDays;
  } else {
    return false;
  }
};

/**
 * Main function.
 *
 * @returns {Promise<void>} A promise.
 */
const run = async () => {
  try {
    // Create octokit client.
    const dryRun = process.env.DRY_RUN === "true" || false;
    const staleDays = process.env.STALE_DAYS || 20;
    debug("Creating octokit client...");
    const octokit = github.getOctokit(getGithubToken());
    const { owner, repo } = getRepoInfo(github.context);
    const reviewer = getReviewer();

    // Retrieve all theme pull requests.
    debug("Retrieving all theme pull requests...");
    const prs = await fetchOpenPRs(octokit, owner, repo, reviewer);
    const themePRs = pullsWithLabel(prs, "themes");
    const invalidThemePRs = pullsWithLabel(themePRs, "invalid");
    debug("Retrieving stale theme PRs...");
    const staleThemePRs = invalidThemePRs.filter((pr) =>
      isStale(pr, staleDays),
    );
    const staleThemePRsNumbers = staleThemePRs.map((pr) => pr.number);
    debug(`Found ${staleThemePRs.length} stale theme PRs`);

    // Loop through all stale invalid theme pull requests and close them.
    for (const prNumber of staleThemePRsNumbers) {
      debug(`Closing #${prNumber} because it is stale...`);
      if (dryRun) {
        debug("Dry run enabled, skipping...");
      } else {
        await octokit.rest.issues.createComment({
          owner,
          repo,
          issue_number: prNumber,
          body: CLOSING_COMMENT,
        });
        await octokit.rest.pulls.update({
          owner,
          repo,
          pull_number: prNumber,
          state: "closed",
        });
      }
    }
  } catch (error) {
    setFailed(error.message);
  }
};

run();
