/**
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 agentEmitter = require('../../agent-emitter');
const AsyncStorage = require('../async-storage');
const Scopes = require('../async-storage/scopes');
const logger = require('.')('contrast:req-perf-logger');
const process = require('process');

/**
 * Class for tracking data about duration of patcher
 * events during a request.  One EventInfo per
 * patched function
 */
class EventInfo {
  constructor(name) {
    this.name = name;
    this.count = 0;
    this.times = {
      pre: 0,
      post: 0,
      orig: 0
    };
  }

  /**
   * increment number of times this event occurred during a request
   */
  increment() {
    this.count++;
  }

  /**
   * store the duration of a pre event
   *
   * @param {number} duration in nanoseconds
   */
  recordPreTime(duration) {
    this._recordTime('pre', duration);
  }

  /**
   * store the duration of a post event
   *
   * @param {number} duration in nanoseconds
   */
  recordPostTime(duration) {
    this._recordTime('post', duration);
  }

  /**
   * store the duration of an original function call
   *
   * @param {number} duration in nanoseconds
   */
  recordOrigTime(duration) {
    this._recordTime('orig', duration);
  }

  /**
   * helper function for storing a duration
   *
   * @param {string} type the type of event (pre, post, orig)
   * @param {number} duration in nanoseconds
   */
  _recordTime(type, duration) {
    this.times[type] += duration;
  }
}

/**
 * Class for tracking patched function calls for a request
 */
class RequestStats {
  constructor(req) {
    this.path = new URL(req.url, `http://${req.headers.host}`).pathname;
    this.method = req.method;
    this.events = {};
    this.instrumentationTime = 0;
    this.startTime = process.hrtime.bigint();
  }

  /**
   * Log that the given function was called during the request
   *
   * @param {string} eventName the function eventName
   */
  logEvent(eventName) {
    if (!this.events[eventName]) {
      this.events[eventName] = new EventInfo(eventName);
    }
    this.events[eventName].increment();
  }

  /**
   * Record the duration of a pre call for a given function
   *
   * @param {string} eventName the function eventName
   * @param {number} duration in nanoseconds
   */
  pre(eventName, duration) {
    this.events[eventName].recordPreTime(duration);
    this.instrumentationTime += duration;
  }

  /**
   * Record the duration of a post call for a given function
   *
   * @param {string} eventName the function eventName
   * @param {number} duration in nanoseconds
   */
  post(eventName, duration) {
    this.events[eventName].recordPostTime(duration);
    this.instrumentationTime += duration;
  }

  /**
   * Record the duration of an original call for a given function
   *
   * @param {string} eventName the function eventName
   * @param {number} duration in nanoseconds
   */
  orig(eventName, duration) {
    this.events[eventName].recordOrigTime(duration);
  }

  /**
   * Log the data for the given request
   */
  finish() {
    const requestLogEntry = {
      path: this.path,
      method: this.method,
      instrumentationTime: this.instrumentationTime,
      totalTime: Number(process.hrtime.bigint() - this.startTime),
      events: Object.values(this.events)
    };

    logger.debug(JSON.stringify(requestLogEntry));
  }
}

const STORAGE_KEY = 'REQ_PERF_DATA';

module.exports = (agent) => {
  const enabled =
    agent.config &&
    agent.config.agent.node.req_perf_logging &&
    process.hrtime.bigint;
  if (enabled) {
    agentEmitter.on('http.requestStart', (req) => {
      AsyncStorage.set(STORAGE_KEY, new RequestStats(req));
    });

    agentEmitter.on('http.responseFinished', (req) => {
      const reqStats = AsyncStorage.get(STORAGE_KEY);
      if (reqStats) {
        Scopes.runInNoInstrumentationScope(() => {
          reqStats.finish();
        });
      }
    });
  }

  /**
   * Log the duration for an event and store it in async storage
   *
   * @param type {string} the type of event (pre, post, orig)
   * @param eventName {string} the name of the function event
   * @param duration {number} duration in nanoseconds
   */
  function record(type, eventName, duration) {
    if (!enabled) {
      return;
    }
    const reqStats = AsyncStorage.get(STORAGE_KEY);
    if (!reqStats) {
      return;
    }
    reqStats[type](eventName, duration);
  }

  return {
    /**
     * Report a function event
     *
     * @param {string} the function name
     */
    logEvent: (eventName) => {
      if (!enabled) {
        return;
      }
      const reqStats = AsyncStorage.get(STORAGE_KEY);
      if (!reqStats) {
        return;
      }
      reqStats.logEvent(eventName);
    },

    /**
     * Report a post event duration
     *
     * @param {string} eventName the function name
     * @param {number} duration in nanoseconds
     */
    post: (eventName, duration) => {
      record('post', eventName, duration);
    },
    /**
     * Report a pre event duration
     *
     * @param {string} eventName the function name
     * @param {number} duration in nanoseconds
     */
    pre: (eventName, duration) => {
      record('pre', eventName, duration);
    },
    /**
     * Report a orig event duration
     *
     * @param {string} eventName the function name
     * @param {number} duration in nanoseconds
     */
    orig: (eventName, duration) => {
      record('orig', eventName, duration);
    }
  };
};
