import cluster, { Worker as ClusterWorker } from 'cluster';
import { EventEmitter } from 'events';
import { isWorkerMessage } from '../../types';
import { sendTestMessage, sendShutdownMessage, subscribeOnWorker } from '../messages';
import { isShuttingDown } from '../utils';
const FORK_RETRIES = 5;
export default class Pool extends EventEmitter {
  workers = [];
  queue = [];
  forcedStop = false;
  get isRunning() {
    return this.workers.length !== this.freeWorkers.length;
  }
  constructor(config, browser) {
    super();
    this.browser = browser;
    this.failFast = config.failFast;
    this.maxRetries = config.maxRetries;
    this.config = config.browsers[browser];
  }
  async init() {
    const poolSize = this.config.limit || 1;
    // TODO Init queue for workers to smooth browser starting load
    this.workers = (await Promise.all(Array.from({
      length: poolSize
    }).map(() => this.forkWorker()))).filter(workerOrError => workerOrError instanceof ClusterWorker);
    if (this.workers.length != poolSize) throw new Error(`Can't instantiate workers for ${this.browser} due many errors`);
    this.workers.forEach(worker => this.exitHandler(worker));
  }
  start(tests) {
    if (this.isRunning) return false;
    this.queue = tests.map(({
      id,
      path
    }) => ({
      id,
      path,
      retries: 0
    }));
    this.process();
    return true;
  }
  stop() {
    if (!this.isRunning) {
      this.emit('stop');
      return;
    }
    this.forcedStop = true;
    this.queue = [];
  }
  process() {
    const worker = this.getFreeWorker();
    const [test] = this.queue;
    if (this.queue.length == 0 && this.workers.length === this.freeWorkers.length) {
      this.forcedStop = false;
      this.emit('stop');
      return;
    }
    if (!worker || !test) return;
    worker.isRunning = true;
    const {
      id
    } = test;
    this.queue.shift();
    this.sendStatus({
      id,
      status: 'running'
    });
    this.subscribe(worker, test);
    sendTestMessage(worker, {
      type: 'start',
      payload: test
    });
    this.process();
  }
  sendStatus(message) {
    this.emit('test', message);
  }
  getFreeWorker() {
    return this.freeWorkers[Math.floor(Math.random() * this.freeWorkers.length)];
  }
  get aliveWorkers() {
    return this.workers.filter(worker => !worker.exitedAfterDisconnect);
  }
  get freeWorkers() {
    return this.aliveWorkers.filter(worker => !worker.isRunning);
  }
  async forkWorker(retry = 0) {
    cluster.setupPrimary({
      args: ['--browser', this.browser, ...process.argv.slice(2)]
    });
    const worker = cluster.fork();
    const message = await new Promise(resolve => {
      const readyHandler = message => {
        if (!isWorkerMessage(message)) return;
        worker.off('message', readyHandler);
        resolve(message);
      };
      worker.on('message', readyHandler);
    });
    if (message.type != 'error') return worker;
    this.gracefullyKill(worker);
    if (retry == FORK_RETRIES) return message.payload;
    return this.forkWorker(retry + 1);
  }
  exitHandler(worker) {
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    worker.once('exit', async () => {
      if (isShuttingDown.current) return;
      const workerOrError = await this.forkWorker();
      if (!(workerOrError instanceof ClusterWorker)) throw new Error(`Can't instantiate worker for ${this.browser} due many errors`);
      this.exitHandler(workerOrError);
      this.workers[this.workers.indexOf(worker)] = workerOrError;
      this.process();
    });
  }
  gracefullyKill(worker) {
    const timeout = setTimeout(() => worker.kill(), 10000);
    worker.on('exit', () => clearTimeout(timeout));
    sendShutdownMessage(worker);
  }
  shouldRetry(test) {
    return test.retries < this.maxRetries && !this.forcedStop;
  }
  handleTestResult(worker, test, result) {
    const shouldRetry = result.status == 'failed' && this.shouldRetry(test);
    if (shouldRetry) {
      test.retries += 1;
      this.queue[this.failFast ? 'unshift' : 'push'](test);
    }
    this.sendStatus({
      id: test.id,
      status: shouldRetry ? 'retrying' : result.status,
      result
    });
    worker.isRunning = false;
    setImmediate(() => this.process());
  }
  subscribe(worker, test) {
    const subscriptions = [subscribeOnWorker(worker, 'worker', message => {
      if (message.type != 'error') return;
      subscriptions.forEach(unsubscribe => unsubscribe());
      this.gracefullyKill(worker);
      this.handleTestResult(worker, test, {
        status: 'failed',
        ...message.payload
      });
    }), subscribeOnWorker(worker, 'test', message => {
      if (message.type != 'end') return;
      subscriptions.forEach(unsubscribe => unsubscribe());
      this.handleTestResult(worker, test, message.payload);
    })];
  }
}