#!/usr/bin/env node
import Progress from 'progress';
import copy from '../lib/copy';
import { resolve } from 'path';
import login from '../lib/login';
import { stat } from 'fs-promise';
import * as cfg from '../lib/cfg';
import { version } from '../../package';
import Logger from '../lib/build-logger';
import bytes from 'bytes';
import chalk from 'chalk';
import minimist from 'minimist';
import Now from '../lib';
import toHumanPath from '../lib/utils/to-human-path';
import promptOptions from '../lib/utils/prompt-options';
import ms from 'ms';
import { handleError, error } from '../lib/error';

const argv = minimist(process.argv.slice(2), {
  string: ['config', 'token'],
  boolean: ['help', 'version', 'debug', 'force', 'login', 'no-clipboard', 'forward-npm', 'docker', 'npm'],
  alias: {
    env: 'e',
    help: 'h',
    config: 'c',
    debug: 'd',
    version: 'v',
    force: 'f',
    token: 't',
    forceSync: 'F',
    login: 'L',
    public: 'p',
    'no-clipboard': 'C',
    'forward-npm': 'N'
  }
});

const help = () => {
  console.log(`
  ${chalk.bold('đťš« now')} [options] <command | path>

  ${chalk.dim('Commands:')}

    deploy       [path]       performs a deployment ${chalk.bold('(default)')}
    ls | list    [app]        list deployments
    rm | remove  [id]         remove a deployment
    ln | alias   [id] [url]   configures aliases for deployments
    domains      [name]       manages your domain names
    help         [cmd]        displays complete help for [cmd]

  ${chalk.dim('Options:')}

    -h, --help                output usage information
    -v, --version             output the version number
    -c ${chalk.underline('FILE')}, --config=${chalk.underline('FILE')}    config file
    -d, --debug               debug mode [off]
    -f, --force               force a new deployment even if nothing has changed
    -t ${chalk.underline('TOKEN')}, --token=${chalk.underline('TOKEN')}   login token
    -L, --login               configure login
    -p, --public              deployment is public (${chalk.dim('`/_src`')} is exposed) [on for oss, off for premium]
    -e, --env                 include an env var (e.g.: ${chalk.dim('`-e KEY=value`')}). Can appear many times.
    -C, --no-clipboard        do not attempt to copy URL to clipboard
    -N, --forward-npm         forward login information to install private NPM modules

  ${chalk.dim('Examples:')}

  ${chalk.gray('–')} Deploys the current directory

    ${chalk.cyan('$ now')}

  ${chalk.gray('–')} Deploys a custom path ${chalk.dim('`/usr/src/project`')}

    ${chalk.cyan('$ now /usr/src/project')}

  ${chalk.gray('–')} Lists all deployments with their IDs

    ${chalk.cyan('$ now ls')}

  ${chalk.gray('–')} Associates deployment ${chalk.dim('`deploymentId`')} with ${chalk.dim('`custom-domain.com`')}

    ${chalk.cyan('$ now alias deploymentId custom-domain.com')}

  ${chalk.gray('–')} Stores a secret

    ${chalk.cyan('$ now secret add mysql-password 123456')}

  ${chalk.gray('–')} Deploys with ENV vars (using the ${chalk.dim('`mysql-password`')} secret stored above)

    ${chalk.cyan('$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password')}

  ${chalk.gray('–')} Displays comprehensive help for the subcommand ${chalk.dim('`list`')}

    ${chalk.cyan('$ now help list')}
`);
};

let path = argv._[0];

if (null != path) {
  // if path is relative: resolve
  // if path is absolute: clear up strange `/` etc
  path = resolve(process.cwd(), path);
} else {
  path = process.cwd();
}

const exit = (code) => {
  // we give stdout some time to flush out
  // because there's a node bug where
  // stdout writes are asynchronous
  // https://github.com/nodejs/node/issues/6456
  setTimeout(() => process.exit(code || 0), 100);
};

// options
const debug = argv.debug;
const clipboard = !argv['no-clipboard'];
const forwardNpm = argv['forward-npm'];
const forceNew = argv.force;
const forceSync = argv.forceSync;
const shouldLogin = argv.login;
const wantsPublic = argv.public;
const apiUrl = argv.url || 'https://api.zeit.co';
const isTTY = process.stdout.isTTY;
const quiet = !isTTY;
if (argv.config) cfg.setConfigFile(argv.config);
const config = cfg.read();
const alwaysForwardNpm = config.forwardNpm;

