'use strict';

const chalk = require('chalk');
const pify = require('pify');

const git = require('./utils/git');

const SEMVER_LABELS = [
  'semver-major',
  'semver-minor',
  'semver-patch',
  'semver-exempt',
];

module.exports = function(vorpal, app) {
  vorpal
    .command('stable-prs')
    .description(
      'Get list of stable pull requests that need to be merged to the stable branch'
    )
    .action(function(args) {
      // This makes the chaining easier but obfuscates the actual API, which is
      // unfortunate. The standalone API will return the right data but
      // promisified will get the response object and then we need to pull data
      // off of that.
      let listMilestones = pify(app.ghissues.listMilestones.bind(app.ghissues));
      let listIssues = pify(app.ghissues.listIssues.bind(app.ghissues));
      let editIssue = pify(app.ghissues.editIssue.bind(app.ghissues));
      let getPullRequest = pify(app.ghrepo.getPullRequest.bind(app.ghrepo));

      let targetMilestone = null;

      return new Promise((resolveAction, rejectAction) => {
        listMilestones(null)
          .then(milestones => {
            app.writeTo('milestones.json', milestones);

            // Turn the milestones into choices for Inquirer
            let milestoneChoices = milestones.map(milestone => {
              return {
                value: milestone.number,
                name: milestone.title,
              };
            });

            // We need label choices too
            let labelChoices = SEMVER_LABELS.map(label => {
              return {
                value: label,
                name: label.split('-')[1], // "major" instead of "semver-major"
              };
            });

            // Ask about source milestone
            // Ask about dest milestone
            // TODO: allow creation of milestone here.
            // Ask about which labels to pull from

            return this.prompt([
              {
                name: 'srcMilestone',
                type: 'list',
                message: 'Which milestone should we pull PRs from?',
                choices: milestoneChoices,
              },
              {
                name: 'destMilestone',
                type: 'list',
                message: 'Which milestone should we assign PRs to upon completion?',
                choices: milestoneChoices,
              },
              {
                name: 'labels',
                type: 'checkbox',
                message: 'Which PRs should we select (use spacebar to check all that apply)',
                choices: labelChoices,
              },
            ]).then(answers => {
              // this.log(JSON.stringify(answers, null, 2));
              targetMilestone = answers.destMilestone;
              let labels = {};
              answers.labels.forEach(label => {
                labels[label] = true;
              });
              return {
                labels: labels,
                query: {
                  milestone: answers.srcMilestone,
                  per_page: 100,
                  state: 'closed',
                },
              };
            });
          })
          // Request issues, filter to applicable PRs
          .then(({labels, query}) => {
            return (
              listIssues(query)
                .then(issues => {
                  app.writeTo('stable-issues.json', issues);

                  // This API *could* return issues that aren't pull requests, so filter out
                  // issues that don't have pull_request set. Also filter out issues that
                  // aren't the right level of semver (eg if running a patch release)
                  let filteringLabels = Object.keys(labels).length > 0;
                  const pulls = issues.filter(issue => {
                    if (!issue.pull_request) {
                      return false;
                    }

                    if (!filteringLabels) {
                      return true;
                    }

                    return issue.labels.some(label => labels[label.name]);
                  });
                  app.writeTo('stable-prs.json', pulls);
                  return pulls;
                })
                // We need to convert the issues to PRs. We don't actually have enough
                // info for the pull request data, so we need to get more. Then we'll
                // do some filtering and sorting to make sure we apply merged PRs in
                // the order they were originally committed to avoid conflicts as much
                // as possible.
                .then(pulls => {
                  return Promise.all(
                    pulls.map(pr => {
                      return getPullRequest(pr.number).then(richPR => {
                        app.writeTo(`pr-${pr.number}.json`, richPR);
                        richPR.__originalIssue = pr;
                        return richPR;
                      });
                    })
                  ).then(richPRs => {
                    return richPRs
                      .filter(pr => {
                        if (!pr.merged_at) {
                          this.log(
                            `${chalk.yellow.bold('WARNING')} ${pr.html_url} was not merged,` +
                              ` should have the milestone unset.`
                          );
                          return false;
                        }
                        return true;
                      })
                      .map(pr => {
                        pr.merged_at_date = new Date(pr.merged_at);
                        return pr;
                      })
                      .sort((a, b) => a.merged_at_date - b.merged_at_date);
                  });
                })
            );
          })
          // Quick prompt to double check that we should proceed.
          .then(pulls => {
            this.log(`Found ${chalk.bold(pulls.length)} pull requests:`);
            pulls.forEach(pr => {
              this.log(`${pr.html_url}: ${chalk.bold(pr.title)}`);
            });

            return this.prompt({
              name: 'merge',
              type: 'confirm',
              message: `Merge these ${pulls.length} pull requests?`,
            }).then(answers => {
              return answers.merge ? pulls : rejectAction('cancelled');
            });
          })
          // Ok, now we finally have rich pull request data. We can start cherry picking…
          .then(pulls => {
            // We're going to do some error handling here so we don't get into a
            // terrible state.
            this.log(`Found ${chalk.bold(pulls.length)} pull requests:`);
            return new Promise((resolve, reject) => {
              cherryPickPRs
                .call(this, app, pulls)
                .then(results => {
                  resolve(results);
                })
                .catch(err => {
                  this.log(
                    `${chalk.red.bold('ERROR')} Something went wrong and your repo is` +
                      ` probably in a bad state. Sorry.`
                  );
                  resolve({
                    successful: [],
                    skipped: [],
                    didAbort: true,
                  });
                });
            });
          })
          // Update the milestone on successful PRs
          // TODO: maybe handle didAbort and git reset --hard to a rev we read when we start the process?
          .then(({successful, aborted, didAbort}) => {
            if (didAbort) {
              return undefined;
            }

            return Promise.all(
              successful.map(pr => {
                return editIssue(pr.number, {milestone: targetMilestone});
              })
            );
          })
          // yay, we're done
          .then(() => {
            resolveAction();
          })
          .catch(err => {
            this.log('ERROR', err);
            rejectAction();
          });
      });
    });
};

