/**
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';

/** @typedef {import('../models/StorageContext')} StorageContext */

const _ = require('lodash');
const { createNamespace, getNamespace } = require('@contrast/cls-hooked');

// Please keep these strings
// cls-hooked debug logging will fail
// when trying to log a Symbol value
const CONTRAST = 'contrastContext';
const CONTRAST_CLS = 'contrastCls';
const SCOPES = 'contrastScopes';

// must create namespace outside of attaching to avoid
// memory leaks/cpu spikes
const cls = createNamespace(CONTRAST_CLS);

/* schema of keys on StorageContext */
const KEYS = {
  ASSESS_INPUT_EXCLUSIONS: 'inputExclusions',
  ASSESS_URL_EXCLUSIONS: 'urlExclusions',
  DEFEND: 'defend',
  FASTIFY_HANDLER_RESOLVED: 'fastify.xss.handler.resolved',
  FASTIFY_REPLY_SEND_STATE: 'fastify.xss.reply.send.state',
  FINALHANDLER_CB_INDEX: 'finalHandlerCbIndex',
  SERVE_STATIC_PATH: 'serveStaticPath',
  HAPI_CALLER: 'hapi.caller',
  INPUT_EXCLUSIONS: 'defend.exclusions',
  KOA_CTX: 'koa.ctx',
  REQUEST: 'request',
  REQ: 'req',
  RES: 'res',
  RESPONSE_CONTENT_TYPE: 'response.contentType',
  ROUTE: 'route',
  RULES: 'defend.rules',
  SAMPLES: 'defend.samples',
  SOURCES: 'sources'
};

/*
 * class made up of static functions for accessing the async contexts.
 * Acts as a map of key/value pairs with the StorageContext
 * as its backend.
 *
 * In order to access methods on StorageContext, getContext()
 * must be used. For all other data, AsyncStorage.get(KEYS.*)
 * is used. The keys defined in KEYS are guaranteed to be a valid
 * path but they may be initialized as null or undefined.
 *
 */
class AsyncStorage {
  /**
   * Returns a value for a given path on the active storage context.
   * @template T
   * @param {string} path Path to data value
   * @param {object} [ctxt=active storage context]
   * @returns {T} Data value
   */
  static get(path, ctxt = AsyncStorage.getContext()) {
    if (ctxt) {
      return _.get(ctxt, path);
    }
  }

  /**
   * Sets a value for a given path on the current active storage context.
   * @template T
   * @param {string} path  The path at which to save value
   * @param {T} value The value to save at path
   * @returns {T}
   */
  static set(path, value) {
    const cls = AsyncStorage.getActive();
    if (cls) {
      const ctxt = cls.get(CONTRAST);
      _.set(ctxt, path, value);
      cls.set(CONTRAST, ctxt);
      return value;
    }
  }

  /**
   * Attaches request state to a new async context, and returns a handle to the
   * context.  Event emitter callbacks will be run within the async context.
   *
   * @param {IncomingMessage} req
   * @param {ServerResponse} res
   * @param {StorageContext} storageContext
   * @param {Object} { run: runs callback in cls and sets the request scope on CONTRAST }
   */
  static attach(req, res, storageContext) {
    cls.bindEmitter(req);
    cls.bindEmitter(res);

    return {
      /**
       * Uses to run in a cls namespace
       * aka cls-hooked(async_hooks)
       */
      run: (callback) =>
        cls.runAndReturn(() => {
          cls.set(CONTRAST, storageContext);
          return callback();
        })
    };
  }

  /**
   * Runs the callback in a proper context
   * cls.run and sets the relevant scope/function name
   */
  static run(callback, scope, functionName) {
    const cls = AsyncStorage.getNamespace();
    return cls.runAndReturn(() => {
      cls.set(SCOPES, { [scope]: functionName });
      return callback();
    });
  }

  /**
   * Bind a callback to the current active request context.
   * This is used for libraries that are not built to support
   * working within an async hook
   *
   * @param {Function} callback
   * @returns {Function} the bound function
   */
  static bind(callback) {
    const cls = AsyncStorage.getNamespace();
    return cls.bind(callback);
  }

  /**
   * Retrives the active cls context
   */
  static getActive() {
    const ns = AsyncStorage.getNamespace();
    if (ns.active) {
      return ns;
    }
  }

  /**
   * Retrieves the cls instance
   */
  static getNamespace() {
    let ns = getNamespace(CONTRAST_CLS);

    /**
     * see: https://contrast.atlassian.net/browse/NODE-771
     * adding logic to re-assign our namespace
     * if it got stomped on by user code
     */
    if (!ns) {
      process.namespaces[CONTRAST_CLS] = cls;
      ns = cls;
    }

    return ns;
  }

  /**
   * Returns the <code>StorageContext</code> instance for the active namespace.
   * @returns {StorageContext}
   */
  static getContext() {
    const cls = AsyncStorage.getActive();
    if (cls) {
      return cls.get(CONTRAST);
    }
  }

  /**
   * Returns all the active scopes from storage context
   */
  static getScope() {
    const cls = AsyncStorage.getActive();
    if (cls) {
      return cls.get(SCOPES);
    }
  }

  /**
   * Retrieves the active storage context from an error
   * context is added to error when one is thrown
   * see: https://github.com/Jeff-Lewis/cls-hooked/blob/master/context.js#L101
   *
   * Then gets a relevant key from context
   */
  static fromException(error, key) {
    const cls = AsyncStorage.getNamespace();
    const clsCtxt = cls.fromException(error);
    const ctxt = _.get(clsCtxt, CONTRAST);

    if (ctxt) {
      return AsyncStorage.get(key, ctxt);
    }
  }
}

module.exports = AsyncStorage;
module.exports.AsyncStorage = AsyncStorage;
module.exports.KEYS = KEYS;
module.exports.SCOPES = SCOPES;

module.exports.CONTRAST = CONTRAST;
global.CONTRAST = CONTRAST;
