/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */


import {getConfig, getRepoBaseDir} from '../../utils/config';
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
import {GithubApiRequestError} from '../../utils/git/github';
import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/git/github-urls';
import {GitClient} from '../../utils/git/index';

import {loadAndValidateConfig, MergeConfigWithRemote} from './config';
import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';

/**
 * Merges a given pull request based on labels configured in the given merge configuration.
 * Pull requests can be merged with different strategies such as the Github API merge
 * strategy, or the local autosquash strategy. Either strategy has benefits and downsides.
 * More information on these strategies can be found in their dedicated strategy classes.
 *
 * See {@link GithubApiMergeStrategy} and {@link AutosquashMergeStrategy}
 *
 * @param prNumber Number of the pull request that should be merged.
 * @param githubToken Github token used for merging (i.e. fetching and pushing)
 * @param projectRoot Path to the local Git project that is used for merging.
 * @param config Configuration for merging pull requests.
 */
export async function mergePullRequest(
    prNumber: number, githubToken: string, projectRoot: string = getRepoBaseDir(),
    config?: MergeConfigWithRemote) {
  // Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
  // rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
  process.env['HUSKY'] = '0';

  const api = await createPullRequestMergeTask(githubToken, projectRoot, config);

  // Perform the merge. Force mode can be activated through a command line flag.
  // Alternatively, if the merge fails with non-fatal failures, the script
  // will prompt whether it should rerun in force mode.
  if (!await performMerge(false)) {
    process.exit(1);
  }

  /** Performs the merge and returns whether it was successful or not. */
  async function performMerge(ignoreFatalErrors: boolean): Promise<boolean> {
    try {
      const result = await api.merge(prNumber, ignoreFatalErrors);
      return await handleMergeResult(result, ignoreFatalErrors);
    } catch (e) {
      // Catch errors to the Github API for invalid requests. We want to
      // exit the script with a better explanation of the error.
      if (e instanceof GithubApiRequestError && e.status === 401) {
        error(red('Github API request failed. ' + e.message));
        error(yellow('Please ensure that your provided token is valid.'));
        error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
        process.exit(1);
      }
      throw e;
    }
  }

  /**
   * Prompts whether the specified pull request should be forcibly merged. If so, merges
   * the specified pull request forcibly (ignoring non-critical failures).
   * @returns Whether the specified pull request has been forcibly merged.
   */
  async function promptAndPerformForceMerge(): Promise<boolean> {
    if (await promptConfirm('Do you want to forcibly proceed with merging?')) {
      // Perform the merge in force mode. This means that non-fatal failures
      // are ignored and the merge continues.
      return performMerge(true);
    }
    return false;
  }

  /**
   * Handles the merge result by printing console messages, exiting the process
   * based on the result, or by restarting the merge if force mode has been enabled.
   * @returns Whether the merge completed without errors or not.
   */
  async function handleMergeResult(result: MergeResult, disableForceMergePrompt = false) {
    const {failure, status} = result;
    const canForciblyMerge = failure && failure.nonFatal;

    switch (status) {
      case MergeStatus.SUCCESS:
        info(green(`Successfully merged the pull request: #${prNumber}`));
        return true;
      case MergeStatus.DIRTY_WORKING_DIR:
        error(
            red(`Local working repository not clean. Please make sure there are ` +
                `no uncommitted changes.`));
        return false;
      case MergeStatus.UNKNOWN_GIT_ERROR:
        error(
            red('An unknown Git error has been thrown. Please check the output ' +
                'above for details.'));
        return false;
      case MergeStatus.GITHUB_ERROR:
        error(red('An error related to interacting with Github has been discovered.'));
        error(failure!.message);
        return false;
      case MergeStatus.USER_ABORTED:
        info(`Merge of pull request has been aborted manually: #${prNumber}`);
        return true;
      case MergeStatus.FAILED:
        error(yellow(`Could not merge the specified pull request.`));
        error(red(failure!.message));
        if (canForciblyMerge && !disableForceMergePrompt) {
          info();
          info(yellow('The pull request above failed due to non-critical errors.'));
          info(yellow(`This error can be forcibly ignored if desired.`));
          return await promptAndPerformForceMerge();
        }
        return false;
      default:
        throw Error(`Unexpected merge result: ${status}`);
    }
  }
}

/**
 * Creates the pull request merge task from the given Github token, project root
 * and optional explicit configuration. An explicit configuration can be specified
 * when the merge script is used outside of a `ng-dev` configured repository.
 */
async function createPullRequestMergeTask(
    githubToken: string, projectRoot: string, explicitConfig?: MergeConfigWithRemote) {
  if (explicitConfig !== undefined) {
    const git = new GitClient(githubToken, {github: explicitConfig.remote}, projectRoot);
    return new PullRequestMergeTask(explicitConfig, git);
  }

  const devInfraConfig = getConfig();
  const git = new GitClient(githubToken, devInfraConfig, projectRoot);
  const {config, errors} = await loadAndValidateConfig(devInfraConfig, git.github);

  if (errors) {
    error(red('Invalid merge configuration:'));
    errors.forEach(desc => error(yellow(`  -  ${desc}`)));
    process.exit(1);
  }

  // Set the remote so that the merge tool has access to information about
  // the remote it intends to merge to.
  config!.remote = devInfraConfig.github;
  // We can cast this to a merge config with remote because we always set the
  // remote above.
  return new PullRequestMergeTask(config! as MergeConfigWithRemote, git);
}
