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

'use strict';

let isMainThread = true;
try {
  isMainThread = require('worker_threads').isMainThread;
} catch (err) {
  // Worker threads are not available, so we know that this is the main thread.
}

const { tracing } = require('@instana/core');
const agentConnection = require('../agentConnection');
const agentOpts = require('../agent/opts');
const initializedTooLate = require('../util/initializedTooLate');
const metrics = require('../metrics');
const requestHandler = require('../agent/requestHandler');
const transmissionCycle = require('../metrics/transmissionCycle');
const uncaught = require('../uncaught');
const { isNodeVersionEOL } = require('../util/eol');

const ONE_MINUTE = 60 * 1000;
const EOL_EVENT_REFRESH_INTERVAL = 6 * 60 * ONE_MINUTE; // 6 hours
const EOL_EVENT_DURATION = 6 * 60 * ONE_MINUTE + ONE_MINUTE; // 6 hours + 1 minute
const FIRE_MONITORING_EVENT_DURATION_IN_MS = process.env.INSTANA_FIRE_MONITORING_EVENT_DURATION_IN_MS
  ? Number(process.env.INSTANA_FIRE_MONITORING_EVENT_DURATION_IN_MS)
  : 600000;

/** @type {*} */
let autoprofile;

/** @type {*} */
let profiler;

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

/** @type {import('./').AnnounceCycleContext} */
let ctx;

let tracingMetricsDelay = 1000;
if (typeof process.env.INSTANA_TRACER_METRICS_INTERVAL === 'string') {
  tracingMetricsDelay = parseInt(process.env.INSTANA_TRACER_METRICS_INTERVAL, 10);
  if (isNaN(tracingMetricsDelay) || tracingMetricsDelay <= 0) {
    tracingMetricsDelay = 1000;
  }
}

/** @type {number | null} */
let originalTracingMetricsDelay = null;

/** @type {NodeJS.Timeout} */
let tracingMetricsTimeout = null;

/** @type {{ pid: number, getEntityId: Function }} */
let pidStore;

/**
 * @type {boolean}
 */
let disableEOLEvents;

/**
 * @param {import('@instana/core/src/config').InstanaConfig} config
 * @param {any} _pidStore
 */
function init(config, _pidStore) {
  logger = config.logger;
  pidStore = _pidStore;
  disableEOLEvents = config.tracing?.disableEOLEvents;
  initializedTooLate.init(config);
  requestHandler.init(config);

  // TODO: Why is autoProfile part of agentOpts? O_o Please refactor this away in next major release.
  if (agentOpts.autoProfile) {
    try {
      // @ts-ignore - TS cannot find @instana/profile.
      // TODO: @instana/autoprofile is not linted or typed
      // eslint-disable-next-line import/no-extraneous-dependencies
      autoprofile = require('@instana/autoprofile');
    } catch (e) {
      logger.info(
        'Could not load @instana/autoprofile. You will not get profiling information for this Node.js app in Instana,' +
          'although autoprofiling has been enabled. This typically occurs when native addons could not be built ' +
          'during module installation (npm install/yarn) or when npm install --no-optional or yarn --ignore-optional ' +
          'have been used to install dependencies. See the instructions to learn more about the requirements of the ' +
          'collector: ' +
          'https://www.ibm.com/docs/en/instana-observability/current?topic=nodejs-collector-installation#native-add-ons'
      );

      fireMonitoringEvent();
      setInterval(fireMonitoringEvent, FIRE_MONITORING_EVENT_DURATION_IN_MS).unref();
    }
  }
}

/**
 * @param {import('./').AnnounceCycleContext} _ctx
 */
function enter(_ctx) {
  ctx = _ctx;

  initializedTooLate.check();

  logger.debug(`isMainThread: ${isMainThread}`);

  if (isMainThread) {
    uncaught.activate();
    metrics.activate();
    requestHandler.activate();
    transmissionCycle.activate(
      metrics,
      agentConnection,
      /**
       * @param {Array.<import('../agent/requestHandler').AnnounceRequest>} requests
       */
      function onSuccess(requests) {
        requestHandler.handleRequests(requests);
      },
      function onError() {
        ctx.transitionTo('unannounced');
      }
    );
    scheduleTracingMetrics();
    if (!disableEOLEvents) {
      detectEOLNodeVersion();
    }
  }

  tracing.activate(agentOpts.config);

  if (agentOpts.autoProfile && autoprofile) {
    profiler = autoprofile.start();
    /**
     * @param {*} profiles
     * @param {(...args: *) => *} callback
     */
    profiler.sendProfiles = (profiles, callback) => {
      agentConnection.sendProfiles(profiles, callback);
    };
    profiler.getExternalPid = () => pidStore.pid;
    profiler.getLogger = () => logger;
    profiler.start();
  }

  logger.info('The Instana Node.js collector is now fully initialized and connected to the Instana host agent.');

  // CASE: This is an IPC message only for a parent process.
  // TODO: Add an EventEmitter functionality for the current process
  //       such as `instana.on('instana.collector.initialized')`.
  // eslint-disable-next-line no-unused-expressions
  process?.send?.('instana.collector.initialized');

  if (!isMainThread) {
    const { parentPort } = require('worker_threads');

    if (parentPort) {
      // CASE: This is for the worker thread if available.
      parentPort.postMessage('instana.collector.initialized');
    }
  }
}

