/* eslint-disable import/no-dynamic-require, global-require */
import commandLineArgs from 'command-line-args';
import commandLineUsage from 'command-line-usage';
import path from 'path';
import camelcase from 'camelcase';
import fs from 'fs';
import deepmerge from 'deepmerge';
import { setDebug, logDebug } from './utils/utils';

export const commandLineOptions = [
  {
    name: 'config',
    alias: 'c',
    type: String,
    description:
      'The file to read configuration from (js or json). Config entries are camelCases flags.',
  },
  {
    name: 'port',
    alias: 'p',
    type: Number,
    description: 'The port to use. Default: 8080',
  },
  {
    name: 'hostname',
    alias: 'h',
    type: String,
    description: 'The hostname to use. Default: localhost',
  },
  {
    name: 'open',
    alias: 'o',
    type: String,
    description: 'Opens the browser on app-index, root dir or a custom path.',
  },
  {
    name: 'app-index',
    alias: 'a',
    type: String,
    description:
      "The app's index.html file. When set, serves the index.html for non-file requests. Use this to enable SPA routing.",
  },
  {
    name: 'root-dir',
    alias: 'r',
    type: String,
    description:
      'The root directory to serve files from. Defaults to the current working directory.',
  },
  {
    name: 'base-path',
    type: String,
    description:
      'Base path the app is served on. This path is only visible in the browser, it is stripped from the request url before resolving files. ' +
      'Starts with a / and ends with no/. For example: /my-app, /foo, /foo/bar',
  },
  {
    name: 'module-dirs',
    alias: 'm',
    type: String,
    multiple: true,
    description: 'Directories to resolve modules from. Used by node-resolve',
  },
  {
    name: 'watch',
    alias: 'w',
    type: Boolean,
    description: 'Reload the browser when files are edited.',
  },
  {
    name: 'watch-excludes',
    type: String,
    description: 'Globs to exclude from file watching. Default: node_modules/**',
  },
  {
    name: 'http2',
    alias: 't',
    type: Boolean,
    description:
      'Serve files over HTTP2. Sets up HTTPS with self-signed certificates or optional custom certificates',
  },
  {
    name: 'ssl-key',
    type: String,
    description: 'Path to local .key file for https',
  },
  {
    name: 'ssl-cert',
    type: String,
    description: 'Path to local .cert file for https',
  },
  {
    name: 'node-resolve',
    alias: 'n',
    type: Boolean,
    description: 'Resolve bare import imports using node resolve.',
  },
  {
    name: 'dedupe',
    alias: 'd',
    type: Boolean,
    description:
      'Dedupe modules by resolving them always from the root, ensuring only one version of a package is ever resolved.',
  },
  {
    name: 'preserve-symlinks',
    type: Boolean,
    description:
      'Preserve symlinks when resolving modules. Default false, which is the default node behavior.',
  },
  {
    name: 'babel',
    alias: 'b',
    type: Boolean,
    description: 'Transform served code through babel. Requires .babelrc',
  },
  {
    name: 'file-extensions',
    type: String,
    multiple: true,
    description: 'Extra file extentions to use when transforming code.',
  },
  {
    name: 'babel-exclude',
    type: String,
    multiple: true,
    description: 'Patterns of files to exclude from babel compilation.',
  },
  {
    name: 'babel-modern-exclude',
    type: String,
    multiple: true,
    description: 'Patterns of files to exclude from babel compilation on modern browsers.',
  },
  {
    name: 'compatibility',
    type: String,
    description:
      'Compatibility mode for older browsers. Can be: "auto", "always", "min", "max" or "none". Default "auto"',
  },
  {
    name: 'debug',
    type: Boolean,
    description: 'Whether to log debug messages',
  },
  { name: 'help', type: Boolean, description: 'See all options' },
];

export interface CommandLineArgs {
  config?: string;
  port?: number;
  hostname?: string;
  open?: string | boolean;
  appIndex?: string;
  rootDir?: string;
  basePath?: string;
  moduleDirs?: string[];
  watch?: boolean;
  watchExcludes?: string;
  http2?: boolean;
  sslKey?: string;
  sslCert?: string;
  nodeResolve?: boolean;
  dedupe?: boolean;
  dedupeModules?: boolean;
  preserveSymlinks?: boolean;
  babel?: boolean;
  fileExtensions?: string[];
  babelExclude?: string[];
  babelModernExclude?: string[];
  compatibility?: string;
  debug?: boolean;
  logStartup: boolean;
  logCompileErrors: boolean;
}

/**
 * Reads command line args from arguments array. Defaults to `process.argv`.
 */
export function readCommandLineArgs(
  argv = process.argv,
  config: { defaultConfigDir?: string; defaultConfigName?: string } = {},
): CommandLineArgs {
  const { defaultConfigDir = '.', defaultConfigName = 'es-dev-server.config.js' } = config;
  const dashesArgs = commandLineArgs(commandLineOptions, { argv, partial: true });
  const openInCommandLineArgs = 'open' in dashesArgs;

  // convert kebab-case to camelCase
  const args = Object.keys(dashesArgs).reduce<commandLineArgs.CommandLineOptions>((acc, key) => {
    acc[camelcase(key)] = dashesArgs[key];
    return acc;
  }, {});

  if (args.debug) {
    setDebug(true);
  }

  if ('help' in args) {
    /* eslint-disable-next-line no-console */
    console.log(
      commandLineUsage([
        {
          header: 'es-dev-server',
          content: `
          A dev server for modern web development workflows.

          Usage: \`es-dev-server [options...]\`
        `.trim(),
        },
        {
          header: 'Global Options',
          optionList: commandLineOptions,
        },
      ]),
    );
    process.exit();
  }

  let options;

  if (args.config) {
    // read config from user provided path
    const configPath = path.join(process.cwd(), args.config);
    if (!fs.existsSync(configPath) || !fs.statSync(configPath).isFile()) {
      throw new Error(`Did not find any config file at ${configPath}`);
    }

    const fileConfig = require(configPath);
    options = deepmerge(fileConfig, args);
    logDebug(`Found a config file at ${configPath}`);
  } else {
    // read default config if present
    const defaultConfigPath = path.join(process.cwd(), defaultConfigDir, defaultConfigName);

    if (fs.existsSync(defaultConfigPath) && fs.statSync(defaultConfigPath).isFile()) {
      const fileConfig = require(defaultConfigPath);
      options = deepmerge(fileConfig, args);
      logDebug(`Found a default config file at ${defaultConfigPath}`);
    } else {
      // no file config, just command line args
      options = args;
      logDebug(`Did not find a default config file at ${defaultConfigPath}`);
    }
  }

  let open: string | boolean | undefined = options.open;

  if (options.dedupe) {
    options.dedupeModules = options.dedupe;
    delete options.dedupe;
  }

  // open can be a boolean nor a string. if it's a boolean from command line args,
  // it's passed as null. so if it's not a string or boolean type, we check for the
  // existence of open in the args object, and treat that as true
  if (typeof open !== 'string' && typeof open !== 'boolean' && openInCommandLineArgs) {
    open = true;
  }

  return {
    ...options,
    open,
    logStartup: true,
    // when used from the command line we log compile errors to the browser,
    // not to the terminal for a better UX
    logCompileErrors: false,
  };
}
