"use strict";

const _ = require('lodash');

const os = require('os');

const url = require('url');

const path = require('path');

const chalk = require('chalk');

const debug = require('debug')('cypress:cli');

const {
  Listr
} = require('listr2');

const Promise = require('bluebird');

const logSymbols = require('log-symbols');

const {
  stripIndent
} = require('common-tags');

const fs = require('../fs');

const download = require('./download');

const util = require('../util');

const state = require('./state');

const unzip = require('./unzip');

const logger = require('../logger');

const {
  throwFormErrorText,
  errors
} = require('../errors');

const verbose = require('../VerboseRenderer');

const getNpmArgv = () => {
  const json = process.env.npm_config_argv;

  if (!json) {
    return;
  }

  debug('found npm argv json %o', json);

  try {
    return JSON.parse(json).original || [];
  } catch (e) {
    return [];
  }
}; // attempt to discover the version specifier used to install Cypress
// for example: "^5.0.0", "https://cdn.cypress.io/...", ...


const getVersionSpecifier = (startDir = path.resolve(__dirname, '../..')) => {
  const argv = getNpmArgv();

  if (argv) {
    const tgz = _.find(argv, t => t.endsWith('cypress.tgz'));

    if (tgz) {
      return tgz;
    }
  }

  const getVersionSpecifierFromPkg = dir => {
    debug('looking for versionSpecifier %o', {
      dir
    });

    const tryParent = () => {
      const parentPath = path.resolve(dir, '..');

      if (parentPath === dir) {
        debug('reached FS root with no versionSpecifier found');
        return;
      }

      return getVersionSpecifierFromPkg(parentPath);
    };

    return fs.readJSON(path.join(dir, 'package.json')).catch(() => ({})).then(pkg => {
      const specifier = _.chain(['dependencies', 'devDependencies', 'optionalDependencies']).map(prop => _.get(pkg, `${prop}.cypress`)).compact().first().value();

      return specifier || tryParent();
    });
  }; // recurse through parent directories until package.json with `cypress` is found


  return getVersionSpecifierFromPkg(startDir).then(versionSpecifier => {
    debug('finished looking for versionSpecifier', {
      versionSpecifier
    });
    return versionSpecifier;
  });
};

const betaNpmUrlRe = /^\/beta\/npm\/(?<version>[0-9.]+)\/(?<artifactSlug>[^/]+)\/cypress\.tgz$/; // convert a prerelease NPM package .tgz URL to the corresponding binary .zip URL

const getBinaryUrlFromPrereleaseNpmUrl = npmUrl => {
  let parsed;

  try {
    parsed = url.parse(npmUrl);
  } catch (e) {
    return;
  }

  const matches = betaNpmUrlRe.exec(parsed.pathname);

  if (parsed.hostname !== 'cdn.cypress.io' || !matches) {
    return;
  }

  const {
    version,
    artifactSlug
  } = matches.groups;
  parsed.pathname = `/beta/binary/${version}/${os.platform()}-${os.arch()}/${artifactSlug}/cypress.zip`;
  return parsed.format();
};

const alreadyInstalledMsg = () => {
  if (!util.isPostInstall()) {
    logger.log(stripIndent`
      Skipping installation:

        Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway.
    `);
  }
};

const displayCompletionMsg = () => {
  // check here to see if we are globally installed
  if (util.isInstalledGlobally()) {
    // if we are display a warning
    logger.log();
    logger.warn(stripIndent`
      ${logSymbols.warning} Warning: It looks like you\'ve installed Cypress globally.

        This will work, but it'\s not recommended.

        The recommended way to install Cypress is as a devDependency per project.

        You should probably run these commands:

        - ${chalk.cyan('npm uninstall -g cypress')}
        - ${chalk.cyan('npm install --save-dev cypress')}
    `);
    return;
  }

  logger.log();
  logger.log('You can now open Cypress by running:', chalk.cyan(path.join('node_modules', '.bin', 'cypress'), 'open'));
  logger.log();
  logger.log(chalk.grey('https://on.cypress.io/installing-cypress'));
  logger.log();
};

