/**
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.
*/
/**
 * @module lib/reporter/models/AppUpdate
 */
'use strict';
const path = require('path');
const Library = require('./library');
const logger = require('../../../core/logger')('contrast:AppUpdate');
const libPathHash = new Map();

/*
  Oversimplified replacement for _.get from Lodash
  that works for predictable paths (split into array of keys)
 */
const get = (obj, path) =>
  path.reduce((xs, x) => (xs && xs[x] ? xs[x] : undefined), obj);

module.exports = class AppUpdate {
  /**
   * @param {Object}    data
   * @param {Library[]} data.libs Libraries
   * @param {Number}    loc       override state for lines of code
   */
  constructor(data, agent) {
    this.timestamp = Date.now();
    this.tags = get(agent, 'config.application.inventory.tags'.split('.'));

    if (data) {
      this.libraries = Object.values(data.libs);
    }
  }

  /**
   * Builds routes, and formats libraries if present and returns
   * serialized object for reporting
   */
  toSerializable() {
    const routeCoverage = require('../../../coverage')();
    const routes = routeCoverage.getReport();
    routeCoverage.clearRoutes();

    const obj = {
      timestamp: this.timestamp,
      routes
    };

    if (this.libraries && this.libraries.length) {
      obj.libraries = this.libraries.reduce((libs, lib) => {
        libs.push(lib.toSerializable());
        return libs;
      }, []);
    }

    return obj;
  }

  /**
   * Adds lib to internal record of libs.
   * @param   {Object} data  library data
   * @param   {Object} agent agent instance for determining if extraneous libs enabled
   * @returns {Boolean} Whether or not to traverse children of the current lib.
   */
  static addLib(data, agent) {
    const readExtraneous = get(
      agent,
      'config.agent.node.library_usage.read_extraneous_libraries'.split('.')
    );
    // this simply means it doesn't appear in the dependencies list in package.json
    // but doesn't necessarily mean it's not being used
    // this is an improvement for now because it means that we won't report any devDependencies,
    // however the only correct way to do this is to begin sending whether or not a library was used
    if (!readExtraneous && data.extraneous) {
      return false;
    }

    const { name, version } = data;

    // exists somewhere else and DOES have a shasum. so, we check if we can find a hash/key, and then
    // we check if the library has been seen or not.
    if (!version) {
      logger.info(
        `package: ${name} is missing version in package.json or it might not have been installed; unable to report lib.`,
        data
      );
      return false;
    }

    // data gets added to map for a given library before we start traversing
    // dependency tree, so we need to get that data and merge when creating a proper Library
    const cachedData = AppUpdate.getLibByPath(data.path);
    const lib = new Library({ ...data, ...cachedData }, this.tags);

    libPathHash.set(lib._path, lib);
    return true;
  }

  /**
   * Gets the library from Map if exists, otherwise creates entry with _path
   *
   * @param {string} file full file path
   */
  static getLibByPath(file) {
    if (!file) {
      return {};
    }
    const { sep } = path;
    const pkgPath = file.split(sep);
    let moduleFolder = pkgPath.lastIndexOf('node_modules') + 1;

    // need to bump by 1 path segement for scoped packages
    if (pkgPath[moduleFolder].startsWith('@')) {
      moduleFolder += 1;
    }

    const _path = pkgPath.slice(0, moduleFolder + 1).join(sep);

    if (_path && !libPathHash.has(_path)) {
      libPathHash.set(_path, { _path });
    }

    return libPathHash.get(_path) || {};
  }

  /**
   * Gets hash for a given file or generates a new one
   *
   * @param {string} file full file path
   */
  static getHashByPath(file) {
    const lib = AppUpdate.getLibByPath(file);
    // For local libraries
    return (
      lib.hash ||
      AppUpdate.generateHashFromPath(lib._path.replace('file://', ''))
    );
  }

  /**
   * generates a hash for a file from a file path
   *
   * @param {string} moduleRootPath path to module root
   */
  static generateHashFromPath(moduleRootPath) {
    const pkg = require(path.resolve(moduleRootPath, 'package.json'));
    return Library.getHash(pkg);
  }

  /**
   * returns collection of libraries
   * @return {{ [key: string]: Library }}
   */
  static get libraries() {
    const data = {};
    for (const lib of libPathHash.values()) {
      // libraries that get require'd outside of app root
      // exist in Map but are not Library instances
      // See: https://contrast.atlassian.net/browse/NODE-866
      if (lib instanceof Library) {
        data[lib._id] = lib;
      }
    }

    return data;
  }
};
