import path from 'path';

import { safeYarnOrNpm, updateElectronDependency } from '@electron-forge/core-utils';
import baseTemplate from '@electron-forge/template-base';
import chalk from 'chalk';
import debug from 'debug';
import fs from 'fs-extra';
import { Listr } from 'listr2';
import { merge } from 'lodash';

import installDepList, { DepType, DepVersionRestriction } from '../util/install-dependencies';
import { readRawPackageJson } from '../util/read-package-json';
import upgradeForgeConfig, { updateUpgradedForgeDevDeps } from '../util/upgrade-forge-config';

import { initGit } from './init-scripts/init-git';
import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm';

const d = debug('electron-forge:import');

export interface ImportOptions {
  /**
   * The path to the app to be imported
   */
  dir?: string;
  /**
   * Whether to use sensible defaults or prompt the user visually
   */
  interactive?: boolean;
  /**
   * An async function that returns true or false in order to confirm the start
   * of importing
   */
  confirmImport?: () => Promise<boolean>;
  /**
   * An async function that returns whether the import should continue if it
   * looks like a forge project already
   */
  shouldContinueOnExisting?: () => Promise<boolean>;
  /**
   * An async function that returns whether the given dependency should be removed
   */
  shouldRemoveDependency?: (dependency: string, explanation: string) => Promise<boolean>;
  /**
   * An async function that returns whether the given script should be overridden with a forge one
   */
  shouldUpdateScript?: (scriptName: string, newValue: string) => Promise<boolean>;
  /**
   * The path to the directory containing generated distributables
   */
  outDir?: string;
}