if (argv.h || argv.help) {
  help();
  exit(0);
} else if (argv.v || argv.version) {
  console.log(chalk.bold('đťš« now'), version);
  process.exit(0);
} else if (!(argv.token || config.token) || shouldLogin) {
  login(apiUrl)
  .then((token) => {
    if (shouldLogin) {
      console.log('> Logged in successfully. Token saved in ~/.now.json');
      process.exit(0);
    } else {
      sync(token).catch((err) => {
        error(`Unknown error: ${err.stack}`);
        process.exit(1);
      });
    }
  })
  .catch((e) => {
    error(`Authentication error – ${e.message}`);
    process.exit(1);
  });
} else {
  sync(argv.token || config.token).catch((err) => {
    error(`Unknown error: ${err.stack}`);
    process.exit(1);
  });
}

async function sync (token) {
  const start = Date.now();

  if (!quiet) {
    console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`);
  }

  try {
    await stat(path);
  } catch (err) {
    error(`Could not read directory ${chalk.bold(path)}`);
    process.exit(1);
  }

  let deploymentType, hasPackage, hasDockerfile;

  if (argv.docker) {
    if (debug) {
      console.log(`> [debug] Forcing \`deploymentType\` = \`docker\``);
    }
    deploymentType = 'docker';
  } else {
    if (argv.npm) {
      deploymentType = 'npm';
    } else {
      try {
        await stat(resolve(path, 'package.json'));
      } catch (err) {
        hasPackage = true;
      }

      [hasPackage, hasDockerfile] = await Promise.all([
        await (async () => {
          try {
            await stat(resolve(path, 'package.json'));
          } catch (err) {
            return false;
          }
          return true;
        })(),
        await (async () => {
          try {
            await stat(resolve(path, 'Dockerfile'));
          } catch (err) {
            return false;
          }
          return true;
        })()
      ]);

      if (hasPackage && hasDockerfile) {
        if (debug) console.log('[debug] multiple manifests found, disambiguating');
        if (isTTY) {
          try {
            console.log(`> Two manifests found. Press [${chalk.bold('n')}] to deploy or re-run with --flag`);
            deploymentType = await promptOptions([
              ['npm', `${chalk.bold('package.json')}\t${chalk.gray('   --npm')} `],
              ['docker', `${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `]
            ]);
          } catch (err) {
            error(err.message);
            process.exit(1);
          }
        } else {
          error('Ambiguous deployment (`package.json` and `Dockerfile` found). ' +
            'Please supply `--npm` or `--docker` to disambiguate.');
        }
      } else if (hasPackage) {
        if (debug) console.log('[debug] `package.json` found, assuming `deploymentType` = `npm`');
        deploymentType = 'npm';
      } else if (hasDockerfile) {
        if (debug) console.log('[debug] `Dockerfile` found, assuming `deploymentType` = `docker`');
        deploymentType = 'docker';
      } else {
        error(`Could not access a ${chalk.dim('`package.json`')} or ${chalk.dim('`Dockerfile`')} in ${chalk.bold(path)}`);
        console.log(`> To deploy statically, try: ${chalk.dim(chalk.underline('https://zeit.co/blog/serve-it-now'))}.`);
        process.exit(1);
      }
    }
  }

  const now = new Now(apiUrl, token, { debug });
  const envs = [].concat(argv.env || []);

  let secrets;
  const findSecret = async (uidOrName) => {
    if (!secrets) secrets = await now.listSecrets();
    return secrets.filter((secret) => {
      return secret.name === uidOrName || secret.uid === uidOrName;
    });
  };

  const env_ = await Promise.all(envs.map(async (kv) => {
    const [key, val_, ...rest] = kv.split('=');
    let val;

    if (rest.length) {
      error(`Invalid env ${chalk.bold(`"${kv}"`)}. It cannot contain more than one ${chalk.dim(`=`)} symbol`);
      return process.exit(1);
    }

    if (/[^A-z0-9_]/i.test(key)) {
      error(`Invalid ${chalk.dim('-e')} key ${chalk.bold(`"${chalk.bold(key)}"`)}. Only letters, digits and underscores are allowed.`);
      return process.exit(1);
    }

    if ('' === key || null == key) {
      error(`Invalid env option ${chalk.bold(`"${kv}"`)}`);
      return process.exit(1);
    }

    if (val_ == null) {
      if (!(key in process.env)) {
        error(`No value specified for env ${chalk.bold(`"${chalk.bold(key)}"`)} and it was not found in your env.`);
        return process.exit(1);
      } else {
        console.log(`> Reading ${chalk.bold(`"${chalk.bold(key)}"`)} from your env (as no value was specified)`);
        // escape value if it begins with @
        val = process.env[key].replace(/^\@/, '\\@');
      }
    } else {
      val = val_;
    }

    if ('@' === val[0]) {
      const uidOrName = val.substr(1);
      const secrets = await findSecret(uidOrName);
      if (secrets.length === 0) {
        if ('' === uidOrName) {
          error(`Empty reference provided for env key ${chalk.bold(`"${chalk.bold(key)}"`)}`);
        } else {
          error(`No secret found by uid or name ${chalk.bold(`"${uidOrName}"`)}`);
        }
        return process.exit(1);
      } else if (secrets.length > 1) {
        error(`Ambiguous secret ${chalk.bold(`"${uidOrName}"`)} (matches ${chalk.bold(secrets.length)} secrets)`);
        return process.exit(1);
      } else {
        val = { uid: secrets[0].uid };
      }
    }

    return [
      key,
      typeof val === 'string'
        // add support for escaping the @ as \@
        ? val.replace(/^\\@/, '@')
        : val
    ];
  }));

  let env = {};
  env_
  .filter(v => !!v)
  .forEach(([key, val]) => {
    if (key in env) console.log(`> ${chalk.yellow('NOTE:')} Overriding duplicate env key ${chalk.bold(`"${key}"`)}`);
    env[key] = val
  });

  try {
    await now.create(path, {
      env,
      deploymentType,
      forceNew,
      forceSync,
      forwardNpm: alwaysForwardNpm || forwardNpm,
      quiet,
      wantsPublic
    });
  } catch (err) {
    if (debug) console.log(`> [debug] error: ${err.stack}`);
    handleError(err);
    process.exit(1);
  }

  const { url } = now;
  const elapsed = ms(new Date() - start);

  if (isTTY) {
    if (clipboard) {
      try {
        await copy(url);
        console.log(`${chalk.cyan('> Ready!')} ${chalk.bold(url)} (copied to clipboard) [${elapsed}]`);
      } catch (err) {
        console.log(`${chalk.cyan('> Ready!')} ${chalk.bold(url)} [${elapsed}]`);
      }
    } else {
      console.log(`> ${url} [${elapsed}]`);
    }
  } else {
    process.stdout.write(url);
  }

  const start_u = new Date();
  const complete = () => {
    if (!quiet) {
      const elapsed_u = ms(new Date() - start_u);
      console.log(`> Sync complete (${bytes(now.syncAmount)}) [${elapsed_u}] `);
      console.log('> Initializing…');
    }

    // close http2 agent
    now.close();

    // show build logs
    printLogs(now.host);
  };

  if (now.syncAmount) {
    const bar = new Progress('> Upload [:bar] :percent :etas', {
      width: 20,
      complete: '=',
      incomplete: '',
      total: now.syncAmount
    });

    now.upload();

    now.on('upload', ({ names, data }) => {
      const amount = data.length;
      if (debug) {
        console.log(`> [debug] Uploaded: ${names.join(' ')} (${bytes(data.length)})`);
      }
      bar.tick(amount);
    });

    now.on('complete', complete);

    now.on('error', (err) => {
      error('Upload failed');
      handleError(err);
      process.exit(1);
    });
  } else {
    if (!quiet) {
      console.log(`> Initializing…`);
    }

    // close http2 agent
    now.close();

    // show build logs
    printLogs(now.host);
  }
}

function printLogs (host) {
  // log build
  const logger = new Logger(host, { debug, quiet });
  logger.on('close', () => {
    if (!quiet) {
      console.log(`${chalk.cyan('> Deployment complete!')}`);
    }
    process.exit(0);
  });
}