/**
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 _ = require('lodash');
const logger = require('../../core/logger')('contrast:protect:assessment');

const {
  IMPORTANCE: { NONE },
  WEIGHTS
} = require('./common');

module.exports = Assessment;

/**
 * An object representing the outcome of applying evaluation rules
 * to an input string. It details scoring information, attack confidence,
 * and keeps track of the rules that contributed to the score outcome.
 * @param   {Object} assessment  In case an existing assessment should be
 *                               used to continue an evaluation process.
 * @returns {Object)
 */
function Assessment(
  assessment = {
    keywords: { points: 0, ids: [] },
    patterns: { points: 0, ids: [] },
    searchers: { points: 0, ids: [] },
    results: {
      importance: NONE,
      points: 0,
      score: NONE
    },
    confident: false
  }
) {
  const updateResults = (function(currentHigh) {
    return function(score) {
      const scoreValue = WEIGHTS[score];
      const isGreater = currentHigh < scoreValue;

      if (isGreater) {
        currentHigh = scoreValue;
        assessment.results.score = score;
      }

      assessment.results.points += scoreValue;
      return assessment.results;
    };
  })(0);

  // module API
  return {
    /**
     * Adds a pattern to the assessment when the evaluation detects it.
     */
    addPattern(patternRule) {
      if (!this.hasPattern(patternRule.id)) {
        assessment.patterns.ids.push(patternRule.id);
        assessment.patterns.points += WEIGHTS[patternRule.score];
        updateResults(patternRule.score);
      }

      return this;
    },

    /**
     * Returns whether the assessment has matched and collected
     * the given custom pattern id.
     * @param   {String}  id The pattern ID
     * @returns {Boolean}
     */
    hasPattern(id) {
      return 0 <= assessment.patterns.ids.indexOf(id);
    },

    /**
     * Adds a keyword to the assessment when the evaluation detects it.
     */
    addKeyword(keywordRule) {
      if (!this.hasKeyword(keywordRule.id)) {
        assessment.keywords.ids.push(keywordRule.id);
        assessment.keywords.points += WEIGHTS[keywordRule.score];
        updateResults(keywordRule.score);
      }

      return this;
    },

    /**
     * Returns whether the assessment has matched and collected
     * the given keyword id.
     * @param   {String}  id The keyword ID
     * @returns {Boolean}
     */
    hasKeyword(id) {
      return 0 <= assessment.keywords.ids.indexOf(id);
    },

    /**
     * Adds a searcher to the assessment when the evaluation detects it.
     */
    addSearcher(searcherRule) {
      if (!this.hasSearcher(searcherRule.id)) {
        assessment.searchers.ids.push(searcherRule.id);
        assessment.searchers.points += WEIGHTS[searcherRule.score];
        updateResults(searcherRule.score);
      }

      return this;
    },

    /**
     * Returns whether the assessment has matched and collected
     * the given custom searcher id.
     * @param   {String}  id The custom searcher ID
     * @returns {Boolean}
     */
    hasSearcher(id) {
      return 0 <= assessment.searchers.ids.indexOf(id);
    },

    has(searcher) {
      const { type } = searcher;
      const { id } = searcher;

      switch (type) {
        case 'custom':
          return this.hasSearcher(id);
        case 'keyword':
          return this.hasKeyword(id);
        case 'pattern':
          return this.hasPattern(id);

        default:
          throw Error('Unrecognized searcher type');
      }
    },

    add(searcher, impact) {
      // TODO: MH|Just update the add* methods to take 2 params
      if (!searcher.score) {
        searcher = _.merge({}, searcher, { score: impact });
      }

      switch (searcher.type) {
        case 'custom':
          return this.addSearcher(searcher);
        case 'keyword':
          return this.addKeyword(searcher);
        case 'pattern':
          return this.addPattern(searcher);

        default:
          throw Error('Unrecognized searcher type');
      }
    },

    /**
     * Setting confidence level is the responsibility of the evaluator that is
     * making the assessment, since those rules will vary per exploit type.
     */
    confident(value) {
      if (![undefined, true, false].includes(value)) {
        logger.warn('value must be either `undefined`, `true, or `false`');
        return false;
      }

      return value === undefined
        ? assessment.confident
        : (assessment.confident = value);
    },

    /**
     * Returns the scoring results of the assessment data.
     */
    results: function(results) {
      return {
        score: results.score,
        points: results.points
      };
    }.bind(null, assessment.results),

    /** This may be costly, but it is to support data integrity.
     *  Returns the full set of assessment data.
     */
    details() {
      /* clone - we don't want the assessment data to be modified  */
      /* outside of scope of this module.                          */
      return _.cloneDeep(assessment);
    }
  };
}
