"use strict";

const la = require('lazy-ass');
const is = require('check-more-types');
const os = require('os');
const url = require('url');
const path = require('path');
const debug = require('debug')('cypress:cli');
const request = require('@cypress/request');
const Promise = require('bluebird');
const requestProgress = require('request-progress');
const {
  stripIndent
} = require('common-tags');
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
const {
  throwFormErrorText,
  errors
} = require('../errors');
const fs = require('../fs');
const util = require('../util');
const defaultBaseUrl = 'https://download.cypress.io/';
const defaultMaxRedirects = 10;
const getProxyForUrlWithNpmConfig = url => {
  return getProxyForUrl(url) || process.env.npm_config_https_proxy || process.env.npm_config_proxy || null;
};
const getBaseUrl = () => {
  if (util.getEnv('CYPRESS_DOWNLOAD_MIRROR')) {
    let baseUrl = util.getEnv('CYPRESS_DOWNLOAD_MIRROR');
    if (!baseUrl.endsWith('/')) {
      baseUrl += '/';
    }
    return baseUrl;
  }
  return defaultBaseUrl;
};
const getCA = () => {
  return new Promise(resolve => {
    if (process.env.npm_config_cafile) {
      fs.readFile(process.env.npm_config_cafile, 'utf8').then(cafileContent => {
        resolve(cafileContent);
      }).catch(() => {
        resolve();
      });
    } else if (process.env.npm_config_ca) {
      resolve(process.env.npm_config_ca);
    } else {
      resolve();
    }
  });
};
const prepend = (arch, urlPath, version) => {
  const endpoint = url.resolve(getBaseUrl(), urlPath);
  const platform = os.platform();
  const pathTemplate = util.getEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', true);
  return pathTemplate ? pathTemplate.replace(/\\?\$\{endpoint\}/g, endpoint).replace(/\\?\$\{platform\}/g, platform).replace(/\\?\$\{arch\}/g, arch).replace(/\\?\$\{version\}/g, version) : `${endpoint}?platform=${platform}&arch=${arch}`;
};
const getUrl = (arch, version) => {
  if (is.url(version)) {
    debug('version is already an url', version);
    return version;
  }
  const urlPath = version ? `desktop/${version}` : 'desktop';
  return prepend(arch, urlPath, version);
};
const statusMessage = err => {
  return err.statusCode ? [err.statusCode, err.statusMessage].join(' - ') : err.toString();
};
const prettyDownloadErr = (err, url) => {
  const msg = stripIndent`
    URL: ${url}
    ${statusMessage(err)}
  `;
  debug(msg);
  return throwFormErrorText(errors.failedDownload)(msg);
};

/**
 * Checks checksum and file size for the given file. Allows both
 * values or just one of them to be checked.
 */
const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => {
  if (expectedSize && expectedChecksum) {
    debug('verifying checksum and file size');
    return Promise.join(util.getFileChecksum(filename), util.getFileSize(filename), (checksum, filesize) => {
      if (checksum === expectedChecksum && filesize === expectedSize) {
        debug('downloaded file has the expected checksum and size ✅');
        return;
      }
      debug('raising error: checksum or file size mismatch');
      const text = stripIndent`
          Corrupted download

          Expected downloaded file to have checksum: ${expectedChecksum}
          Computed checksum: ${checksum}

          Expected downloaded file to have size: ${expectedSize}
          Computed size: ${filesize}
        `;
      debug(text);
      throw new Error(text);
    });
  }
  if (expectedChecksum) {
    debug('only checking expected file checksum %d', expectedChecksum);
    return util.getFileChecksum(filename).then(checksum => {
      if (checksum === expectedChecksum) {
        debug('downloaded file has the expected checksum ✅');
        return;
      }
      debug('raising error: file checksum mismatch');
      const text = stripIndent`
        Corrupted download

        Expected downloaded file to have checksum: ${expectedChecksum}
        Computed checksum: ${checksum}
      `;
      throw new Error(text);
    });
  }
  if (expectedSize) {
    // maybe we don't have a checksum, but at least CDN returns content length
    // which we can check against the file size
    debug('only checking expected file size %d', expectedSize);
    return util.getFileSize(filename).then(filesize => {
      if (filesize === expectedSize) {
        debug('downloaded file has the expected size ✅');
        return;
      }
      debug('raising error: file size mismatch');
      const text = stripIndent`
          Corrupted download

          Expected downloaded file to have size: ${expectedSize}
          Computed size: ${filesize}
        `;
      throw new Error(text);
    });
  }
  debug('downloaded file lacks checksum or size to verify');
  return Promise.resolve();
};

