/**
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 util = require('util');
const tracker = require('../../tracker');
const stackFactory = require('../../core/stacktrace').singleton;
const { PROXY_TARGET } = require('../../../lib/constants');
const TagRange = require('../models/tag-range');
const distringuish = require('@contrast/distringuish');
const agent = require('../../agent');

/**
 * Holds information about the call context of a function
 */
class CallContext {
  constructor({ obj = {}, args = [], result }) {
    this.obj = obj;
    this.args = args;
    this.result = result;
  }

  set result(result) {
    this.__result = valueString(result);
    this.resultTracked = isTracked(result);
  }
  get result() {
    return this.__result;
  }

  set args(args) {
    this.__args = args.map(valueString);
    this.argsTracked = args.map((arg) => isTracked(arg));
    this.hasArgsTracked = args.map((arg) => hasTrackedArg(arg));
    this.argsDisplayRanges = args.map((arg) => getDisplayRange(arg));
  }

  get args() {
    return this.__args;
  }

  set obj(obj) {
    this.__obj = valueString(obj);
    this.objTracked = isTracked(obj);
  }
  get obj() {
    return this.__obj;
  }
}

function isTracked(str) {
  if (tracker.getData(str)) {
    return true;
  }
  return !!(str && typeof str === 'object' && str[PROXY_TARGET]);
}

function hasTrackedArg(arg, iteration = 0) {
  if (tracker.getData(arg)) {
    return true;
  }

  if (arg && typeof arg === 'object') {

    for (const key in arg) {
      if (tracker.getData(arg[key])) {
        return true;
      }
      if (arg[key] && typeof arg[key] === 'object' && iteration < 100) {
        return hasTrackedArg(arg[key], iteration += 1);
      }
    }
  }
  return false;
}

// eslint-disable-next-line complexity
function getDisplayRange(arg, orgArg = arg, iteration = 0) {
  if (tracker.getData(arg)) {
    return new TagRange(0, arg.length - 1, 'untrusted');
  }

  if (arg && typeof arg === 'object') {
    for (const key in arg) {
      if (arg[key] && typeof arg[key] === 'object' && iteration < 5) {
        const nestedDisplayRange = getDisplayRange(arg[key], orgArg, iteration += 1);
        if (!_.isEmpty(nestedDisplayRange)) {
          return nestedDisplayRange;
        }
      }
      const trackedData = tracker.getData(arg[key]);
      if (trackedData && trackedData.tagRanges.length > 0) {
        const { start, stop } = trackedData.tagRanges[0];
        const offset = Array.isArray(orgArg) ? 2 : 0;
        const taintedString = arg[key].substring(start, stop + 1);
        const taintRangeStart = valueString(orgArg).indexOf(taintedString) + offset;
        if (taintRangeStart === -1) {
          // If tracked string is not in the abbreviated stringified obj, disable highlighting
          return new TagRange(0, 0, 'disable-highlighting');
        }
        return new TagRange(taintRangeStart, taintRangeStart + taintedString.length - 1, 'untrusted');
      }
    }
  }
  return {};
}

/**
 * Returns either the value of a string passed into it, or the constructor
 * name for an object passed into it.
 * @static
 * @param {*} value
 * @return {string}
 */
function valueString(value) {
  if (_.isString(value)) {
    if (distringuish.isExternal(value.valueOf())) {
      return distringuish.internalize(value.valueOf());
    }
    return value.valueOf();
  }

  if (_.isNumber(value)) {
    return value.toString();
  }

  const type = _.get(value, 'constructor.name', 'null');

  if ((type === 'Object' || type === 'Array') && value) {
    // make string representation uniform with no new lines and consistent spaces
    let str = util
      .inspect(value)
      .replace(/(\n|\s+)/g, (match) => (match === '\n' ? '' : ' '));
    if (str.length > 50) {
      str = str.substring(0, 50);
      str += '...';
    }

    return str;
  }
  return type;
}

const appendStack = agent.config.assess.enable_lazy_stacktraces ?
    (instance, snapshot) => {
      let stack;
      // This enables the lazy generation of a stacktrace which is a performance
      // improvement, but the snapshot method call is in a closure which creates
      // a memory leak in longer running tests
      Object.defineProperty(instance, 'stack', {
        enumerable: true,
        configurable: true,
        get() {
          if (!stack) {
            stack = snapshot();
          }
          return stack;
        },
        set(value) {
          stack = value;
        }
      });
    } : (instance, snapshot) => {
      instance.stack = snapshot();
    };

function create (params) {
  const instance = new CallContext(params);
  const snapshot = params.stacktrace || stackFactory.createSnapshot({
    constructorOpt: params.hooked
  });

  appendStack(instance, snapshot);

  return instance;
}

module.exports = {
  create,
  valueString,
  hasTrackedArg,
  isTracked,
  getDisplayRange
};
