import cluster, { Worker } from 'cluster';
import { Server } from 'http';
import { cpus } from 'os';

import { BasePlugin } from './BasePlugin';

export enum MsgCmd {
  LOG_LEVEL_UPDATE_FROM_WORKER,
  LOG_LEVEL_UPDATE_FROM_MASTER,
  GET_LOG_LEVEL_REQUEST,
}

export interface SocketMsg {
  cmd: MsgCmd;
  value?: string;
}

export class ProductionPluginRunner {
  numCPUs = cpus().length;

  pluginPort: number;

  plugin: BasePlugin;
  server: Server;

  constructor(plugin: BasePlugin) {
    this.plugin = plugin;
  }

  updatePluginLogLevel = (value: string) => {
    this.plugin.logger.level = value;
  };

  broadcastLogLevelToWorkers = () => {
    const msg = {
      cmd: MsgCmd.LOG_LEVEL_UPDATE_FROM_MASTER,
      value: this.plugin.logger.level,
    };

    for (const id in cluster.workers) {
      const worker = cluster.workers[id];
      if (worker) {
        worker.send(msg);
      }
    }
  };

  /**
   * Socker Listener for master process. It has 4 jobs:
   * 1 - If a new token is detected by a Worker, it receives a message and should send a message to each workers
   * 2 - If a worker is asking for a token update (ex: the worker was just created because one of his friends died), the master should send a message to each workers
   * 3 - If a log level change is detected by a worker, it receives a message and should send a message to each workers
   * 4 - If a worker is asking for a log level update (ex: the worker was just created because one of his friends died), the master should send a message to each workers
   * @param recMsg
   */
  masterListener = (worker: Worker, recMsg: SocketMsg) => {
    this.plugin.logger.debug(
      `Master ${process.pid} is being called with cmd: ${MsgCmd[recMsg.cmd]}, value: ${
        recMsg.value ? recMsg.value : 'undefined'
      }`,
    );

    if (recMsg.cmd === MsgCmd.LOG_LEVEL_UPDATE_FROM_WORKER) {
      if (recMsg.value) {
        // If we receive a Log Level, we update the token
        this.updatePluginLogLevel(recMsg.value);
        // We send the log level to each of the workers
        this.broadcastLogLevelToWorkers();
      } else {
        throw new Error(
          'We received a LOG_LEVEL_UPDATE_FROM_WORKER msg without logLevel in the value field of the msg.',
        );
      }
    }
  };

  /**
   * Socker Listener of the workers. It should listen for Token update from master and for Log Level changes
   * @param recMsg
   */
  workerListener = (recMsg: SocketMsg) => {
    this.plugin.logger.debug(
      `Worker ${process.pid} is being called with cmd: ${MsgCmd[recMsg.cmd]}, value: ${
        recMsg.value ? recMsg.value : 'undefined'
      }`,
    );

    if (recMsg.cmd === MsgCmd.LOG_LEVEL_UPDATE_FROM_MASTER) {
      if (!recMsg.value) {
        throw new Error(
          'We received a LOG_LEVEL_UPDATE_FROM_MASTER msg without logLevel in the value field of the msg.',
        );
      }
      const level = recMsg.value.toLocaleLowerCase();

      this.plugin.onLogLevelUpdate(level);

      this.plugin.logger.debug(`${process.pid}: Updated log level with: ${JSON.stringify(this.plugin.logger.level)}`);
    }
  };

  /**
   * Multi threading launch of the App, with socket communicaton to propagate token updates
   * @param port
   */
  start(port?: number, multiProcessEnabled = false) {
    const pluginPort = process.env.PLUGIN_PORT;
    this.pluginPort = pluginPort ? parseInt(pluginPort) : 8080;

    const serverPort = port ? port : this.pluginPort;

    if (multiProcessEnabled) {
      if (cluster.isMaster) {
        this.plugin.logger.info(`Master ${process.pid} is running`);

        // Fork workers.
        for (let i = 0; i < this.numCPUs; i++) {
          cluster.fork();
        }

        // Listener for when the Cluster is being called by a worker
        cluster.on('message', this.masterListener);

        // Sometimes, workers dies
        cluster.on('exit', (worker, code, signal) => {
          this.plugin.logger.info(`worker ${worker.process.pid as number} died`);

          // We add a new worker, with the proper socket listener
          cluster.fork();
        });
      } else {
        // We pass the Plugin into MT mode
        this.plugin.multiThread = true;

        // We attach a socket listener to get messages from master
        process.on('message', this.workerListener);

        const serverPort = port ? port : this.pluginPort;

        this.server = this.plugin.app.listen(serverPort, () =>
          this.plugin.logger.info(`${process.pid} Plugin started, listening at ${serverPort}`),
        );

        this.plugin.logger.info(`Worker ${process.pid} started`);
      }
    } else {
      this.server = this.plugin.app.listen(serverPort, () =>
        this.plugin.logger.info(`${process.pid} Plugin started, listening at ${serverPort}`),
      );
    }
  }
}
