#!/usr/bin/env node

// we can import from '@nrwl/workspace' because it will require typescript
import { output } from '@nrwl/workspace/src/utils/output';
import { getPackageManagerExecuteCommand } from '@nrwl/workspace/src/utils/detect-package-manager';
import { execSync } from 'child_process';
import { writeFileSync } from 'fs';
import * as inquirer from 'inquirer';
import * as path from 'path';
import { dirSync } from 'tmp';
import * as yargsParser from 'yargs-parser';
import { showNxWarning } from './shared';

enum Preset {
  Empty = 'empty',
  OSS = 'oss',
  WebComponents = 'web-components',
  Angular = 'angular',
  AngularWithNest = 'angular-nest',
  React = 'react',
  ReactWithExpress = 'react-express',
  NextJs = 'next',
  Nest = 'nest',
}

const presetOptions = [
  {
    value: Preset.Empty,
    name:
      'empty             [an empty workspace with a layout that works best for building apps]',
  },
  {
    value: Preset.React,
    name: 'react             [a workspace with a single React application]',
  },
  {
    value: Preset.Angular,
    name: 'angular           [a workspace with a single Angular application]',
  },
  {
    value: Preset.NextJs,
    name: 'next.js           [a workspace with a single Next.js application]',
  },
  {
    value: Preset.Nest,
    name: 'nest              [a workspace with a single Nest application]',
  },
  {
    value: 'web-components',
    name:
      'web components    [a workspace with a single app built using web components]',
  },
  {
    value: Preset.ReactWithExpress,
    name:
      'react-express     [a workspace with a full stack application (React + Express)]',
  },
  {
    value: Preset.AngularWithNest,
    name:
      'angular-nest      [a workspace with a full stack application (Angular + Nest)]',
  },
  {
    value: 'oss',
    name:
      'oss               [an empty workspace with a layout that works best for open-source projects]',
  },
];

const tsVersion = 'TYPESCRIPT_VERSION';
const cliVersion = 'NX_VERSION';
const nxVersion = 'NX_VERSION';
const angularCliVersion = 'ANGULAR_CLI_VERSION';
const prettierVersion = 'PRETTIER_VERSION';

const parsedArgs = yargsParser(process.argv, {
  string: [
    'cli',
    'preset',
    'appName',
    'style',
    'linter',
    'defaultBase',
    'packageManager',
  ],
  alias: {
    appName: 'app-name',
    nxCloud: 'nx-cloud',
    defaultBase: 'default-base',
  },
  boolean: ['help', 'interactive', 'nxCloud'],
});

if (parsedArgs.help) {
  showHelp();
  process.exit(0);
}
const packageManager = parsedArgs.packageManager || 'npm';
determineWorkspaceName(parsedArgs).then((name) => {
  determinePreset(parsedArgs).then((preset) => {
    return determineAppName(preset, parsedArgs).then((appName) => {
      return determineStyle(preset, parsedArgs).then((style) => {
        return determineCli(preset, parsedArgs).then((cli) => {
          return determineLinter(preset, parsedArgs).then((linter) => {
            return askAboutNxCloud(parsedArgs).then((cloud) => {
              const tmpDir = createSandbox(packageManager);
              createApp(
                tmpDir,
                cli,
                parsedArgs,
                name,
                preset,
                appName,
                style,
                linter,
                cloud,
                parsedArgs.interactive,
                parsedArgs.defaultBase
              );
              showNxWarning(name);
              pointToTutorialAndCourse(preset);
            });
          });
        });
      });
    });
  });
});

function showHelp() {
  const options = Object.values(Preset)
    .map((preset) => '"' + preset + '"')
    .join(', ');

  console.log(`
  Usage: create-nx-workspace <name> [options] [new workspace options]

  Create a new Nx workspace

  Options:

    name                      Workspace name (e.g., org name)

    preset                    What to create in a new workspace (options: ${options})

    appName                   The name of the application created by some presets  

    cli                       CLI to power the Nx workspace (options: "nx", "angular")
    
    style                     Default style option to be used when a non-empty preset is selected 
                              options: ("css", "scss", "styl", "less") for React/Next.js also ("styled-components", "@emotion/styled")    
                              
    linter                    Default linter. Options: "eslint", "tslint".

    interactive               Enable interactive mode when using presets (boolean)
    
    nx-cloud                  Use Nx Cloud (boolean)

    [new workspace options]   any 'new workspace' options
`);
}

function determineWorkspaceName(parsedArgs: any): Promise<string> {
  const workspaceName: string = parsedArgs._[2];

  if (workspaceName) {
    return Promise.resolve(workspaceName);
  }

  return inquirer
    .prompt([
      {
        name: 'WorkspaceName',
        message: `Workspace name (e.g., org name)    `,
        type: 'string',
      },
    ])
    .then((a) => {
      if (!a.WorkspaceName) {
        output.error({
          title: 'Invalid workspace name',
          bodyLines: [`Workspace name cannot be empty`],
        });
        process.exit(1);
      }
      return a.WorkspaceName;
    });
}

