const path = require('path');
const debug = require('debug');
const inquirer = require('inquirer');
const EventEmitter = require('events');
const Generator = require('./Generator');
const cloneDeep = require('lodash.clonedeep');
const sortObject = require('./util/sortObject');
const getVersions = require('./util/getVersions');
const PackageManager = require('./util/ProjectPackageManager');
const { clearConsole } = require('./util/clearConsole');
const PromptModuleAPI = require('./PromptModuleAPI');
const writeFileTree = require('./util/writeFileTree');
const { formatFeatures } = require('./util/features');
const loadLocalPreset = require('./util/loadLocalPreset');
const loadRemotePreset = require('./util/loadRemotePreset');
const generateReadme = require('./util/generateReadme');
const { resolvePkg, isOfficialPlugin } = require('@vue/cli-shared-utils');

const {
  defaults,
  saveOptions,
  loadOptions,
  savePreset,
  validatePreset,
  rcPath
} = require('./options');

const {
  chalk,
  execa,

  log,
  warn,
  error,

  hasGit,
  hasProjectGit,
  hasYarn,
  hasPnpm3OrLater,
  hasPnpmVersionOrLater,

  exit,
  loadModule
} = require('@vue/cli-shared-utils');

const isManualMode = answers => answers.preset === '__manual__';

