/*
 * (c) Copyright IBM Corp. 2021
 * (c) Copyright Instana Inc. and contributors 2020
 */
// @ts-nocheck - tracing/instrumentation is out of context for now

'use strict';

const coreChildProcess = require('child_process');
const { tracing } = require('@instana/core');
const shimmer = tracing.shimmer;
const getCls = tracing.getCls;

const selfPath = require('./selfPath');

/** @type {import('@instana/core/src/core').GenericLogger} */
let logger;

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

  shimmer.wrap(coreChildProcess, 'spawn', shimSpawn);
  shimmer.wrap(coreChildProcess, 'fork', shimFork);
};

// This instruments the code that is used when edgemicro is started with the forever monitor, that is, via
// `edgemicro forever -a start`. It adds --require /path/to/@instana/collecor/src/immediate to the arguments,
// effectively adding Instana instrumentation to the processes started via forever-monitor.
//
// There is also ./edgemicro.js, which is responsible for instrumenting the code that is used to spawn the individual
// edgemicro workers.
//
// TODO: We have not yet explored whether ESM support needs to be added for EdgeMicro.
//       https://jsw.ibm.com/browse/INSTA-27880
function shimSpawn(original) {
  return function (command, args) {
    if (
      // check if a command has been specified and it is the Node.js executable
      typeof command === 'string' &&
      /[^\w]node$/.test(command) &&
      // check if arguments have been specified
      args &&
      Array.isArray(args) &&
      args.length >= 1 &&
      // check if it is edgemicro trying to spawn its main process via forever
      // (happens when the `edgemicro forever -f forever.json -a start` command is used)
      /[^\w]edgemicro[^\w]app\.js$/.test(args[0])
    ) {
      if (!selfPath.immediate) {
        logger.warn(
          "Detected a child_process.spawn of 'edgemicro/app', but the path to @instana/collector is not available, " +
            'so this edgemicro instance will not be instrumented.'
        );
      } else {
        logger.debug(
          `Detected a child_process.spawn of edgemicro/app, instrumenting it by adding --require ${selfPath.immediate}.`
        );
        args.unshift(selfPath.immediate);
        args.unshift('--require');
      }
    }

    return original.apply(this, arguments);
  };
}

const bullMasterProcessMatch = /bull\/lib\/process\/master\.js/;

// Bull is not using ESM internally yet. They fork a childProcess for "master.js" (CJS) internally here
// https://github.com/OptimalBits/bull/blob/489c6ab8466c1db122f92af3ddef12eacc54179e/lib/process/child-pool.js#L52
// Currently, we only support childProcess.fork for Bull.
// If Bull migrates from CJS to ESM, we have to change the fork implementation.
function shimFork(original) {
  return function () {
    // args: modulePath, args, options
    const _args = Array.prototype.slice.call(arguments);
    const modulePath = _args[0];
    const args = _args[1];

    if (typeof modulePath === 'string' && bullMasterProcessMatch.test(modulePath) && args && args.execArgv) {
      if (!selfPath.immediate) {
        logger.warn(
          "Detected a child_process.fork of 'Bull processor', but the path to @instana/collector is not available, " +
            'so this Bull processor instance will not be instrumented.'
        );
      } else {
        logger.debug(
          `Detected a child_process.fork of Bull, instrumenting it by adding --require ${selfPath.immediate}.`
        );

        // NOTE: master.js is forked here!
        //       https://github.com/OptimalBits/bull/blob/v4.16.5/lib/process/master.js
        process.env.INSTANA_IS_BULL_CHILD_PROCESS = 'true';
        args.execArgv.unshift(selfPath.immediate);
        args.execArgv.unshift('--require');
      }

      /** @type {import('child_process').ChildProcess} */
      const childProcess = original.apply(this, _args);

      // Retrieve the entry span created by bull.js#instrumentedProcessJob.
      const originalChildProcessSend = childProcess.send;
      childProcess.send = function (message) {
        const cls = getCls();
        let entrySpan = null;

        if (cls) {
          entrySpan = cls.getCurrentSpan();
        }

        if (
          //
          message && //
          message.cmd === 'start' &&
          message.job &&
          typeof message.job === 'object' &&
          message.job.opts &&
          entrySpan &&
          entrySpan.k === 1
        ) {
          if (message.job.opts == null) {
            message.job.opts = {};
          }

          /**
           * Because this handles the process case, we don't need to care about repeatable jobs and handle
           * them by job id.
           * The reason is that here, we treat each job independently of each other, so it's ok to define the
           * instanaTracingContext object, instead of opts[job.id] as we do for the Callback and Promise case.
           */
          if (message.job.opts.instanaTracingContext == null) {
            message.job.opts.instanaTracingContext = {};
          }
          message.job.opts.instanaTracingContext.X_INSTANA_T = entrySpan.t;
          message.job.opts.instanaTracingContext.X_INSTANA_S = entrySpan.s;
        }

        originalChildProcessSend.apply(this, arguments);
      };

      return childProcess;
    } else {
      return original.apply(this, arguments);
    }
  };
}

exports.activate = function () {
  // no-op
};

exports.deactivate = function () {
  // no-op
};