const downloadAndUnzip = ({
  version,
  installDir,
  downloadDir
}) => {
  const progress = {
    throttle: 100,
    onProgress: null
  };
  const downloadDestination = path.join(downloadDir, `cypress-${process.pid}.zip`);
  const rendererOptions = getRendererOptions(); // let the user know what version of cypress we're downloading!

  logger.log(`Installing Cypress ${chalk.gray(`(version: ${version})`)}`);
  logger.log();
  const tasks = new Listr([{
    options: {
      title: util.titleize('Downloading Cypress')
    },
    task: (ctx, task) => {
      // as our download progresses indicate the status
      progress.onProgress = progessify(task, 'Downloading Cypress');
      return download.start({
        version,
        downloadDestination,
        progress
      }).then(redirectVersion => {
        if (redirectVersion) version = redirectVersion;
        debug(`finished downloading file: ${downloadDestination}`);
      }).then(() => {
        // save the download destination for unzipping
        util.setTaskTitle(task, util.titleize(chalk.green('Downloaded Cypress')), rendererOptions.renderer);
      });
    }
  }, unzipTask({
    progress,
    zipFilePath: downloadDestination,
    installDir,
    rendererOptions
  }), {
    options: {
      title: util.titleize('Finishing Installation')
    },
    task: (ctx, task) => {
      const cleanup = () => {
        debug('removing zip file %s', downloadDestination);
        return fs.removeAsync(downloadDestination);
      };

      return cleanup().then(() => {
        debug('finished installation in', installDir);
        util.setTaskTitle(task, util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)), rendererOptions.renderer);
      });
    }
  }], {
    rendererOptions
  }); // start the tasks!

  return Promise.resolve(tasks.run());
};

