/*
 * (c) Copyright IBM Corp. 2021
 * (c) Copyright Instana Inc. and contributors 2018
 */

/* eslint-disable max-len */

'use strict';

const { inspect } = require('util');
const shimmer = require('../../shimmer');

const hook = require('../../../util/hook');
const tracingUtil = require('../../tracingUtil');
const constants = require('../../constants');
const cls = require('../../cls');

let isActive = false;

exports.init = function init() {
  // TODO: Fix the issue with Pino instrumentation. If Pino is required multiple times,
  //       only the first instance gets instrumented. This behavior is caused by `onFileLoad`.
  //       Fix is being tracked in https://jsw.ibm.com/browse/INSTA-23066.
  hook.onFileLoad(/\/pino\/lib\/tools\.js/, instrumentPinoTools);
};

function instrumentPinoTools(toolsModule) {
  shimmer.wrap(toolsModule, 'genLog', shimGenLog);
}

function shimGenLog(originalGenLog) {
  return function (level) {
    // pino uses numerical log levels, 40 is 'warn', level increases with severity.
    if (!level || level < 40) {
      // we are not interested in anything below warn
      return originalGenLog.apply(this, arguments);
    } else {
      const originalLoggingFunction = originalGenLog.apply(this, arguments);

      return function log(mergingObject, message) {
        if (cls.skipExitTracing({ isActive, skipAllowRootExitSpanPresence: true })) {
          return originalLoggingFunction.apply(this, arguments);
        }

        const originalArgs = new Array(arguments.length);

        for (let i = 0; i < arguments.length; i++) {
          originalArgs[i] = arguments[i];
        }

        const ctx = this;
        return cls.ns.runAndReturn(() => {
          const span = cls.startSpan({
            spanName: 'log.pino',
            kind: constants.EXIT
          });
          span.stack = tracingUtil.getStackTrace(log);

          if (typeof mergingObject === 'string') {
            // calls like logger.error('only a message')
            message = mergingObject;
          } else if (mergingObject && typeof mergingObject.message === 'string' && typeof message === 'string') {
            // calls like
            // logger.error({ message: 'a message in the merging object'}, 'an additional  message as a string')
            message = `${mergingObject.message} -- ${message}`;
          } else if (mergingObject && typeof mergingObject.message === 'string') {
            // calls like
            // logger.error({ message: 'a message in the merging object'}) or
            // logger.error({ message: 'a message in the merging object: %s'}, /* non-string interpolation param */)
            message = mergingObject.message;
          } else if (typeof message === 'string') {
            // calls like
            // logger.error({ /* merging object without message attribute */ }, 'a string message)
            // Nothing to do, just use the given message (second argument) and ignore the first argument, which
            // apparently has no message attribute
          } else if (mergingObject != null) {
            // Fallback for calls with an unknown shape, like:
            // logger.error({ /* merging object without message attribute */ })
            // Serialize the first argument, but only the first level, and also shorten it:
            message = inspect(mergingObject, { depth: 1 }).substring(0, 500);
          } else {
            // If it is neither of those call patterns, we give up and do not capture a message.
            message = 'Pino log call without message and mergingObject.';
          }

          span.data.log = {
            message
          };

          if (level >= 50) {
            span.ec = 1;
          }

          try {
            return originalLoggingFunction.apply(ctx, originalArgs);
          } finally {
            span.d = Date.now() - span.ts;
            span.transmit();
          }
        });
      };
    }
  };
}

exports.activate = function activate() {
  isActive = true;
};

exports.deactivate = function deactivate() {
  isActive = false;
};