function leave() {
  if (isMainThread) {
    uncaught.deactivate();
    metrics.deactivate();
    requestHandler.deactivate();
    transmissionCycle.deactivate();
    deScheduleTracingMetrics();
  }

  tracing.deactivate();

  if (profiler) {
    profiler.destroy();
    profiler = null;
  }
}

function sendTracingMetrics() {
  const payload = tracing._getAndResetTracingMetrics();

  agentConnection.sendTracingMetricsToAgent(payload, error => {
    if (error) {
      if (typeof error.message === 'string' && error.message.indexOf('Got status code 404') !== -1) {
        logger.info(
          'Apparently the version of the Instana host agent on this host does not support the POST /tracermetrics ' +
            'endpoint, will stop sending tracing metrics. This will not affect monitoring or tracing.'
        );

        return;
      }

      logger.warn(
        `Error received while trying to send tracing metrics to agent: ${error?.message}.` +
          ' This will not affect monitoring or tracing. Will retry sending tracing metrics on next interval.'
      );

      if (originalTracingMetricsDelay === null) {
        originalTracingMetricsDelay = tracingMetricsDelay;
      }

      // Reduce noisyness, increase retry time on error.
      tracingMetricsDelay += 1000;
    } else if (originalTracingMetricsDelay !== null) {
      // on success, restore original delay
      tracingMetricsDelay = originalTracingMetricsDelay;
      originalTracingMetricsDelay = null;
    }

    scheduleTracingMetrics();
  });
}

function scheduleTracingMetrics() {
  tracingMetricsTimeout = setTimeout(sendTracingMetrics, tracingMetricsDelay);
  tracingMetricsTimeout.unref();
}

function deScheduleTracingMetrics() {
  if (tracingMetricsTimeout) {
    clearTimeout(tracingMetricsTimeout);
    tracingMetricsTimeout = null;
  }
}

function fireMonitoringEvent() {
  agentConnection.sendAgentMonitoringEvent('nodejs_collector_native_addon_autoprofile_missing', 'PROFILER', error => {
    if (error) {
      logger.error(
        `Error received while trying to send a monitoring event to the Instana host agent: ${error?.message}`
      );
    }
  });
}

function sendEOLEvent() {
  const pid = pidStore.getEntityId();
  agentConnection.sendEvent(
    {
      title: `Node.js version ${process.versions.node} reached its end of life`,
      text:
        'This version no longer receives updates or security fixes and might contain unfixed vulnerabilities.\n\n' +
        'Please consider upgrading Node.js to an active version.\n\n' +
        'For a list of active versions visit ' +
        '[https://nodejs.org/en/about/releases/](https://nodejs.org/en/about/releases/)',
      plugin: 'com.instana.forge.infrastructure.runtime.nodejs.NodeJsRuntimePlatform',
      id: pid,
      timestamp: Date.now(),
      duration: EOL_EVENT_DURATION,
      severity: agentConnection.AgentEventSeverity.WARNING,
      path: `${agentOpts.agentUuid}/${pid}/nodejs-eol`
    },
    err => {
      if (err) {
        logger.debug(
          `Sending a monitoring event for the Node.js version end-of-life check has failed.
          ${err?.message} ${err?.stack}`
        );
      }
    }
  );
}

/**
 * Sends an issue event to the agent when the Node.js version has reached end of life.
 * It will work for non-serverless environments where an agent is present.
 * (At the time of writing, the backend service that our serverless in-process collectors send data to does not have
 * support for events.)
 */
function detectEOLNodeVersion() {
  if (isNodeVersionEOL()) {
    setTimeout(() => {
      sendEOLEvent();
      setInterval(sendEOLEvent, EOL_EVENT_REFRESH_INTERVAL).unref();
    }, 2000);
  }
}

module.exports = exports = {
  init,
  enter,
  leave
};