const start = (options = {}) => {
  debug('installing with options %j', options);

  _.defaults(options, {
    force: false
  });

  const pkgVersion = util.pkgVersion();
  let needVersion = pkgVersion;
  let binaryUrlOverride;
  debug('version in package.json is', needVersion); // let this environment variable reset the binary version we need

  if (util.getEnv('CYPRESS_INSTALL_BINARY')) {
    // because passed file paths are often double quoted
    // and might have extra whitespace around, be robust and trim the string
    const trimAndRemoveDoubleQuotes = true;
    const envVarVersion = util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes);
    debug('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion);

    if (envVarVersion === '0') {
      debug('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install');
      logger.log(stripIndent`
        ${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`);
      logger.log();
      return Promise.resolve();
    }

    binaryUrlOverride = envVarVersion;
  }

  if (util.getEnv('CYPRESS_CACHE_FOLDER')) {
    const envCache = util.getEnv('CYPRESS_CACHE_FOLDER');
    logger.log(stripIndent`
        ${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)}

              Previous installs of Cypress may not be found.
      `);
    logger.log();
  }

  const installDir = state.getVersionDir(pkgVersion);
  const cacheDir = state.getCacheDir();
  const binaryDir = state.getBinaryDir(pkgVersion);
  return fs.ensureDirAsync(cacheDir).catch({
    code: 'EACCES'
  }, err => {
    return throwFormErrorText(errors.invalidCacheDirectory)(stripIndent`
    Failed to access ${chalk.cyan(cacheDir)}:

    ${err.message}
    `);
  }).then(() => {
    return Promise.all([state.getBinaryPkgAsync(binaryDir).then(state.getBinaryPkgVersion), getVersionSpecifier()]);
  }).then(([binaryVersion, versionSpecifier]) => {
    if (!binaryUrlOverride && versionSpecifier) {
      const computedBinaryUrl = getBinaryUrlFromPrereleaseNpmUrl(versionSpecifier);

      if (computedBinaryUrl) {
        debug('computed binary url from version specifier %o', {
          computedBinaryUrl,
          needVersion
        });
        binaryUrlOverride = computedBinaryUrl;
      }
    }

    needVersion = binaryUrlOverride || needVersion;
    debug('installed version is', binaryVersion, 'version needed is', needVersion);

    if (!binaryVersion) {
      debug('no binary installed under cli version');
      return true;
    }

    logger.log();
    logger.log(stripIndent`
      Cypress ${chalk.green(binaryVersion)} is installed in ${chalk.cyan(installDir)}
      `);
    logger.log();

    if (options.force) {
      debug('performing force install over existing binary');
      return true;
    }

    if (binaryVersion === needVersion || !util.isSemver(needVersion)) {
      // our version matches, tell the user this is a noop
      alreadyInstalledMsg();
      return false;
    }

    return true;
  }).then(shouldInstall => {
    // noop if we've been told not to download
    if (!shouldInstall) {
      debug('Not downloading or installing binary');
      return;
    }

    if (needVersion !== pkgVersion) {
      logger.log(chalk.yellow(stripIndent`
          ${logSymbols.warning} Warning: Forcing a binary version different than the default.

            The CLI expected to install version: ${chalk.green(pkgVersion)}

            Instead we will install version: ${chalk.green(needVersion)}

            These versions may not work properly together.
        `));
      logger.log();
    } // see if version supplied is a path to a binary


    return fs.pathExistsAsync(needVersion).then(exists => {
      if (exists) {
        return path.extname(needVersion) === '.zip' ? needVersion : false;
      }

      const possibleFile = util.formAbsolutePath(needVersion);
      debug('checking local file', possibleFile, 'cwd', process.cwd());
      return fs.pathExistsAsync(possibleFile).then(exists => {
        // if this exists return the path to it
        // else false
        if (exists && path.extname(possibleFile) === '.zip') {
          return possibleFile;
        }

        return false;
      });
    }).then(pathToLocalFile => {
      if (pathToLocalFile) {
        const absolutePath = path.resolve(needVersion);
        debug('found local file at', absolutePath);
        debug('skipping download');
        const rendererOptions = getRendererOptions();
        return new Listr([unzipTask({
          progress: {
            throttle: 100,
            onProgress: null
          },
          zipFilePath: absolutePath,
          installDir,
          rendererOptions
        })], {
          rendererOptions
        }).run();
      }

      if (options.force) {
        debug('Cypress already installed at', installDir);
        debug('but the installation was forced');
      }

      debug('preparing to download and unzip version ', needVersion, 'to path', installDir);
      const downloadDir = os.tmpdir();
      return downloadAndUnzip({
        version: needVersion,
        installDir,
        downloadDir
      });
    }) // delay 1 sec for UX, unless we are testing
    .then(() => {
      return Promise.delay(1000);
    }).then(displayCompletionMsg);
  });
};

module.exports = {
  start,
  _getVersionSpecifier: getVersionSpecifier,
  _getBinaryUrlFromPrereleaseNpmUrl: getBinaryUrlFromPrereleaseNpmUrl
};

const unzipTask = ({
  zipFilePath,
  installDir,
  progress,
  rendererOptions
}) => {
  return {
    options: {
      title: util.titleize('Unzipping Cypress')
    },
    task: (ctx, task) => {
      // as our unzip progresses indicate the status
      progress.onProgress = progessify(task, 'Unzipping Cypress');
      return unzip.start({
        zipFilePath,
        installDir,
        progress
      }).then(() => {
        util.setTaskTitle(task, util.titleize(chalk.green('Unzipped Cypress')), rendererOptions.renderer);
      });
    }
  };
};

const progessify = (task, title) => {
  // return higher order function
  return (percentComplete, remaining) => {
    percentComplete = chalk.white(` ${percentComplete}%`); // pluralize seconds remaining

    remaining = chalk.gray(`${remaining}s`);
    util.setTaskTitle(task, util.titleize(title, percentComplete, remaining), getRendererOptions().renderer);
  };
}; // if we are running in CI then use
// the verbose renderer else use
// the default


const getRendererOptions = () => {
  let renderer = util.isCi() ? verbose : 'default';

  if (logger.logLevel() === 'silent') {
    renderer = 'silent';
  }

  return {
    renderer
  };
};