/*
 * (c) Copyright IBM Corp. 2023
 */
// @ts-nocheck

'use strict';

const shimmer = require('shimmer');
const EventEmitter = require('events').EventEmitter;
let logger;

exports.init = function init(config) {
  logger = config.logger;
};

exports.unwrap = shimmer.unwrap;

/**
 * This wrapper around the shimmer library prevents that the application of the customer terminates when one of our
 * instrumentations throws an error _in the instrumencation code_. Errors that are produced by the application or the
 * instrumented library are re-thrown (we do not want to change the behavior of the application under monitoring).
 *
 * We won't catch errors which happened e.g. when the result of the original fn comes back.
 * This is fine, because we want to primarly catch errors which happened before the library call.
 *
 * ----------------------------------------------------------------
 *
 * Assuming the following flow for an uninstrumented application:
 *
 * application code -> original library function
 *
 * After applying instrumentation with the help of this wrapper, the flow will looks like this:
 *
 * application code -> (anonymous function in line 24 which we might want to give a name, see below) ->
 * instrumentationWrapperMethod (e.g. shimQuery from our mysql instrumentation) ->
 * instanaOriginalFunctionWrapper -> original library function.
 *
 * This serves two purposes:
 * 1. We can insert one central try-catch into the flow that is applied for all our instrumentations.
 * 2. The additional named function instanaOriginalFunctionWrapper enables us to inspect the stack trace of errors
 *    that are thrown and thus determine whether they have been thrown in our instrumentation code or in the
 *    instrumented library.
 */
exports.wrap = (origObject, origMethod, instrumentationWrapperMethod) => {
  // NOTE: We do not want to wrap a function of a library twice.
  // CASE: Some instrumentations are really complex such as redis.
  //       e.g. there is the concept of client and/or cluster connections.
  //       We might use the same underlying object, but we call `.wrap` twice in the instrumentation
  //       but we can't control it.
  // NOTE: EventEmitter can have multple wraps / listeners.
  if (
    origObject &&
    origObject[origMethod] &&
    origObject[origMethod].__wrapped &&
    !(origObject instanceof EventEmitter)
  ) {
    shimmer.unwrap(origObject, origMethod);
    logger.debug(`Method ${origMethod} of ${origObject} was already wrapped. Unwrapping.`);
  }

  shimmer.wrap(origObject, origMethod, function instanaShimmerWrap(originalFunction) {
    return function instanaShimmerWrapInner() {
      let originalCalled = false;
      const originalThis = this;
      const originalArgs = arguments;

      function instanaOriginalFunctionWrapper() {
        // NOTE: we keep this check for super safety. we are not aware of a use case atm.
        if (originalCalled) return;
        originalCalled = true;
        return originalFunction.apply(this, arguments);
      }

      try {
        return instrumentationWrapperMethod(instanaOriginalFunctionWrapper).apply(originalThis, originalArgs);
      } catch (err) {
        // CASE: not our business, error comes from the target lib
        if (err && err.stack && err.stack.indexOf('instanaOriginalFunctionWrapper') !== -1) {
          throw err;
        }

        // CASE: some libs throw an error like throw ({ message: 'this is a msg' }) which does not create a proper stack
        //       e.g. db2
        if (!err.stack) {
          throw err;
        }

        logger.warn(
          `An internal error happend in the Instana Node.js collector. Please contact support. ${err?.stack}`
        );

        if (originalCalled) return;
        originalCalled = true;

        // CASE: we pretend that there was no error in our instrumentation, we only log a warning and
        //       ensure tht the customer's application does not die.
        return originalFunction.apply(originalThis, originalArgs);
      }
    };
  });
};
