/**
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 onFinished = require('on-finished');
const agentEmitter = require('../../../agent-emitter');
const StorageContext = require('../context');
const { AsyncStorage } = require('..');

const ALLOW_ALL = 'ALLOW_ALL';
const NO_INSTRUMENTATION = 'NO_INSTRUMENTATION';
const NO_PROPAGATION = 'NO_PROPAGATION';
const SCOPE_NAMES = { ALLOW_ALL, NO_INSTRUMENTATION, NO_PROPAGATION };

class Scopes {
  /**
   * Returns the active scopes.
   * @return {Scopes}
   */
  static get() {
    return AsyncStorage.getScope() || {};
  }

  /**
   * Binds a callback, StorageContext and a request/response object to the async
   * context and initializes the http events necessary for agent functionality.
   * @param {Object} params
   * @param {IncomingMessage} params.req
   * @param {ServerResponse} params.res
   * @param {ContrastAgent} params.agent
   * @param {Function} params.callback callback to run in async scope
   * @param {Function} params.hookResponse function to hook res
   */
  static runInRequestScope({ req, res, agent, hookResponse, callback }) {
    const storageContext = this.createStorageContext(req, res);
    const clsContext = AsyncStorage.attach(req, res, storageContext);
    agentEmitter.emit('http.incomingRequest', req);
    return clsContext.run(() => {
      onFinished(res, function contrastFinished(err, res) {
        const context = AsyncStorage.getContext();
        agentEmitter.emit('http.responseFinished', req, res);
        agentEmitter.emit('activity', context);
      });
      hookResponse(res, agent);
      return callback();
    });
  }

  /**
   * Creates the storage context in a scope preventing instrumentation.
   * @param {IncomingMessage} req
   * @param {ServerResponse} res
   * @returns {StorageContext}
   */
  static createStorageContext(req, res) {
    return this.runInNoInstrumentationScope(
      () => new StorageContext(req, res),
      'StorageContextCreation'
    );
  }

  /**
   * Run a callback in the requested scope
   *
   * @param {String} scope name of the scope to run
   * @param {Function} callback the function to run
   * @param {String} functionName Name of the function
   *
   * @return {any} the return value of the callback
   *
   * NOTE: when returning a promise from this function
   * the behavior of handler functions wrt Scope can
   * be surprising.  In some circumstances handlers will
   * also be run in the Scope, whereas others they will not.
   * See:
   * https://contrast.atlassian.net/wiki/spaces/~18775625/pages/1222052407/Async+Context+when+a+Promise+is+returned+from+runInScope
   */
  static runIn(scope, callback, functionName) {
    if (!(scope in SCOPE_NAMES)) {
      return callback();
    }

    return AsyncStorage.run(callback, scope, functionName);
  }

  /**
   * Binds a callback to the AsyncStorage context and calls it within a
   * no instrumentation scope.
   * @param {Function} callback
   * @param {String} functionName Name of the function that enters NoInstrumentationScope
   * @return {any}
   */
  static runInNoInstrumentationScope(callback, functionName) {
    return Scopes.runIn(NO_INSTRUMENTATION, callback, functionName);
  }

  /**
   * Binds a callback to the AsyncStorage context and calls it within a
   * no propagation scope.
   * @param {Function} callback
   * @param {string} propagator name of propagator
   * @return {any}
   */
  static runInNoPropagationScope(callback, propagator) {
    return Scopes.runIn(NO_PROPAGATION, callback, propagator);
  }

  /**
   * Binds a callback to the AsyncStorage context and calls it within an
   * allow-all scope. This is used in cases where the agent may be deadzoned due
   * to being in another scope, but we want to run instrumentation/propagation
   * in a controlled and explicit way, as if in a "sandbox".
   * @param {Function} callback
   * @param {string} identifier Label/name provided by caller to identify reason
   * @return {any}
   */
  static runInAllowAllScope(callback, identifier) {
    return Scopes.runIn(ALLOW_ALL, callback, identifier);
  }

  /**
   * Returns true when executing within a no instrumentation scope.
   * @return {boolean}
   */
  static isNoInstrumentationScoped() {
    const scopes = Scopes.get();
    return scopes.hasOwnProperty(NO_INSTRUMENTATION);
  }

  /**
   * Returns true when executing within a no propagation scope.
   * @return {boolean}
   */
  static isNoPropagationScoped() {
    const scopes = Scopes.get();
    return scopes.hasOwnProperty(NO_PROPAGATION);
  }
}

module.exports = Scopes;
module.exports.Scopes = Scopes;
module.exports.SCOPE_NAMES = SCOPE_NAMES;