module.exports = class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    super();

    this.name = name;
    this.context = process.env.VUE_CLI_CONTEXT = context;
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();

    this.presetPrompt = presetPrompt;
    this.featurePrompt = featurePrompt;
    this.outroPrompts = this.resolveOutroPrompts();
    this.injectedPrompts = [];
    this.promptCompleteCbs = [];
    this.afterInvokeCbs = [];
    this.afterAnyInvokeCbs = [];

    this.run = this.run.bind(this);

    const promptAPI = new PromptModuleAPI(this);
    promptModules.forEach(m => m(promptAPI));
  }

  async create(cliOptions = {}, preset = null) {
    const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG;
    const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this;

    if (!preset) {
      if (cliOptions.preset) {
        // vue create foo --preset bar
        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone);
      } else if (cliOptions.default) {
        // vue create foo --default
        preset = defaults.presets['Default (Vue 3)'];
      } else if (cliOptions.inlinePreset) {
        // vue create foo --inlinePreset {...}
        try {
          preset = JSON.parse(cliOptions.inlinePreset);
        } catch (e) {
          error(
            `CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`
          );
          exit(1);
        }
      } else {
        preset = await this.promptAndResolvePreset();
      }
    }

    // clone before mutating
    preset = cloneDeep(preset);
    // inject core service
    preset.plugins['@vue/cli-service'] = Object.assign(
      {
        projectName: name
      },
      preset
    );

    if (cliOptions.bare) {
      preset.plugins['@vue/cli-service'].bare = true;
    }

    // legacy support for router
    if (preset.router) {
      preset.plugins['@vue/cli-plugin-router'] = {};

      if (preset.routerHistoryMode) {
        preset.plugins['@vue/cli-plugin-router'].historyMode = true;
      }
    }

    // legacy support for vuex
    if (preset.vuex) {
      preset.plugins['@vue/cli-plugin-vuex'] = {};
    }

    const packageManager =
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : null) ||
      (hasPnpm3OrLater() ? 'pnpm' : 'npm');

    await clearConsole();
    const pm = new PackageManager({
      context,
      forcePackageManager: packageManager
    });

    log(`✨  Creating project in ${chalk.yellow(context)}.`);
    this.emit('creation', { event: 'creating' });

    // get latest CLI plugin version
    const { latestMinor } = await getVersions();

    // generate package.json with plugin dependencies
    const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {},
      ...resolvePkg(context)
    };
    const deps = Object.keys(preset.plugins);
    deps.forEach(dep => {
      if (preset.plugins[dep]._isPreset) {
        return;
      }

      let { version } = preset.plugins[dep];

      if (!version) {
        if (
          isOfficialPlugin(dep) ||
          dep === '@vue/cli-service' ||
          dep === '@vue/babel-preset-env'
        ) {
          version = isTestOrDebug ? `latest` : `~${latestMinor}`;
        } else {
          version = 'latest';
        }
      }

      pkg.devDependencies[dep] = version;
    });

    // write package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    });

    // generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
    if (packageManager === 'pnpm') {
      const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
        ? 'shamefully-hoist=true\n'
        : 'shamefully-flatten=true\n';

      await writeFileTree(context, {
        '.npmrc': pnpmConfig
      });
    }

    // intilaize git repository before installing deps
    // so that vue-cli-service can setup git hooks.
    const shouldInitGit = this.shouldInitGit(cliOptions);
    if (shouldInitGit) {
      log(`🗃  Initializing git repository...`);
      this.emit('creation', { event: 'git-init' });
      await run('git init');
    }

    // install plugins
    log(`⚙\u{fe0f}  Installing CLI plugins. This might take a while...`);
    log();
    this.emit('creation', { event: 'plugins-install' });

    if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
      // in development, avoid installation process
      await require('./util/setupDevProject')(context);
    } else {
      await pm.install();
    }

    // run generator
    log(`🚀  Invoking generators...`);
    this.emit('creation', { event: 'invoking-generators' });
    const plugins = await this.resolvePlugins(preset.plugins, pkg);
    const generator = new Generator(context, {
      pkg,
      plugins,
      afterInvokeCbs,
      afterAnyInvokeCbs
    });
    await generator.generate({
      extractConfigFiles: preset.useConfigFiles
    });

    // install additional deps (injected by generators)
    log(`📦  Installing additional dependencies...`);
    this.emit('creation', { event: 'deps-install' });
    log();
    if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
      await pm.install();
    }

    // run complete cbs if any (injected by generators)
    log(`⚓  Running completion hooks...`);
    this.emit('creation', { event: 'completion-hooks' });
    for (const cb of afterInvokeCbs) {
      await cb();
    }
    for (const cb of afterAnyInvokeCbs) {
      await cb();
    }

    if (!generator.files['README.md']) {
      // generate README.md
      log();
      log('📄  Generating README.md...');
      await writeFileTree(context, {
        'README.md': generateReadme(generator.pkg, packageManager)
      });
    }

    // commit initial state
    let gitCommitFailed = false;
    if (shouldInitGit) {
      await run('git add -A');
      if (isTestOrDebug) {
        await run('git', ['config', 'user.name', 'test']);
        await run('git', ['config', 'user.email', 'test@test.com']);
        await run('git', ['config', 'commit.gpgSign', 'false']);
      }
      const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init';
      try {
        await run('git', ['commit', '-m', msg, '--no-verify']);
      } catch (e) {
        gitCommitFailed = true;
      }
    }

    // log instructions
    log();
    log(`🎉  Successfully created project ${chalk.yellow(name)}.`);
    if (!cliOptions.skipGetStarted) {
      log(
        `👉  Get started with the following commands:\n\n` +
          (this.context === process.cwd()
            ? ``
            : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
          chalk.cyan(
            ` ${chalk.gray('$')} ${
              packageManager === 'yarn'
                ? 'yarn serve'
                : packageManager === 'pnpm'
                ? 'pnpm run serve'
                : 'npm run serve'
            }`
          )
      );
    }
    log();
    this.emit('creation', { event: 'done' });

    if (gitCommitFailed) {
      warn(
        `Skipped git commit due to missing username and email in git config, or failed to sign commit.\n` +
          `You will need to perform the initial commit yourself.\n`
      );
    }

    generator.printExitLogs();
  }

  run(command, args) {
    if (!args) {
      [command, ...args] = command.split(/\s+/);
    }
    return execa(command, args, { cwd: this.context });
  }

  async promptAndResolvePreset(answers = null) {
    // prompt
    if (!answers) {
      await clearConsole(true);
      answers = await inquirer.prompt(this.resolveFinalPrompts());
    }
    debug('vue-cli:answers')(answers);

    if (answers.packageManager) {
      saveOptions({
        packageManager: answers.packageManager
      });
    }

    let preset;
    if (answers.preset && answers.preset !== '__manual__') {
      preset = await this.resolvePreset(answers.preset);
    } else {
      // manual
      preset = {
        useConfigFiles: answers.useConfigFiles === 'files',
        plugins: {}
      };
      answers.features = answers.features || [];
      // run cb registered by prompt modules to finalize the preset
      this.promptCompleteCbs.forEach(cb => cb(answers, preset));
    }

    // validate
    validatePreset(preset);

    // save preset
    if (
      answers.save &&
      answers.saveName &&
      savePreset(answers.saveName, preset)
    ) {
      log();
      log(
        `🎉  Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(
          rcPath
        )}`
      );
    }

    debug('vue-cli:preset')(preset);
    return preset;
  }

  async resolvePreset(name, clone) {
    let preset;
    const savedPresets = this.getPresets();

    if (name in savedPresets) {
      preset = savedPresets[name];
    } else if (name === 'default') {
      preset = savedPresets['Default (Vue 3)'];
    } else if (
      name.endsWith('.json') ||
      /^\./.test(name) ||
      path.isAbsolute(name)
    ) {
      preset = await loadLocalPreset(path.resolve(name));
    } else if (name.includes('/')) {
      log(`Fetching remote preset ${chalk.cyan(name)}...`);
      this.emit('creation', { event: 'fetch-remote-preset' });
      try {
        preset = await loadRemotePreset(name, clone);
      } catch (e) {
        error(`Failed fetching remote preset ${chalk.cyan(name)}:`);
        throw e;
      }
    }

    if (!preset) {
      error(`preset "${name}" not found.`);
      const presets = Object.keys(savedPresets);
      if (presets.length) {
        log();
        log(`available presets:\n${presets.join(`\n`)}`);
      } else {
        log(`you don't seem to have any saved preset.`);
        log(`run vue-cli in manual mode to create a preset.`);
      }
      exit(1);
    }
    return preset;
  }

  // { id: options } => [{ id, apply, options }]
  async resolvePlugins(rawPlugins, pkg) {
    // ensure cli-service is invoked first
    rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true);
    const plugins = [];
    for (const id of Object.keys(rawPlugins)) {
      const apply = loadModule(`${id}/generator`, this.context) || (() => {});
      let options = rawPlugins[id] || {};

      if (options.prompts) {
        let pluginPrompts = loadModule(`${id}/prompts`, this.context);

        if (pluginPrompts) {
          const prompt = inquirer.createPromptModule();

          if (typeof pluginPrompts === 'function') {
            pluginPrompts = pluginPrompts(pkg, prompt);
          }
          if (typeof pluginPrompts.getPrompts === 'function') {
            pluginPrompts = pluginPrompts.getPrompts(pkg, prompt);
          }

          log();
          log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`);
          options = await prompt(pluginPrompts);
        }
      }

      plugins.push({ id, apply, options });
    }
    return plugins;
  }

  getPresets() {
    const savedOptions = loadOptions();
    return Object.assign({}, savedOptions.presets, defaults.presets);
  }

  resolveIntroPrompts() {
    const presets = this.getPresets();
    const presetChoices = Object.entries(presets).map(([name, preset]) => {
      let displayName = name;
      // Vue version will be showed as features anyway,
      // so we shouldn't display it twice.
      if (name === 'Default (Vue 2)' || name === 'Default (Vue 3)') {
        displayName = 'Default';
      }

      return {
        name: `${displayName} (${formatFeatures(preset)})`,
        value: name
      };
    });
    const presetPrompt = {
      name: 'preset',
      type: 'list',
      message: `Please pick a preset:`,
      choices: [
        ...presetChoices,
        {
          name: 'Manually select features',
          value: '__manual__'
        }
      ]
    };
    const featurePrompt = {
      name: 'features',
      when: isManualMode,
      type: 'checkbox',
      message: 'Check the features needed for your project:',
      choices: [],
      pageSize: 10
    };
    return {
      presetPrompt,
      featurePrompt
    };
  }

  resolveOutroPrompts() {
    const outroPrompts = [
      {
        name: 'useConfigFiles',
        when: isManualMode,
        type: 'list',
        message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
        choices: [
          {
            name: 'In dedicated config files',
            value: 'files'
          },
          {
            name: 'In package.json',
            value: 'pkg'
          }
        ]
      },
      {
        name: 'save',
        when: isManualMode,
        type: 'confirm',
        message: 'Save this as a preset for future projects?',
        default: false
      },
      {
        name: 'saveName',
        when: answers => answers.save,
        type: 'input',
        message: 'Save preset as:'
      }
    ];

    // ask for packageManager once
    const savedOptions = loadOptions();
    if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {
      const packageManagerChoices = [];

      if (hasYarn()) {
        packageManagerChoices.push({
          name: 'Use Yarn',
          value: 'yarn',
          short: 'Yarn'
        });
      }

      if (hasPnpm3OrLater()) {
        packageManagerChoices.push({
          name: 'Use PNPM',
          value: 'pnpm',
          short: 'PNPM'
        });
      }

      packageManagerChoices.push({
        name: 'Use NPM',
        value: 'npm',
        short: 'NPM'
      });

      outroPrompts.push({
        name: 'packageManager',
        type: 'list',
        message:
          'Pick the package manager to use when installing dependencies:',
        choices: packageManagerChoices
      });
    }

    return outroPrompts;
  }

  resolveFinalPrompts() {
    // patch generator-injected prompts to only show in manual mode
    this.injectedPrompts.forEach(prompt => {
      const originalWhen = prompt.when || (() => true);
      prompt.when = answers => {
        return isManualMode(answers) && originalWhen(answers);
      };
    });

    const prompts = [
      this.presetPrompt,
      this.featurePrompt,
      ...this.injectedPrompts,
      ...this.outroPrompts
    ];
    debug('vue-cli:prompts')(prompts);
    return prompts;
  }

  shouldInitGit(cliOptions) {
    if (!hasGit()) {
      return false;
    }
    // --git
    if (cliOptions.forceGit) {
      return true;
    }
    // --no-git
    if (cliOptions.git === false || cliOptions.git === 'false') {
      return false;
    }
    // default: true unless already in a git repo
    return !hasProjectGit(this.context);
  }
};