// downloads from given url
// return an object with
// {filename: ..., downloaded: true}
const downloadFromUrl = ({
  url,
  downloadDestination,
  progress,
  ca,
  version,
  redirectTTL = defaultMaxRedirects
}) => {
  if (redirectTTL <= 0) {
    return Promise.reject(new Error(stripIndent`
          Failed downloading the Cypress binary.
          There were too many redirects. The default allowance is ${defaultMaxRedirects}.
          Maybe you got stuck in a redirect loop?
        `));
  }
  return new Promise((resolve, reject) => {
    const proxy = getProxyForUrlWithNpmConfig(url);
    debug('Downloading package', {
      url,
      proxy,
      downloadDestination
    });
    if (ca) {
      debug('using custom CA details from npm config');
    }
    const reqOptions = {
      uri: url,
      ...(proxy ? {
        proxy
      } : {}),
      ...(ca ? {
        agentOptions: {
          ca
        }
      } : {}),
      method: 'GET',
      followRedirect: false
    };
    const req = request(reqOptions);

    // closure
    let started = null;
    let expectedSize;
    let expectedChecksum;
    requestProgress(req, {
      throttle: progress.throttle
    }).on('response', response => {
      // we have computed checksum and filesize during test runner binary build
      // and have set it on the S3 object as user meta data, available via
      // these custom headers "x-amz-meta-..."
      // see https://github.com/cypress-io/cypress/pull/4092
      expectedSize = response.headers['x-amz-meta-size'] || response.headers['content-length'];
      expectedChecksum = response.headers['x-amz-meta-checksum'];
      if (expectedChecksum) {
        debug('expected checksum %s', expectedChecksum);
      }
      if (expectedSize) {
        // convert from string (all Amazon custom headers are strings)
        expectedSize = Number(expectedSize);
        debug('expected file size %d', expectedSize);
      }

      // start counting now once we've gotten
      // response headers
      started = new Date();
      if (/^3/.test(response.statusCode)) {
        const redirectVersion = response.headers['x-version'];
        const redirectUrl = response.headers.location;
        debug('redirect version:', redirectVersion);
        debug('redirect url:', redirectUrl);
        downloadFromUrl({
          url: redirectUrl,
          progress,
          ca,
          downloadDestination,
          version: redirectVersion,
          redirectTTL: redirectTTL - 1
        }).then(resolve).catch(reject);

        // if our status code does not start with 200
      } else if (!/^2/.test(response.statusCode)) {
        debug('response code %d', response.statusCode);
        const err = new Error(stripIndent`
          Failed downloading the Cypress binary.
          Response code: ${response.statusCode}
          Response message: ${response.statusMessage}
        `);
        reject(err);
        // status codes here are all 2xx
      } else {
        // We only enable this pipe connection when we know we've got a successful return
        // and handle the completion with verify and resolve
        // there was a possible race condition between end of request and close of writeStream
        // that is made ordered with this Promise.all
        Promise.all([new Promise(r => {
          return response.pipe(fs.createWriteStream(downloadDestination).on('close', r));
        }), new Promise(r => response.on('end', r))]).then(() => {
          debug('downloading finished');
          verifyDownloadedFile(downloadDestination, expectedSize, expectedChecksum).then(() => debug('verified')).then(() => resolve(version)).catch(reject);
        });
      }
    }).on('error', e => {
      if (e.code === 'ECONNRESET') return; // sometimes proxies give ECONNRESET but we don't care

      reject(e);
    }).on('progress', state => {
      // total time we've elapsed
      // starting on our first progress notification
      const elapsed = new Date() - started;

      // request-progress sends a value between 0 and 1
      const percentage = util.convertPercentToPercentage(state.percent);
      const eta = util.calculateEta(percentage, elapsed);

      // send up our percent and seconds remaining
      progress.onProgress(percentage, util.secsRemaining(eta));
    });
  });
};

/**
 * Download Cypress.zip from external versionUrl to local file.
 * @param [string] version Could be "3.3.0" or full URL
 * @param [string] downloadDestination Local filename to save as
 */
const start = async opts => {
  let {
    version,
    downloadDestination,
    progress,
    redirectTTL
  } = opts;
  if (!downloadDestination) {
    la(is.unemptyString(downloadDestination), 'missing download dir', opts);
  }
  if (!progress) {
    progress = {
      onProgress: () => {
        return {};
      }
    };
  }
  const arch = await util.getRealArch();
  const versionUrl = getUrl(arch, version);
  progress.throttle = 100;
  debug('needed Cypress version: %s', version);
  debug('source url %s', versionUrl);
  debug(`downloading cypress.zip to "${downloadDestination}"`);

  // ensure download dir exists
  return fs.ensureDirAsync(path.dirname(downloadDestination)).then(() => {
    return getCA();
  }).then(ca => {
    return downloadFromUrl({
      url: versionUrl,
      downloadDestination,
      progress,
      ca,
      version,
      ...(redirectTTL ? {
        redirectTTL
      } : {})
    });
  }).catch(err => {
    return prettyDownloadErr(err, versionUrl);
  });
};
module.exports = {
  start,
  getUrl,
  getProxyForUrlWithNpmConfig,
  getCA
};