/**
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:signature');
const Assessment = require('../assessment');
const {
  REQUIRED_SIGNATURE_KEYS,
  IMPORTANCE,
  SCORES,
  THRESHOLDS,
  WEIGHTS
} = require('../common');

class SignatureEvaluator {
  constructor(signature = {}) {
    this.breakOnThreshold = true;
    this.haltingScore = signature.definiteThreshold;

    this.validate(signature);
    this.signature = signature;
  }

  validate(signature) {
    const badKeys = REQUIRED_SIGNATURE_KEYS.filter(
      (key) => !signature.hasOwnProperty(key)
    );

    if (badKeys.length) {
      logger.warn(
        'Invalid signature, missing the following properties: %s',
        badKeys.join(', ')
      );
    }
  }

  /**
   *
   * @param {} userInput
   * @param {} assessment
   * @returns {}
   */
  evaluate(userInput, assessment) {
    assessment = assessment || Assessment();

    if (!this.meetsEvalCriteria(userInput.value)) {
      return assessment.details();
    }

    const options = { thresholdMet: false, stringLower: '' };

    const searchers = [
      ...this.signature.keywordSearchers,
      ...this.signature.patternSearchers,
      ...this.signature.customSearchers
    ];

    this.evaluateSearchers(searchers, assessment, userInput, options);

    const details = assessment.details();

    this.setImportance(assessment, details.results);

    return details;
  }

  evaluateSearchers(searchers, assessment, userInput, options) {
    const method = this.breakOnThreshold ? 'some' : 'forEach';

    searchers[method]((searcher) => {
      // Don't run if we've matched already
      if (assessment.has(searcher)) {
        return false;
      }

      // Word score ( NONE | LOW | MED | HIGH | CRITICAL )
      const impact = searcher.getImpact(userInput);

      if (impact != SCORES.NONE) {
        assessment.add(searcher, impact);
        if (this.isHaltingImpact(impact)) {
          assessment.confident(true);
        }
      }

      return (
        this.breakOnThreshold &&
        assessment.confident() &&
        this.signature.definiteThreshold <= assessment.results().points
      );
    });

    return this;
  }

  /**
   * Given a score, will return whether it is significant enough to
   * halt the current rule's evaluation. Set by the signature `definiteThreshold` value.
   * @param   {String}  score The score to check for halting ability.
   * @returns {Boolean}
   */
  isHaltingImpact(score) {
    return this.haltingScore <= WEIGHTS[score];
  }

  /**
   * Calculates the importance from the assessment and sets the value on
   * the result set given.
   * @param {Object} assessment Used for calulation.
   * @param {Object} results    The value on which to mark calculated value.
   */
  setImportance(assessment, results) {
    const meets = this.meetsThreshold.bind(this, assessment);
    const importance = assessment.confident()
      ? IMPORTANCE.DEFINITE
      : meets(THRESHOLDS.DEFINITE)
      ? IMPORTANCE.DEFINITE
      : meets(THRESHOLDS.WATCH)
      ? IMPORTANCE.WORTH_WATCHING
      : IMPORTANCE.NONE;

    results.importance = importance;

    // The purpose of this method is to only to update @results.
    // We'll return `this`, so we can chain.
    return this;
  }

  /**
   * Returns true if the assessment has the same threshold as the one supplied.
   * If none is given, will compute and return the threshold met by the assessment.
   * @param   {Object}            assessment  The assessment to examine
   * @param   {THRESHOLD}         [threshold] Check if the assessment meets this.
   * @returns {Boolean|THRESHOLD}
   */
  meetsThreshold(assessment, threshold) {
    const { points } = assessment.results();

    if (threshold) {
      // return Boolean
      return threshold === THRESHOLDS.DEFINITE
        ? this.signature.definiteThreshold <= points
        : threshold === THRESHOLDS.WATCH
        ? this.signature.watchThreshold <= points
        : false;
    } else {
      // return THRESHOLD
      return this.signature.definiteThreshold <= points
        ? THRESHOLDS.DEFINITE
        : this.signature.definiteThreshold <= points
        ? THRESHOLDS.WATCH
        : null;
    }
  }

  meetsEvalCriteria(string) {
    if (!string) {
      return false;
    }

    const chars = this.signature.exploitChars;

    const length = this.signature.minimumInputLength;
    const hasChars = !_.isEmpty(chars)
      ? _.some(chars, (c) => string.includes(c))
      : true;

    const longEnough = (length || 0) <= string.length ? true : false;

    return hasChars && longEnough;
  }
}

module.exports = SignatureEvaluator;