function determinePreset(parsedArgs: any): Promise<Preset> {
  if (parsedArgs.preset) {
    if (Object.values(Preset).indexOf(parsedArgs.preset) === -1) {
      output.error({
        title: 'Invalid preset',
        bodyLines: [
          `It must be one of the following:`,
          '',
          ...Object.values(Preset),
        ],
      });
      process.exit(1);
    } else {
      return Promise.resolve(parsedArgs.preset);
    }
  }

  return inquirer
    .prompt([
      {
        name: 'Preset',
        message: `What to create in the new workspace`,
        default: 'empty',
        type: 'list',
        choices: presetOptions,
      },
    ])
    .then((a: { Preset: Preset }) => a.Preset);
}

function determineAppName(preset: Preset, parsedArgs: any): Promise<string> {
  if (preset === Preset.Empty || preset === Preset.OSS) {
    return Promise.resolve('');
  }

  if (parsedArgs.appName) {
    return Promise.resolve(parsedArgs.appName);
  }

  return inquirer
    .prompt([
      {
        name: 'AppName',
        message: `Application name                   `,
        type: 'string',
      },
    ])
    .then((a) => {
      if (!a.AppName) {
        output.error({
          title: 'Invalid name',
          bodyLines: [`Name cannot be empty`],
        });
        process.exit(1);
      }
      return a.AppName;
    });
}

function determineCli(
  preset: Preset,
  parsedArgs: any
): Promise<'nx' | 'angular'> {
  if (parsedArgs.cli) {
    if (['nx', 'angular'].indexOf(parsedArgs.cli) === -1) {
      output.error({
        title: 'Invalid cli',
        bodyLines: [`It must be one of the following:`, '', 'nx', 'angular'],
      });
      process.exit(1);
    }
    return Promise.resolve(parsedArgs.cli);
  }

  switch (preset) {
    case Preset.Angular:
    case Preset.AngularWithNest: {
      return Promise.resolve('angular');
    }
    default: {
      return Promise.resolve('nx');
    }
  }
}

function determineStyle(preset: Preset, parsedArgs: any) {
  if (
    preset === Preset.Empty ||
    preset === Preset.OSS ||
    preset === Preset.Nest
  ) {
    return Promise.resolve(null);
  }

  const choices = [
    {
      value: 'css',
      name: 'CSS',
    },
    {
      value: 'scss',
      name: 'SASS(.scss)  [ http://sass-lang.com   ]',
    },
    {
      value: 'styl',
      name: 'Stylus(.styl)[ http://stylus-lang.com ]',
    },
    {
      value: 'less',
      name: 'LESS         [ http://lesscss.org     ]',
    },
  ];

  if ([Preset.ReactWithExpress, Preset.React, Preset.NextJs].includes(preset)) {
    choices.push(
      {
        value: 'styled-components',
        name: 'styled-components [ https://styled-components.com            ]',
      },
      {
        value: '@emotion/styled',
        name: 'emotion           [ https://emotion.sh                       ]',
      },
      {
        value: 'styled-jsx',
        name: 'styled-jsx        [ https://www.npmjs.com/package/styled-jsx ]',
      }
    );
  }

  if (!parsedArgs.style) {
    return inquirer
      .prompt([
        {
          name: 'style',
          message: `Default stylesheet format          `,
          default: 'css',
          type: 'list',
          choices,
        },
      ])
      .then((a) => a.style);
  }

  const foundStyle = choices.find(
    (choice) => choice.value === parsedArgs.style
  );

  if (foundStyle === undefined) {
    output.error({
      title: 'Invalid style',
      bodyLines: [
        `It must be one of the following:`,
        '',
        ...choices.map((choice) => choice.value),
      ],
    });

    process.exit(1);
  }

  return Promise.resolve(parsedArgs.style);
}

function determineLinter(preset: Preset, parsedArgs: any) {
  if (!parsedArgs.linter) {
    if (preset === Preset.Angular || preset === Preset.AngularWithNest) {
      return inquirer
        .prompt([
          {
            name: 'linter',
            message: `Default linter                     `,
            default: 'tslint',
            type: 'list',
            choices: [
              {
                value: 'tslint',
                name: 'TSLint [ Used by Angular CLI ]',
              },
              {
                value: 'eslint',
                name: 'ESLint [ Modern linting tool ]',
              },
            ],
          },
        ])
        .then((a) => a.linter);
    } else {
      return Promise.resolve('eslint');
    }
  } else {
    if (parsedArgs.linter !== 'eslint' && parsedArgs.linter !== 'tslint') {
      output.error({
        title: 'Invalid linter',
        bodyLines: [`It must be one of the following:`, '', 'eslint', 'tslint'],
      });
      process.exit(1);
    } else {
      return Promise.resolve(parsedArgs.linter);
    }
  }
}

