/**
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 logger = require('../../core/logger')('hooks:fastify');
const utils = require('./utils');
const {
  prototype: { decorateRequest },
  setFrameworkRequest
} = require('../../hooks/frameworks/base');
const FastifyRequest = require('../../reporter/models/frameworks/fastify-request');
const Stream = require('stream');

const constants = {
  CONTRAST_HOOKS: ['onRequest', 'preValidation', 'onSend'],
  EVENTS: {
    FASTIFY_EXPORT: 'fastify-export', // used to patch methods on the fastify framework
    PRE_VALIDATION: 'fastify-prevalidation-hook', // used to implement fastify `preValidation` hook
    ON_SEND: 'fastify-onsend-hook', // used to implement fastify `onSend` hook
    ON_ROUTE: 'fastify-onroute-hook', // used to implement fastify `onRoute` hook
  }
};

class FastifyCore {
  constructor() {
    this.instrumentFastify();
  }

  /**
   * Sets up handler to patch createServer when fastify is required
   */
  instrumentFastify() {
    moduleHook.resolve(
      { name: 'fastify' },
      this.registerFastifyHandler.bind(this)
    );
  }

  /**
   * Patches `createServer` for other instrumentation to patch methods
   * on a fastify instance, and emits `FASTIFY_EXPORT` so other instrumentation
   * can patch methods on the fastify export
   *
   * @param {Object} xport fastify export
   */
  registerFastifyHandler(xport) {
    const self = this;
    logger.info('Hooking fastify build');
    return patcher.patch(xport, {
      name: 'fastify.build',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post(data) {
        const fastify = data.result;
        // install fastify lifecycle hooks. Order:
        // 1. onRequest
        // 2. preParsing
        // 3. preValidation
        // 4. preHandler
        // 5. onSend
        // 6. onResponse (does not offer a payload to inspect and modify. Is fired _after_ sending)
        // if for some reason a new hook needs to be added, add it to the list of importantHooks
        // below.
        fastify.addHook('onRequest', FastifyCore.onRequest.bind(self));
        fastify.addHook(
          'preValidation',
          FastifyCore.onPreValidation.bind(self)
        );
        fastify.addHook('onSend', FastifyCore.onSend.bind(self));
        self.registerContrastHooksHandler(
          utils.getHidden(fastify, 'fastify.hooks')
        );
        agentEmitter.emit(constants.EVENTS.FASTIFY_EXPORT, fastify);
        fastify.addHook('onRoute', FastifyCore.onRoute.bind(self));
      }
    });
  }

  /**
   * make sure that our hooks are the last that run in the fastify hook lifecycle.
   * This assumes that we are already in the hook stack.
   * also makes the assumption that the hook did not throw.
   * @param {fastify.Hooks} hooks instance of hooks stored on fastify
   */
  registerContrastHooksHandler(hooks) {
    patcher.patch(hooks, 'add', {
      name: 'fastify.Hooks',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post(data) {
        // we only care about the following hooks as these are what we use
        const {
          args: [hook],
          obj: self
        } = data;

        if (constants.CONTRAST_HOOKS.includes(hook) && self[hook].length > 1) {
          const len = self[hook].length;
          const top = self[hook][len - 1];
          self[hook][len - 1] = self[hook][len - 2];
          self[hook][len - 2] = top;
        }
      }
    });
  }

  /**
   * Fastify lifecycle hook as defined in official docs.
   * @param {Fastify.routeOptions} routeOptions properties of a given route
   */
  static onRoute(routeOptions) {
    agentEmitter.emit(constants.EVENTS.ON_ROUTE, routeOptions);
  }

  /**
   * Fastify lifecycle hook as defined in official docs.
   * @external https://github.com/fastify/fastify/blob/master/docs/Hooks.md#onrequest
   * @external https://github.com/fastify/fastify/blob/master/docs/Request.md
   * @param {Fastify.Request} request incoming request
   * @param {Fastify.Reply}   reply   unbuilt outgoing response
   * @param {Function}        done    callback to signal the hook is finished.
   */
  static onRequest(request, reply, done) {
    // Set the framework-specific request model in our first hook point
    setFrameworkRequest(request, FastifyRequest);
    done();
  }

  /**
   * Fastify lifecycle hook as defined in official docs.
   * @external https://github.com/fastify/fastify/blob/master/docs/Hooks.md#prevalidation
   * @external https://github.com/fastify/fastify/blob/master/docs/Request.md
   * @param {Fastify.Request} request incoming request
   * @param {Fastify.Reply}   reply   unbuilt outgoing response
   * @param {Function}        done    callback to signal the hook is finished.
   */
  static onPreValidation(...data /*request, reply, done*/) {
    const [request, , done] = data;
    agentEmitter.emit(constants.EVENTS.PRE_VALIDATION, ...data);
    decorateRequest({
      body: request.body,
      parameters: request.params,
      query: request.query
    });
    done();
  }

  /**
   * Fastify lifecycle hook as defined in:
   * @external https://github.com/fastify/fastify/blob/master/docs/Hooks.md#onsend
   * @external https://github.com/fastify/fastify/blob/master/docs/Reply.md
   * @param {Fastify.Request} request incoming request
   * @param {Fastify.Reply}   reply   built outgoing response
   * @param {*}               payload the response body
   * @param {Function}        done    callback to signal the hook is finished.
   */
  static onSend(...data /* request, reply, payload, done */) {
    const [, , payload, done] = data;
    // because these hooks run in customer code, we want to be sure that even if
    // we can't emit source events, we still hit that `done()` call.
    // payload can be of type:
    // - String
    // - Stream
    // - Buffer
    // - Error
    // - undefined
    // - null
    if (
      !(
        payload instanceof Stream ||
        payload instanceof Buffer ||
        payload instanceof Error
      )
    ) {
      agentEmitter.emit(constants.EVENTS.ON_SEND, ...data);
    }

    done(null, payload);
  }
}

module.exports = FastifyCore;
module.exports.constants = constants;
module.exports.utils = utils;
