/**
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 semver = require('semver');
const agent = require('../agent');
const process = require('process');
const sourceMap = require('../util/source-map');
const _isAgentPath = require('../util/is-agent-path');

const STACK_TRACE_LIMIT = agent.config.agent.stack_trace_limit;
const EVAL_ORIGIN_REGEX = /\((.*?):(\d+):\d+\)/;
const EVENTS_FILE = semver.gte(process.version, '16.0.0')
  ? 'node:events'
  : 'events.js';

/**
 * The factory will set stacktrace limit for stackframe lists created by it.
 * @param {number} stackTraceLimit set the stack trace limit
 * @param {function} isAgentPath function indicating if path is agent code
 */
class Factory {
  constructor({
    stackTraceLimit = STACK_TRACE_LIMIT,
    isAgentPath = _isAgentPath
  } = {}) {
    this.stackTraceLimit = stackTraceLimit;
    this.isAgentPath = isAgentPath;
  }

  /**
   * Creates a function that will build a stacktrace when invoked. It will keep
   * an error in a closure whose stack will be generated and processed when the
   * returned function executes. The result will be cached.
   * @param {object} params
   * @param {function} params.limitFn The constructorOpt param used when creating stack
   * @returns {function}
   */
  createSnapshot({ constructorOpt, prependFrames } = {}) {
    const { isAgentPath } = this;
    const target = {};

    this.appendStackGetter(target, constructorOpt);

    let ret;

    /**
     * Generates array of frames from `target`'s `stack` getter
     * @returns {array}
     */
    return function snapshot() {
      if (!ret) {
        const eventFrames = [];
        const clsFrames = [];
        const callsites = Factory.generateCallsites(target);

        // eslint-disable-next-line complexity
        ret = (callsites || []).reduce((acc, callsite) => {
          if (Factory.isCallsiteValid(callsite)) {
            const frame = Factory.makeFrame(callsite);

            if (
              frame.file &&
              !`${frame.type}${frame.method}`.includes('ContrastMethods')
            ) {
              const isAgentFrame = isAgentPath(frame.file);
              const isAgentEmitter = frame.type === 'AgentEmitter';

              // Filter out cls-hooked code called by the agent
              if (frame.file.indexOf('cls-hooked') !== -1) {
                clsFrames.push(frame);
                return acc;
              } else {
                if (isAgentFrame) {
                  clsFrames.length = 0;
                } else {
                  clsFrames.length && acc.push(...clsFrames);
                }
              }

              // Filter out AgentEmitter and any event.js code for it
              if (frame.file === EVENTS_FILE) {
                eventFrames.push(frame);
                return acc;
              } else {
                if (isAgentEmitter) {
                  eventFrames.length = 0;
                } else {
                  eventFrames.length && acc.push(...eventFrames);
                }
              }

              // Finally add any non-agent frames
              if (!isAgentFrame && !isAgentEmitter) {
                acc.push(frame);
              }
            }
          }

          return acc;
        }, []);
      }

      return prependFrames ? [...prependFrames, ...ret] : ret;
    };
  }

  /**
   * Based on stacktrace limit and constructor opt, will append a stack getter
   * @param {} error
   * @param {} limitFn
   */
  appendStackGetter(error, constructorOpt) {
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = this.stackTraceLimit;
    Error.captureStackTrace(error, constructorOpt);
    Error.stackTraceLimit = stackTraceLimit;
  }

  static isCallsiteValid(callsite) {
    return callsite instanceof Callsite;
  }

  /**
   * Creates an array frame objects from an array of Callsite instances
   * @param {} callsite
   * @returns {}
   */
  static makeFrame(callsite) {
    let evalOrigin;
    let file;
    let lineNumber;
    let columnNumber;

    if (callsite.isEval()) {
      evalOrigin = Factory.formatFileName(callsite.getEvalOrigin());

      const match = evalOrigin.match(EVAL_ORIGIN_REGEX);
      if (match) {
        [, file, lineNumber, columnNumber] = match;
      }
    }

    file = file || callsite.getFileName();
    lineNumber = lineNumber || callsite.getLineNumber();
    columnNumber = columnNumber || callsite.getColumnNumber();

    let mappedData;
    if (sourceMap.util) {
      mappedData = sourceMap.util.applyMap(file, lineNumber, columnNumber);
    }

    if (mappedData) {
      ({ lineNumber, file } = mappedData);
    }

    return {
      eval: evalOrigin,
      file,
      lineNumber,
      method: callsite.getFunctionName(),
      type: callsite.getTypeName()
    };
  }

  static formatFileName(fileName = '') {
    const cwd = `${process.cwd()}/`;
    const idx = fileName.indexOf(cwd);

    if (idx > -1) {
      return fileName.replace(cwd, ''); // + 1 to remove /
    }
    return fileName;
  }

  /**
   * Will access `stack` getter propery on the provided error to generate
   * a stacktrace. We capture the callsite instances passed to `prepareStacktrace`
   * using an ephemeral monkey patch.
   * @param {object} error object with a `stack` getter property
   * @returns {}
   */
  static generateCallsites(error) {
    let callsites;

    const { prepareStackTrace } = Error;

    Error.prepareStackTrace = function(_, _callsites) {
      callsites = _callsites;
      return _callsites;
    };

    // accessing the getter will call `Error.prepareStacktrace`
    error.stack;

    // restore original method
    Error.prepareStackTrace = prepareStackTrace;

    return callsites;
  }
}

module.exports.Factory = Factory;
module.exports.generateCallsites = Factory.generateCallsites;
module.exports.singleton = new Factory();

const Callsite = (module.exports.Callsite = Factory.generateCallsites(
  new Error()
)[0].constructor);
