import * as cli from 'commander';

import { spawn } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { AddressInfo } from 'net';
import { resolve } from 'path';
import * as shell from 'shelljs';
import * as url from 'url';

import { getLogger, init } from './Logger';
import { IConfig, IProxy, ProxyServer } from './ProxyServer';

let displayError = true;

const optionNames = [
  'logLevel',
  'config',
  'debug',
  'tunneling',
];

export interface ICmdOptions {
  logLevel?: string;
  config?: string;
  debug?: boolean;
  tunneling?: boolean;
}

export function getFileConfig(filePath: string): IConfig {
  const absFile = resolve(process.cwd(), filePath);

  if (!existsSync(absFile)) {
    getLogger()
      .error('Cannot find auto-pod.yaml. You may also use another config file by "-c" param.');
    throw new Error(`Cannot find ${absFile}`);
  }

  const content = readFileSync(absFile).toString('utf8');

  let config = null;

  try {
    config = yaml.safeLoad(content);
  } catch (err) {
    getLogger().error(`invalid yaml content: ${err.message}`);
    const error = new Error(`invalid yaml content: ${err.message}`);
    // error.code = err.code;
    throw error;
  }

  const fileConfig = {
    excludes: config.excludes || [],
    forceTunneling: false,
    includes: config.includes || [],
    proxies: [],
  };

  // Parse each proxies as providers.
  if (!config.proxies || !(config.proxies instanceof Array)) {
    getLogger()
      .error('No proxies is set. Please provide us at least one element in `proxies` field.');
    throw new Error("No proxies is set.");
  }

  if (config.proxies.length === 0) {
    throw new Error("No proxies is provided.");
  }

  for (const proxy of config.proxies) {
    let opts: IProxy = {};
    if ('string' === typeof proxy) opts = url.parse(proxy);

    // prefer `hostname` over `host`, because of `url.parse()`
    opts.host = opts.hostname || opts.host;

    // SOCKS doesn't *technically* have a default port, but this is
    // the same default that `curl(1)` uses
    opts.nport = +opts.port || 1080;

    if (opts.host && opts.path) {
      // if both a `host` and `path` are specified then it's most likely the
      // result of a `url.parse()` call... we need to remove the `path` portion so
      // that `net.connect()` doesn't attempt to open that as a unix socket file.
      delete opts.path;
      delete opts.pathname;
    }

    const originalLookup = opts.lookup;
    // figure out if we want socks v4 or v5, based on the "protocol" used.
    // Defaults to 5.
    opts.lookup = false;
    switch (opts.protocol) {
      case 'socks4:':
        opts.lookup = true;
      // pass through
      case 'socks4a:':
        opts.version = 4;
        break;
      case 'socks5:':
        opts.lookup = true;
      // pass through
      case 'socks:': // no version specified, default to 5h
      case 'socks5h:':
        opts.version = 5;
        break;
      default:
        throw new TypeError(
          `A "socks" protocol must be specified! Got: ${proxy.protocol}`,
        );
    }

    if (opts.auth) {
      const auth = opts.auth.split(':');
      opts.authentication = { username: auth[0], password: auth[1] };
      // opts.userid = auth[0];
    }

    // Set to manually set lookup value.
    if (typeof originalLookup !== 'undefined') {
      opts.lookup = !!originalLookup;
    }

    fileConfig.proxies.push(opts);
  }

  if ('force-tunneling' in config) {
    fileConfig.forceTunneling = !!config['force-tunneling'];
  }

  return fileConfig;
}

export function getOptionsArgs(args): ICmdOptions {
  const options = {};

  optionNames.forEach((name) => {
    if (Object.hasOwnProperty.apply(args, [name])) {
      if (typeof args[name] !== 'string') {
        throw new Error(`string "${name}" expected`);
      }
      options[name] = args[name];
    }
  });

  return options;
}

export function setUpGitProxy(port: number) {
  shell.exec(`git config --global http.proxy http://127.0.0.1:${port}/`);
  shell.exec(`git config --global https.proxy http://127.0.0.1:${port}/`);
}

export function unsetGitProxy() {
  shell.exec(`git config --unset --global http.proxy`);
  shell.exec(`git config --unset --global https.proxy`);
}

function unsafeMain() {
  const pkgJson = readFileSync(`${__dirname}/../package.json`, 'utf-8');
  const version = JSON.parse(pkgJson).version;

  cli.version(version)
    // .option('-s, --socks [socks]', 'specify your socks proxy host, default: 127.0.0.1:1080')
    // .option('-p, --port [port]',
    // 'specify the listening port of http proxy server, default: 8080')
    // .option('-l, --host [host]',
    //         'specify the listening host of http proxy server, default: 127.0.0.1')
    .option('-c, --config [config]', 'read configs from file in json format')
    .option('-t, --tunneling', 'Force tunneling all traffic through proxies.')
    .option('--debug ', 'Enable debug mode')
    .option('--level [level]', 'log level, vals: info, error')
    .parse(process.argv);

  const options = getOptionsArgs(cli);

  if (!options.debug) {
    displayError = false;
  }

  if (options.logLevel && typeof options.logLevel === 'string') {
    init(options.logLevel);
  }

  // Find auto-pod.yaml.
  const configFile = options.config || 'auto-pod.yaml';
  const fileConfig = getFileConfig(configFile);

  if (options.tunneling) {
    // Overwrites force tunneling
    fileConfig.forceTunneling = true;
  }

  // Startup the server.
  const server = new ProxyServer(fileConfig);
  server.on('listening', () => {
    // Get running port
    const address = server.address() as AddressInfo;

    getLogger().info(`Server listening on: ${address.address}:${address.port}`);

    //
    // Shell run the pod.
    setUpGitProxy(address.port);
    getLogger().info("Setup git proxy.");

    // Run shelljs.
    // shell.exec("pod")
    // Copy the process env.
    const env = JSON.parse(JSON.stringify(process.env));
    env.http_proxy = `http://127.0.0.1:${address.port}`;
    env.https_proxy = `http://127.0.0.1:${address.port}`;

    getLogger().info("Starting pod...");
    const pod = spawn('pod', cli.args, {
      env,
      shell: true,
      stdio: 'pipe',
    });

    pod.stdout.on('data', (data) => {
      process.stdout.write(data);
    });

    pod.stderr.on('data', (data) => {
      process.stdout.write(data);
    });

    pod.on('close', (code: number) => {
      if (code !== 0) {
        getLogger().error('Failed to execute the pod.');
      } else {
        getLogger().info("Pod finished.");
      }

      unsetGitProxy();
      getLogger().info("Git proxy removed.");

      // Stop the server
      server.unref();

      // Shutdown the http proxy server.
      server.close();
    });
  });

  // Listening at any random port.
  server.listen({ host: '127.0.0.1', port: 0 });
}

export function main() {
  try {
    unsafeMain();
  } catch (e) {
    if (displayError) {
      getLogger().error(e);
      process.exit(1);
    }
  }
}