function cherryPickPRs(app, prs) {
  let successful = [];
  let skipped = [];
  return new Promise((resolve, reject) => {
    // Build array of thenables
    let promises = prs.map(pr => {
      return () =>
        new Promise((res, rej) => {
          this.log(
            chalk.yellow(`Cherry-picking #${pr.number} (${pr.title})...`)
          );
          let failed = false;
          try {
            git.cherryPickMerge(app, pr.merge_commit_sha);
          } catch (e) {
            failed = true;
          }

          if (!failed) {
            this.log(chalk.green`Success`);
            successful.push(pr);
            return res();
          }

          return this.prompt({
            name: 'handle',
            type: 'list',
            message: `${chalk.red`Failed!`} ${chalk.yellow('This must be resolved manually!')}`,
            choices: [
              {value: 'ok', name: 'Continue, mark successful'},
              {value: 'skip', name: 'Continue, mark skipped'},
              {
                value: 'abort',
                name: 'Abort process. Will require manual resetting of git state.',
              },
            ],
          }).then(answers => {
            switch (answers.handle) {
              case 'ok':
                successful.push(pr);
                break;
              case 'skip':
                skipped.push(pr);
                break;
              case 'abort':
                return rej(pr.number);
            }
            res(pr.number);
          });
        });
    });

    // Since promises run on creation and we don't actually want that, we create
    // an array of functions that return promises. We'll chain them here, not
    // actually creating the next promise until we're ready.
    var p = promises[0]();
    for (let i = 1; i < promises.length; i++) {
      p = p.then(() => promises[i]());
    }
    p
      .then(() => {
        resolve({successful, skipped, didAbort: false});
      })
      .catch(e => {
        resolve({successful, skipped, didAbort: true});
      });
  });
}