function createSandbox(packageManager: string) {
  console.log(`Creating a sandbox with Nx...`);
  const tmpDir = dirSync().name;
  writeFileSync(
    path.join(tmpDir, 'package.json'),
    JSON.stringify({
      dependencies: {
        '@nrwl/workspace': nxVersion,
        '@nrwl/tao': cliVersion,
        typescript: tsVersion,
        prettier: prettierVersion,
      },
      license: 'MIT',
    })
  );

  execSync(`${packageManager} install --silent`, {
    cwd: tmpDir,
    stdio: [0, 1, 2],
  });

  return tmpDir;
}

function createApp(
  tmpDir: string,
  cli: 'nx' | 'angular',
  parsedArgs: any,
  name: string,
  preset: Preset,
  appName: string,
  style: string | null,
  linter: string,
  nxCloud: boolean,
  interactive: boolean,
  defaultBase: string
) {
  const filterArgs = [
    '_',
    'app-name',
    'appName',
    'cli',
    'default-base',
    'defaultBase',
    'interactive',
    'nx-cloud',
    'nxCloud',
    'preset',
    'style',
    'linter',
  ];

  // These are the arguments that are passed to the schematic
  const args = Object.keys(parsedArgs)
    .filter((key) => !filterArgs.includes(key))
    .map((key) => `--${key}=${parsedArgs[key]}`)
    .join(' ');

  const appNameArg = appName ? ` --appName="${appName}"` : ``;
  const styleArg = style ? ` --style="${style}"` : ``;
  const linterArg = ` --linter="${linter}"`;
  const nxCloudArg = nxCloud ? ` --nxCloud` : ``;
  const interactiveArg = interactive
    ? ` --interactive=true`
    : ` --interactive=false`;
  const defaultBaseArg = defaultBase ? ` --defaultBase="${defaultBase}"` : ``;

  const packageExec = getPackageManagerExecuteCommand(packageManager);
  const command = `new ${name} ${args} --preset="${preset}"${appNameArg}${styleArg}${linterArg}${nxCloudArg}${interactiveArg}${defaultBaseArg} --collection=@nrwl/workspace`;
  console.log(command);

  execSync(
    `${packageExec} tao ${command}/collection.json --cli=${cli} --nxWorkspaceRoot="${process.cwd()}"`,
    {
      stdio: [0, 1, 2],
      cwd: tmpDir,
    }
  );

  if (nxCloud) {
    output.addVerticalSeparator();
    execSync(`${packageExec} nx g @nrwl/nx-cloud:init --no-analytics`, {
      stdio: [0, 1, 2],
      cwd: path.join(process.cwd(), name),
    });
  }
}

async function askAboutNxCloud(parsedArgs: any) {
  if (parsedArgs.nxCloud === undefined) {
    return inquirer
      .prompt([
        {
          name: 'NxCloud',
          message: `Use Nx Cloud? (It's free and doesn't require registration.)`,
          type: 'list',
          choices: [
            {
              value: 'yes',
              name:
                'Yes [Faster builds, run details, Github integration. Learn more at https://nx.app]',
            },

            {
              value: 'no',
              name: 'No',
            },
          ],
          default: 'no',
        },
      ])
      .then((a: { NxCloud: 'yes' | 'no' }) => a.NxCloud === 'yes');
  } else {
    return parsedArgs.nxCloud;
  }
}

function pointToTutorialAndCourse(preset: Preset) {
  const title = `First time using Nx? Check out this interactive Nx tutorial.`;
  switch (preset) {
    case Preset.React:
    case Preset.ReactWithExpress:
    case Preset.NextJs:
      output.addVerticalSeparator();
      output.note({
        title: title,
        bodyLines: [
          `https://nx.dev/react/tutorial/01-create-application`,
          ...pointToCourse(),
        ],
      });
      break;
    case Preset.Angular:
    case Preset.AngularWithNest:
      output.addVerticalSeparator();
      output.note({
        title: title,
        bodyLines: [
          `https://nx.dev/angular/tutorial/01-create-application`,
          ...pointToCourse(),
        ],
      });
      break;
    // TODO(Cammisuli): include this after the nx.dev site is updated with the node flavour
    // case Preset.Nest:
    //   output.addVerticalSeparator();
    //   output.note({
    //     title,
    //     bodyLines: [
    //       `https://nx.dev/node/tutorial/01-create-application`,
    //       ...pointToCourse(),
    //     ],
    //   });
    //   break;
  }
}

function pointToCourse(): string[] {
  return [
    ``,
    `Prefer watching videos? Check out this free Nx course on YouTube.`,
    `https://www.youtube.com/watch?v=2mYLe9Kp9VM&list=PLakNactNC1dH38AfqmwabvOszDmKriGco`,
  ];
}
