/**
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.
*/
const { AsyncStorage, KEYS } = require('../async-storage');
const Debraner = require('../../../lib/assess/membrane/debraner');

/**
 * Diagnostic util. Logs request start, request end, and dataflow event construction
 * Does this by logger.debug, and logging a JSON.stringify of the following object type:
 * {
 *  stack: [string] - stack trace
 *  event: obj - event
 *  req: obj - request
 *  res: obj - response
 *  id: - unique (randomly generated per request) id to associate events with requests in the logs
 * }
 *
 * Request start events will in include an id and a req
 * Request end events will include an id and a res
 * Event logs will include an s, an e, and if available, an id for association with a request
 *
 * If logging fails for whatever reason, we emit an error level log.
 */
class DataflowMonitor {
  constructor() {
    /**
     * request --> id
     */
    this.ids = new WeakMap();
    /**
     * incremented to get an id for the request
     */
    this.count = 1;
  }

  event(event) {
    const p = {
      stack: new Error().stack,
      event
    };

    const req = AsyncStorage.get(KEYS.REQ);

    if (req && this.ids.has(req)) {
      p.id = this.ids.get(req);
    }

    this.log(p);
  }

  requestEnd(req, res) {
    const id = this.ids.get(req);
    if (id) {
      this.log({
        id,
        res
      });
    }
  }

  requestStart(req) {
    if (this.ids.has(req)) {
      return;
    }

    const id = this.count++;
    this.ids.set(req, id);

    this.log({
      id,
      req
    });
  }

  log(data) {
    const nestedObjects = new WeakSet();
    const debraner = new Debraner(data);
    debraner.remove(data);
    try {
      const str = JSON.stringify(data, function(key, value) {
        if (value && typeof value === 'object') {
          if (nestedObjects.has(value)) {
            return '[Circular]';
          }

          nestedObjects.add(value);
        }

        return value;
      });
      this.logger.debug(str);
      // not sure how this catch can happen since we protect against
      // circular references above
    } catch (err) {
      this.logger.debug(err);
    }
    debraner.restore();
  }

  // be lazy about creating a logger; we don't want to have one if the flag is disabled
  get logger() {
    if (!this._logger) {
      this._logger = require('../logger')('contrast:dataflow-monitor');
    }

    return this._logger;
  }
}

// attempt at getting around cache clobbering shit
const KEY = Symbol('dataflow-monitor');
global[KEY] = global[KEY] || new DataflowMonitor();
module.exports = global[KEY];
