/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';

const semver = require('semver');
const util = require('util');

const {
  AGENT_INFO: { SUPPORTED_NPM_VERSIONS },
} = require('./constants');

const VERSION_REGEX = /^npm@(\S+)\s+(\S+)$/m;
const isWin32 = process.platform === 'win32';

const execFile = util.promisify(require('child_process').execFile);

/**
 * @typedef {Object} Result
 * @property {string} name
 * @property {string} version
 * @property {{ [key: string]: Result | string }} dependencies
 * @property {{ [key: string]: Result | string }} devDependencies
 */

/**
 * @param {string} npm the path to the npm executable
 * @param {string} cwd directory in which we want to execute `npm ls`
 * @param {*} logger
 * @returns {Promise<Result>}
 */
// eslint-disable-next-line complexity
module.exports = async function listInstalled(npm, cwd, logger) {
  const execFileOpts = {
    cwd,
    env: { ...process.env, NODE_OPTIONS: undefined },
    maxBuffer: 1024 * 1024 * 128,
    shell: isWin32
  };
  let stdout;

  try {
    const result = await execFile(npm, ['help'], execFileOpts);
    stdout = result.stdout;
  } catch (err) {
    logger.debug('`npm help` returned an error: %o', err);
    // If npm encounters any errors whatsoever it will return with a non-zero
    // exit code but still output the relevant information to stdout.
    // If an even worse error occurs, we may not be able to parse stdout.
    stdout = err.stdout || '';
  }

  const [, version, location] = stdout.match(VERSION_REGEX) || [];
  if (!version)
    throw new Error(
      "Unable to locate `npm`. `npm` is required for your application's libraries to be reported to Contrast for analysis. You can use the `agent.node.npm_path` configuration variable to specify the path to the npm executable if it is not correctly detected by the agent. Please enable debug level logs for more information."
    );

  logger.debug('using npm version %s at %s', version, location);

  const lsArgs = ['ls', '--json', '--long'];
  if (semver.gte(version, '7.0.0')) lsArgs.push('--all');
  if (!semver.satisfies(version, SUPPORTED_NPM_VERSIONS))
    logger.warn(
      'The installed version of npm (%s at %s) can cause unexpected behavior. Please install a version that satisfies %s',
      version,
      location,
      SUPPORTED_NPM_VERSIONS
    );

  try {
    const result = await execFile(npm, lsArgs, execFileOpts);
    stdout = result.stdout;
  } catch (err) {
    logger.debug('`npm ls` returned an error: %s', err.stderr || err.message);
    stdout = err.stdout || '';
  }

  try {
    return JSON.parse(stdout);
  } catch (err) {
    logger.debug('parsing the output of `npm ls` failed: %o', err);
    throw new Error(
      '`npm ls` failed to provide a list of installed dependencies. Please enable debug level logs for more information.'
    );
  }
};