export default async ({
  dir = process.cwd(),
  interactive = false,
  confirmImport,
  shouldContinueOnExisting,
  shouldRemoveDependency,
  shouldUpdateScript,
  outDir,
}: ImportOptions): Promise<void> => {
  const listrOptions = {
    concurrent: false,
    rendererOptions: {
      collapse: false,
      collapseErrors: false,
    },
    rendererSilent: !interactive,
    rendererFallback: Boolean(process.env.DEBUG && process.env.DEBUG.includes('electron-forge')),
  };

  const runner = new Listr(
    [
      {
        title: 'Locating importable project',
        task: async () => {
          d(`Attempting to import project in: ${dir}`);
          if (!(await fs.pathExists(dir)) || !(await fs.pathExists(path.resolve(dir, 'package.json')))) {
            throw new Error(`We couldn't find a project with a package.json file in: ${dir}`);
          }

          if (typeof confirmImport === 'function') {
            if (!(await confirmImport())) {
              // TODO: figure out if we can just return early here
              // eslint-disable-next-line no-process-exit
              process.exit(0);
            }
          }

          await initGit(dir);
        },
      },
      {
        title: 'Processing configuration and dependencies',
        options: {
          persistentOutput: true,
          bottomBar: Infinity,
        },
        task: async (ctx, task) => {
          const calculatedOutDir = outDir || 'out';

          const importDeps = ([] as string[]).concat(deps);
          let importDevDeps = ([] as string[]).concat(devDeps);
          let importExactDevDeps = ([] as string[]).concat(exactDevDeps);

          let packageJSON = await readRawPackageJson(dir);
          if (!packageJSON.version) {
            task.output = chalk.yellow(`Please set the ${chalk.green('"version"')} in your application's package.json`);
          }
          if (packageJSON.config && packageJSON.config.forge) {
            if (packageJSON.config.forge.makers) {
              task.output = chalk.green('Existing Electron Forge configuration detected');
              if (typeof shouldContinueOnExisting === 'function') {
                if (!(await shouldContinueOnExisting())) {
                  // TODO: figure out if we can just return early here
                  // eslint-disable-next-line no-process-exit
                  process.exit(0);
                }
              }
            } else if (!(typeof packageJSON.config.forge === 'object')) {
              task.output = chalk.yellow(
                "We can't tell if the Electron Forge config is compatible because it's in an external JavaScript file, not trying to convert it and continuing anyway"
              );
            } else {
              d('Upgrading an Electron Forge < 6 project');
              packageJSON.config.forge = upgradeForgeConfig(packageJSON.config.forge);
              importDevDeps = updateUpgradedForgeDevDeps(packageJSON, importDevDeps);
            }
          }

          packageJSON.dependencies = packageJSON.dependencies || {};
          packageJSON.devDependencies = packageJSON.devDependencies || {};

          [importDevDeps, importExactDevDeps] = updateElectronDependency(packageJSON, importDevDeps, importExactDevDeps);

          const keys = Object.keys(packageJSON.dependencies).concat(Object.keys(packageJSON.devDependencies));
          const buildToolPackages: Record<string, string | undefined> = {
            '@electron/get': 'already uses this module as a transitive dependency',
            '@electron/osx-sign': 'already uses this module as a transitive dependency',
            'electron-builder': 'provides mostly equivalent functionality',
            'electron-download': 'already uses this module as a transitive dependency',
            'electron-forge': 'replaced with @electron-forge/cli',
            'electron-installer-debian': 'already uses this module as a transitive dependency',
            'electron-installer-dmg': 'already uses this module as a transitive dependency',
            'electron-installer-flatpak': 'already uses this module as a transitive dependency',
            'electron-installer-redhat': 'already uses this module as a transitive dependency',
            'electron-packager': 'already uses this module as a transitive dependency',
            'electron-winstaller': 'already uses this module as a transitive dependency',
          };

          for (const key of keys) {
            if (buildToolPackages[key]) {
              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              const explanation = buildToolPackages[key]!;
              let remove = true;
              if (typeof shouldRemoveDependency === 'function') {
                remove = await shouldRemoveDependency(key, explanation);
              }

              if (remove) {
                delete packageJSON.dependencies[key];
                delete packageJSON.devDependencies[key];
              }
            }
          }

          packageJSON.scripts = packageJSON.scripts || {};
          d('reading current scripts object:', packageJSON.scripts);

          const updatePackageScript = async (scriptName: string, newValue: string) => {
            if (packageJSON.scripts[scriptName] !== newValue) {
              let update = true;
              if (typeof shouldUpdateScript === 'function') {
                update = await shouldUpdateScript(scriptName, newValue);
              }
              if (update) {
                packageJSON.scripts[scriptName] = newValue;
              }
            }
          };

          await updatePackageScript('start', 'electron-forge start');
          await updatePackageScript('package', 'electron-forge package');
          await updatePackageScript('make', 'electron-forge make');

          d('forgified scripts object:', packageJSON.scripts);

          const writeChanges = async () => {
            await fs.writeJson(path.resolve(dir, 'package.json'), packageJSON, { spaces: 2 });
          };

          return task.newListr(
            [
              {
                title: 'Installing dependencies',
                task: async (_, task) => {
                  const packageManager = safeYarnOrNpm();
                  await writeChanges();

                  d('deleting old dependencies forcefully');
                  await fs.remove(path.resolve(dir, 'node_modules/.bin/electron'));
                  await fs.remove(path.resolve(dir, 'node_modules/.bin/electron.cmd'));

                  d('installing dependencies');
                  task.output = `${packageManager} install ${importDeps.join(' ')}`;
                  await installDepList(dir, importDeps);

                  d('installing devDependencies');
                  task.output = `${packageManager} install --dev ${importDevDeps.join(' ')}`;
                  await installDepList(dir, importDevDeps, DepType.DEV);

                  d('installing exactDevDependencies');
                  task.output = `${packageManager} install --dev --exact ${importExactDevDeps.join(' ')}`;
                  await installDepList(dir, importExactDevDeps, DepType.DEV, DepVersionRestriction.EXACT);
                },
              },
              {
                title: 'Copying base template Forge configuration',
                task: async () => {
                  const pathToTemplateConfig = path.resolve(baseTemplate.templateDir, 'forge.config.js');

                  // if there's an existing config.forge object in package.json
                  if (packageJSON?.config?.forge && typeof packageJSON.config.forge === 'object') {
                    d('detected existing Forge config in package.json, merging with base template Forge config');
                    // eslint-disable-next-line @typescript-eslint/no-var-requires
                    const templateConfig = require(path.resolve(baseTemplate.templateDir, 'forge.config.js'));
                    packageJSON = await readRawPackageJson(dir);
                    merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object
                    await writeChanges();
                    // otherwise, write to forge.config.js
                  } else {
                    d('writing new forge.config.js');
                    await fs.copyFile(pathToTemplateConfig, path.resolve(dir, 'forge.config.js'));
                  }
                },
              },
              {
                title: 'Fixing .gitignore',
                task: async () => {
                  if (await fs.pathExists(path.resolve(dir, '.gitignore'))) {
                    const gitignore = await fs.readFile(path.resolve(dir, '.gitignore'));
                    if (!gitignore.includes(calculatedOutDir)) {
                      await fs.writeFile(path.resolve(dir, '.gitignore'), `${gitignore}\n${calculatedOutDir}/`);
                    }
                  }
                },
              },
            ],
            listrOptions
          );
        },
      },
      {
        title: 'Finalizing import',
        options: {
          persistentOutput: true,
          bottomBar: Infinity,
        },
        task: (_, task) => {
          task.output = `We have attempted to convert your app to be in a format that Electron Forge understands.
          
          Thanks for using ${chalk.green('Electron Forge')}!`;
        },
      },
    ],
    listrOptions
  );

  await runner.run();
};
