/**
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';

/**
 * @module lib/rules/protect/SampleAggregator
 */

const _ = require('lodash');

const { IMPORTANCE } = require('../constants');

/**
 * Compares two findings and returns the one which is
 * ranked higher in terms of effectiveness. If their
 * effectiveness is the same, <code>null</code> is
 * returned, signifying that a ranking order cannot be
 * determined for this criteria.
 * @param   {object}      a1 First finding
 * @param   {object}      b2 Second finding
 * @returns {object | null}
 */
const rankEffectiveness = (a, b) => {
  /* If both findings share effectiveness, we can't rank them. */
  if (a.sample.effective === b.sample.effective) {
    return null;
  }

  // effective is a boolean, so there is no "higher" effectiveness.
  return a.sample.effective ? a : b;
};

/**
 * Compares two findings and returns the one which has the highest
 * score. If their scores are the same, return the first finding.
 * @param   {object}      a First finding
 * @param   {object}      b Second finding
 * @returns {object}
 */
const POINTS_PATH = 'sample.assessment.results.points';

function rankPoints(a, b) {
  const pA = _.get(a, POINTS_PATH);
  const pB = _.get(b, POINTS_PATH);

  // return a if a is greater than or equal to b
  return pA < pB ? b : a;
}

/**
 * Ranks two findings against effectiveness and score.
 * If the second findings does not "outrank" the first,
 * then the first will be returned.
 * @param   {object} a First finding
 * @param   {object} b Second finding
 * @returns {object}   The highest-ranked finding
 */
const rankFindings = (a, b) => rankEffectiveness(a, b) || rankPoints(a, b);

/**
 * Takes a collection of findings and organizes them into a map having keys
 * being unique input types and paths, and values being unique findings pertaining
 * to each key.
 * @param   {object} memo    The map being created
 * @param   {object} finding Current finding in iteratee
 * @returns {object}         Map of ranked findings per type/path
 */
function rankIntoMap(memo, finding) {
  const type = _.get(finding, 'sample.input.type', '_');
  const path = _.get(finding, 'sample.input.documentPath', '_');
  const name = _.get(finding, 'sample.input.name', '_');

  // this string is the key to the map
  const memoPath = `${type}.${path}.${name}`;

  const existing = memo[memoPath];
  // if it exists, rank it against the new finding
  memo[memoPath] = existing ? rankFindings(existing, finding) : finding;

  // memo returned despite having side effects because this function is
  // called using reduce.
  return memo;
}

/**
 * Accepts a collection of findings and returns a new collection whose samples
 * are unique with respect to input type and path of the finding per input type.
 * Any duplicate findings for the same type and path will be filtered to only
 * include the one with the highest points/effectiveness ranking.
 * @param   {object[]} findings The findings to aggregate.
 * @param   {function} wwFilter filter to check if a sample should be saved.
 * @returns {object[]}
 *
 * effective, but not blocked, becomes PROBED.
 *
 */
function aggregate(findings, wwFilter) {
  // separate the findings into effective and ineffective
  // but marked as worth-watching by agent-lib. if not in
  // one of the two lists, the finding is not relevant.
  const effective = [];
  const wwFindings = [];
  // and the rest were not agent-lib worth-watching or effective
  for (const finding of findings) {
    const { sample } = finding;
    if (sample.confirmedAttack || sample.effective) {
      effective.push(finding);
    } else if (finding.rule.agentLibBit && sample.assessment.results.importance === IMPORTANCE.WORTH_WATCHING) {
      //console.log(finding.rule.id, s.results)
      wwFindings.push(finding);
    }
  }

  // do we need to run any inputs through the full scoring (not the abbreviated
  // worth-watching scoring)?
  if (wwFindings.length) {
    for (const finding of wwFindings) {
      if (wwFilter(finding)) {
        finding.sample.assessment.results.importance = IMPORTANCE.DEFINITE;
        // put this in the "effective list", even though it wasn't effective, so it
        // will be reported as PROBED.
        effective.push(finding);
      }
    }
  }

  return _.values(effective.reduce(rankIntoMap, {}));
}

module.exports = {
  aggregate
};
