"use strict";

/**
 * This Node script fetches MSC proposals and stores them in /data/msc,
 * so they can be used by a Hugo template to render summary tables of them
 * in the specification.
 *
 * In detail, it:
 * - fetches all GitHub issues from matrix-spec-proposals that have the `proposal` label
 * - groups them by their state in the MSC process
 * - does some light massaging of them so it's easier for the Hugo template to work with them
 * - store them at /data/msc
 */

// built-in modules
const path = require('path');
const fs = require('fs');

// third-party modules
const fetch = require('node-fetch');

// We will write proposals into the /data/msc directory
const outputDir = path.join(__dirname, "../data/msc");

/**
 * This defines the different proposal lifecycle states.
 * Each state has:
 * - `label`: a GitHub label used to identify issues in this state
 * - `title`: used for things like headings in renderings of the proposals
 */
const states = [
  {
    label: "proposed-final-comment-period",
    title: "Proposed Final Comment Period"
  },
  {
    label: "final-comment-period",
    title: "Final Comment Period"
  },
  {
    label: "finished-final-comment-period",
    title: "Finished Final Comment Period"
  },
  {
    label: "spec-pr-missing",
    title: "Spec PR Missing"
  },
  {
    label: "spec-pr-in-review",
    title: "Spec PR In Review"
  },
  {
    label: "merged",
    title: "Merged"
  },
  {
    label: "proposal-postponed",
    title: "Postponed"
  },
  {
    label: "abandoned",
    title: "Abandoned"
  },
  {
    label: "obsolete",
    title: "Obsolete"
  }
];

let issues = [];

/**
 * Fetch all the MSC proposals from GitHub.
 *
 * GitHub only lets us fetch 100 items at a time, and it gives us a `link`
 * response header containing the URL for the next batch.
 * So we will keep fetching until the response doesn't contain the "next" link.
 */
async function getIssues() {

  /**
   * A pretty ugly function to get us the "next" link in the header if there
   * was one, or `null` otherwise.
   */
  function getNextLink(header) {
    const links = header.split(",");
    for (const link of links) {
      const linkPieces = link.split(";");
      if (linkPieces[1] == ` rel=\"next\"`) {
        const next = linkPieces[0].trim();
        return next.substring(1, next.length-1);
      }
    }
    return null;
  }

  let pageLink = "https://api.github.com/repos/matrix-org/matrix-spec-proposals/issues?state=all&labels=proposal&per_page=100";
  while (pageLink) {
    const response = await fetch(pageLink);
    const issuesForPage = await response.json();
    issues = issues.concat(issuesForPage);
    const linkHeader = response.headers.get("link");
    pageLink = !!linkHeader ? getNextLink(linkHeader) : null;
  }
}

getIssues().then(processIssues);

/**
 * Rather than just store the complete issue, we'll extract
 * only the pieces we need.
 * We'll also do some transformation of the issues, just because
 * it's easier to do in JS than in the template.
 */
function getProposalFromIssue(issue) {

  /**
   * A helper function to fetch the contents of special
   * directives in the issue body.
   * Looks for a directive in the format:
   * `^${directiveName}: (.+?)$`, returning the matched
   *  part or null if the directive wasn't found.
   */
  function getDirective(directiveName, issue) {
    const re = new RegExp(`^${directiveName}: (.+?)$`, "m");
    const found = issue.body?.match(re);
    return found? found[1]: null;
  }

  function getDocumentation(issue) {
    const found = getDirective("Documentation", issue);
    if (found) {
      return found.split(",").map(a => a.trim());
    } else {
      return [];
    }
  }

  function getAuthors(issue) {
    const found = getDirective("Author", issue);
    if (found) {
      return found.split(",").map(a => a.trim().substr(1));
    } else {
      return [`${issue.user.login}`];
    }
  }

  function getShepherd(issue) {
    const found = getDirective("Shepherd", issue);
    if (found) {
      return found.substr(1);
    } else {
      return null;
    }
  }

  return {
    number: issue.number,
    url: issue.html_url,
    title: issue.title,
    created_at: issue.created_at.substr(0, 10),
    updated_at: issue.updated_at.substr(0, 10),
    authors: getAuthors(issue),
    shepherd: getShepherd(issue),
    documentation: getDocumentation(issue)
  }
}

/**
 * Returns the intersection of two arrays.
 */
function intersection(array1, array2) {
  return array1.filter(i => array2.includes(i));
}

/**
 * Given all the GitHub issues with a "proposal" label:
 * - group issues by the state they are in, and for each group:
 *     - extract the bits we need from each issue
 *     - write the result under /data/msc
 */
function processIssues()  {
  if (!fs.existsSync(outputDir)){
    fs.mkdirSync(outputDir);
  }
  const output = [];

  // make a group of "work in progress" proposals, for those in draft.
  output.push({
    title: "Work In Progress",
    label: "work-in-progress",
    proposals: issues
      .filter(issue => issue.state == "open" && issue.draft)
      .map(issue => getProposalFromIssue(issue))
  });

  // now the 'Proposal In Review' section, which are identified by
  // not having any of the state labels
  const stateLabels = states.map(s => s.label);
  const proposalsInReview = issues.filter(issue => {
    const labelsForIssue = issue.labels.map(l => l.name);
    return !issue.draft && intersection(labelsForIssue, stateLabels).length === 0;
  });
  output.push({
    title: "Proposal In Review",
    label: "proposal-in-review",
    proposals: proposalsInReview.map(issue => getProposalFromIssue(issue))
  });

  // for each defined state
  for (const state of states) {
    // get the set of issues for that state
    const issuesForState = issues.filter(msc => {
      return !msc.draft && msc.labels.some(l => l.name === state.label);
    });
    // store it in /data
    output.push({
      title: state.title,
      label: state.label,
      proposals: issuesForState.map(issue => getProposalFromIssue(issue))
    });
  }
  const outputData = JSON.stringify(output, null, '\t');
  const outputFile = path.join(outputDir, `proposals.json`);
  fs.writeFileSync(outputFile, outputData);
}
