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

const customMappings = {
  'ContrastMethods.__contrastEval'(event) {
    _.set(event, 'context.obj', '');
    const args = _.get(event, 'context.args[0]');

    event.code = `eval(${args})`;
  },
  'ContrastMethods.__contrastTag'(event) {
    const tempStr = _.get(event, 'context.obj');
    _.set(event, 'context.obj', '');
    event.code = `const str = \`${tempStr}\``;
  },
  'ContrastMethods.__add'(event) {
    _.set(event, 'context.obj', '');
    const left = _.get(event, 'context.args[0]');
    const right = _.get(event, 'context.args[1]');

    event.code = `const str = '${left}' + '${right}'`;
  },
  response(event) {
    const res = _.get(event, 'context.args[0]');
    event.code = `return h.response("${res}")`;
  },
  'ctx.body'(event) {
    const bodyStr = _.get(event, 'context.args[0]');
    event.code = `ctx.body = "${bodyStr}"`;
  }
};

let counter = 0;

class BaseEvent {
  /**
   * Represents a dataflow event. Constructor is given the bare minimum
   * of information it needs and events are not fully rendered until
   * a finding is triggered.
   * @param {CallContext} context
   * @param {Signature} signature
   * @param {TagRange[]} tags
   */
  constructor(context, signature, tagRanges = []) {
    this.context = context;
    this.signature = signature;
    this.tagRanges = tagRanges;
    this.time = new Date().getTime();

    this.parents = [];
    this.counter = counter++;

    if (!this.signature) {
      const e = new Error();
      logger.warn(
        `dataFlowEvent created without signature.  stack: ${e.stack}`
      );
    }
  }

  /**
   * traverse the event graph. parents are the events attached to
   * an event's sources. note that 'parents' may be misleading in the
   * context of this traversal; usually, when traversing a tree, we'd call them children.
   */
  getAllParents(set = new Set()) {
    this.parents = sortEvents(this.parents);

    try {
      this.parents.forEach((p) => {
        if (p && !set.has(p)) {
          set = p.getAllParents(set);
          set.add(p);
        }
      });
    } catch (err) {
      logger.warn(
        'Unable to get all parents for dataflowEvent',
        err
      );
    }

    return set;
  }

  /**
   * Handle custom event mapping for rewritten functions and sources
   */
  mapCustomMethods() {
    const methodName = _.get(this, 'signature.methodName');
    const mapper = customMappings[methodName];
    if (!mapper) {
      return;
    }

    mapper.call(this, this);
  }

  expand() {
    if (this._expanded) {
      return;
    }
    this._expanded = true;
    this.thread = process.pid;
    if (this.source === 'P') {
      const numArgs = this.context.hasArgsTracked.length;
      if (numArgs === 1) {
        this.source = 'P0';
      } else {
        this.source = '';
        for (let i = 0; i < numArgs; i++) {
          if (this.context.hasArgsTracked[i]) {
            this.source += `P${i},`;
          }
        }
      }
    }

    if (this.signature) {
      this.signature.format(this.context.args, this.context.result);
    }

    // weird special case---if the object's constructor was just object,
    // just fall back to reporting the module itself
    if (this.context.obj === 'Object' && this.signature) {
      this.context.obj = this.signature.class;
    }
    this.mapCustomMethods();
    this._serialized = true;
  }

  /**
   * Reset the events counter. Useful for testing.
   */
  static resetCounter() {
    counter = 0;
  }
}

/**
 * When events are displayed in TS, they're displayed linearly, so this is an
 * attempt to put them in an order which makes sense to users.
 * Events are sorted based on two critera: time and number of parents.
 * Neither criterion is weighted above the other; where they 'cancel out',
 * the two events being compared will remain in the same order with respect
 * to each other.
 *
 * Time is an obvious candidate, but we also factor in number of parents because
 * this leaves us with something closer to our graph representation; nodes that are fewer
 * edges away from the sink, are closer to the sink.
 *
 * An unsorted list might look like:
 * [Source0]
 * [Source1]
 * [Prop0]
 * [Prop1]
 * [SinkEvent] (parents: Source0, Prop1)
 *
 * Sorting by parent length gives us:
 * [Source1]
 * [Prop0]
 * [Prop1]
 * [Source0]
 * [SinkEvent] (parents: Source0, Prop1)
 *
 * @param {BaseEvent[]} events list of events to sort
 * @return {BaseEvent[]} sorted list of events
 */
function sortEvents(events) {
  events.sort((a, b) => {
    let parentsfactor = 0;
    if (a.parents.length > b.parents.length) {
      parentsfactor = -1;
    } else if (b.parents.length > a.parents.length) {
      parentsfactor = 1;
    }

    let timefactor = 0;
    if (a.time < b.time) {
      timefactor = -1;
    } else if (b.time < a.time) {
      timefactor = 1;
    }

    return parentsfactor + timefactor;
  });

  return events;
}

module.exports = BaseEvent;
