/**
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 moduleHook = require('../../hooks/require');
const patcher = require('../../hooks/patcher');
const { PATCH_TYPES } = require('../../constants');
const agentEmitter = require('../../agent-emitter');
const utils = require('./utils');

const constants = {
  LIFECYCLE_METHODS: [
    'onRequest',
    'onPreAuth',
    'onPostAuth',
    'onPreHandler',
    'onPostHandler',
    'onPreResponse'
  ],
  RESTIFY_SEMVER_RANGE: '>=8.0.0 <9.0.0',
  EVENTS: {
    RESTIFY_EXPORT: 'restify-export', // used to patch methods on the restify framework
    RESPONSE: 'restify-response-export', // used to patch methods on the restify/lib/response.js export
    CREATE_SERVER: 'restify-create-server', // use to patch methods on a given restify instance
    SEND: {
      PRE: 'restify-response-send-pre', // used to handle response.__send before the actual method gets called
      POST: 'restify-response-send-post' // used to handle response.__send after the actual method gets called
    }
  }
};

class RestifyCore {
  /**
   * @param {Boolean} [instrument=true] used for testing only
   */
  constructor(instrument = true) {
    if (instrument) {
      this.instrumentRestify();
      this.instrumentRestifyResponse();
    }
  }

  /**
   * Sets up handler to patch createServer when restify is required
   */
  instrumentRestify() {
    moduleHook.resolve(
      { name: 'restify', version: constants.RESTIFY_SEMVER_RANGE },
      this.registerServerHandler.bind(this)
    );
  }

  /**
   * Patches `createServer` for other instrumentation to patch methods
   * on a restify instance, and emits `RESTIFY_EXPORT` so other instrumentation
   * can patch methods no the restify export
   *
   * @param {Object} restify restify export
   */
  registerServerHandler(restify) {
    agentEmitter.emit(constants.EVENTS.RESTIFY_EXPORT, restify);
    patcher.patch(restify, 'createServer', {
      name: 'restify',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post(data) {
        const server = data.result;
        agentEmitter.emit(constants.EVENTS.CREATE_SERVER, server);
      }
    });
  }

  /**
   * Sets up handler to patch Response in restify
   */
  instrumentRestifyResponse() {
    moduleHook.resolve(
      {
        name: 'restify',
        file: 'lib/response.js',
        version: constants.RESTIFY_SEMVER_RANGE
      },
      this.registerResponseHandler.bind(this)
    );
  }

  /**
   * Wraps the patch method in `restify/lib/response`
   * to properly patch `Response`
   *
   * @param {Function} xport patch method in lib/response
   * @return {Function} returns a wrapped function so we can properly
   * get access to the `Response`
   */
  registerResponseHandler(xport) {
    const self = this;

    /**
     * This is done because restify decorates the response with
     * some of its own methods
     * @param {ServerResponse}
     * @return {Function}
     */
    return function contrastPatch(...patchArgs) {
      const [Response] = patchArgs;
      const result = Reflect.apply(xport, null, patchArgs);
      self.registerSendHandler(Response);
      agentEmitter.emit(constants.EVENTS.RESPONSE, Response);
      return result;
    };
  }

  /**
   * Patches __send to emit pre/post events for assess and protect
   * instrumentation
   *
   * @param {ServerResponse} Response restify's patched `Response`
   */
  registerSendHandler(Response) {
    patcher.patch(Response.prototype, '__send', {
      name: 'Response.prototype',
      patchType: PATCH_TYPES.SINK,
      alwaysRun: true,
      pre(data) {
        agentEmitter.emit(constants.EVENTS.SEND.PRE, data);
      },
      post(data) {
        agentEmitter.emit(constants.EVENTS.SEND.POST, data);
      }
    });
  }
}

module.exports = RestifyCore;
module.exports.constants = constants;
module.exports.utils = utils;
