/* eslint-disable no-await-in-loop */
import { Octokit } from "@octokit/rest";
import commandLineArgs from "command-line-args";

const owner = "danielyxie";
const repo = "bitburner"
const basePath = `https://github.com/${owner}/${repo}`;

const cliArgs = commandLineArgs([
  { name: 'from', alias: 'f', type: String },
  { name: 'to', alias: 't', type: String },
  { name: 'detailed', alias: 'd', type: Boolean }
]);

class MergeChangelog {
  constructor(options) {
    this.octokit = new Octokit(options);
  }

  async getCommitsSearchResults(query) {
    const iterator = this.octokit.paginate.iterator(
      this.octokit.rest.search.commits,
      {
        owner, repo,
        q: query,
        sort: 'updated',
        direction: 'desc',
      },
    );
    const searchResults = [];
    for await (const response of iterator) {
      const entries = response.data.map((entry) => ({
        sha: entry.sha,
        url: entry.html_url,
        user: {
          id: entry.author?.id,
          login: entry.author?.login,
          avatar: entry.author?.avatar_url,
          url: entry.author?.html_url,
        },
        commit_date: entry.commit.committer.date,
        message: entry.commit.message,
      }));
      searchResults.push(...entries);
    }
    return searchResults;
  }

  async getPullsSearchResults(query) {
    const iterator = this.octokit.paginate.iterator(
      this.octokit.rest.search.issuesAndPullRequests,
      {
        owner, repo,
        q: query,
        sort: 'committer-date',
        direction: 'desc',
      },
    );

    const searchResults = [];
    for await (const response of iterator) {
      const entries = response.data.map((entry) => ({
        id: entry.id,
        number: entry.number,
        created_at: entry.updated_at,
        merged_at: entry.pull_request.merged_at,
        url: entry.pull_request.html_url,
        title: entry.title,
        body: entry.body,
        diff: entry.diff_url,
        patch: entry.patch_url,
        user: {
          id: entry.user.id,
          login: entry.user.login,
          avatar: entry.user.avatar_url,
          url: entry.user.html_url,
        },
      }));
      searchResults.push(...entries);
    }

    const pulls = [];
    for (const entry of searchResults) {
      const r = await (this.octokit.rest.pulls.get({
        owner, repo,
        pull_number: entry.number,
      }).then((response) => ({
        ...entry,
        merge_commit_sha: response.data.merge_commit_sha,
        head_commit_sha: response.data.head.sha,
      })));
      pulls.push(r);
      await sleep(1000);
    }
    return pulls;
  }

  async getCommit(sha) {
    const response = await this.octokit.rest.git.getCommit({
      owner, repo,
      commit_sha: sha,
    });
    const commit = {
      date: response.data.committer.date,
      message: response.data.message,
      sha: response.data.sha,
      url: response.data.html_url,
    }
    return commit;
  }

  async getPullsMergedBetween(sha_from, sha_to) {
    const from = {};
    const to = {};
    from.commit = await this.getCommit(sha_from);
    from.date = new Date(from.commit.date);

    if (!sha_to) {
      const newest = await this.getLastCommitByBranch('dev');
      to.commit = await this.getCommit(newest)
    } else {
      to.commit = await this.getCommit(sha_to);
    }

    to.date = new Date(to.commit.date);

    const commitQuery = `user:${owner} repo:${repo} merge:false committer-date:"${from.date.toISOString()}..${to.date.toISOString()}"`;
    const pullQuery = `user:${owner} repo:${repo} is:pr is:merged merged:"${from.date.toISOString()}..${to.date.toISOString()}"`;

    const commits = await this.getCommitsSearchResults(commitQuery);
    await sleep(5000)
    const pulls = await this.getPullsSearchResults(pullQuery);
    await sleep(5000)
    // We only have the merge commit sha & the HEAD sha in this data, but it can exclude some entries
    const pullsCommitSha = pulls.
      map((p) => [p.merge_commit_sha, p.head_commit_sha]).
      reduce((all, current) => [...all, ...current]);

    let danglingCommits = commits.filter((c) => !pullsCommitSha.includes(c.sha));
    const listPullsPromises = [];
    for (const commit of danglingCommits) {
      const promise = this.octokit.rest.repos.listPullRequestsAssociatedWithCommit({
        owner, repo, commit_sha: commit.sha
      }).then((response) => ({
        ...commit,
        nbPulls: response.data.length,
      }));
      listPullsPromises.push(promise);
    }

    const commitsThatAreIncludedInPulls = (await Promise.all(listPullsPromises)).
      filter((c) => c.nbPulls > 0).
      map((c) => c.sha);

    danglingCommits = danglingCommits.
      filter((c) => !commitsThatAreIncludedInPulls.includes(c.sha));
    return {
      from,
      to,
      pulls,
      danglingCommits,
      pullQuery,
      commitQuery,
    }
  }

  async getLastCommitByBranch(branch) {
    const response = await this.octokit.rest.repos.getBranch({
      owner,
      repo,
      branch,
    });
    return response.data.commit.sha;
  }

  async getChangelog(from, to, detailedOutput) {
    const changes = await this.getPullsMergedBetween(from, to);
    const pullLines = changes.pulls.map((line) => this.getPullMarkdown(line, detailedOutput));
    const commitLines = changes.danglingCommits.map((line) => this.getCommitMarkdown(line, detailedOutput));
    commitLines.push(`* Nerf noodle bar.`)
    const shortFrom = changes.from.date.toISOString().split('T')[0];
    const shortTo = changes.to.date.toISOString().split('T')[0]
    const shortFromSha = changes.from.commit.sha.slice(0, 7);
    const shortToSha = changes.to.commit.sha.slice(0, 7);
    const title = `## [draft] v1.x.x - ${shortFrom} to ${shortTo}`;
    let log = `
${title}

#### Information

Modifications included between **${shortFrom}** and **${shortTo}** (\`${shortFromSha}\` to \`${shortToSha}\`).

*[See Pull Requests on GitHub](https://github.com/search?q=${encodeURIComponent(changes.pullQuery)})*

#### Merged Pull Requests

${pullLines.join('\n')}

`;

    if (commitLines.length > 0) {
      log += `
#### Other Changes

${commitLines.join('\n')}
`;
    }
    return {
      log: log.trim(),
      changes: changes,
    };
  }

  getPullMarkdown(pr, detailedOutput) {
    if (!detailedOutput) {
      return `* ` +
        `${pr.title} (by @${pr.user.login})` +
        ` #[${pr.number}](${pr.url})`;
    } else {
      return `* [${pr.merge_commit_sha.slice(0, 7)}](${basePath}/commit/${pr.merge_commit_sha}) | ` +
        `${pr.title} ([@${pr.user.login}](${pr.user.url}))` +
        ` PR #[${pr.number}](${pr.url})`;
      }
  }

  getCommitMarkdown(commit, detailedOutput) {
    if (!detailedOutput) {
      return `* ` +
      `${commit.message} (by @${commit.user.login})` +
      ` - [${commit.sha.slice(0, 7)}](${commit.url})`;
    } else {
      return `* [${commit.sha.slice(0, 7)}](${commit.url}) | ` +
      `${commit.message} ([@${commit.user.login}](${commit.user.url}))`;
    }
  }
}

const sleep = async (wait) => {
  return new Promise((resolve) => {
    setTimeout(resolve, wait)
  })
}

const api = new MergeChangelog({ auth: process.env.GITHUB_API_TOKEN });
api.getChangelog(cliArgs.from, cliArgs.to, cliArgs.detailed).then((data) => {
  console.log(data.log);
});