/* eslint-disable class-methods-use-this,no-await-in-loop */
import { io, Socket } from 'socket.io-client';
import { sleep } from '../utils/utils';
import AbstractSender from './AbstractSender';
import { cyan, lBlue, reset } from '../utils/color';
import { IRecordsComposite, ISenderConstructorOptions, Nullable, TAccessPoint } from '../interfaces';

const AWAIT_SOCKET_TIMEOUT = 10_000;
const LOOP_SLEEP_MILLIS = 1000;

class WSSender extends AbstractSender {
  private lastConfigServiceAddress: string;

  private address: string;

  private mConsulServiceName: string;

  private socketClient: Nullable<Socket>;

  private readonly accessPointId: string;

  private token: string;

  private readonly socketRequestId: string;

  constructor (options: ISenderConstructorOptions) {
    super(options);
    this.lastConfigServiceAddress = '';
    const { senderConfig, eventEmitter } = options;
    const { host, port } = senderConfig;
    const ap = senderConfig.accessPoint as TAccessPoint;
    this.address = `http://${host}:${port}`;
    this.mConsulServiceName = `${cyan}${ap.consulServiceName}${reset}`;
    this.socketClient = null;
    this.accessPointId = ap.id as string;
    this.token = ap.token as string;
    this.socketRequestId = ap.socketRequestId as string;

    eventEmitter.on('access-point-updated', ({ accessPoint }: { accessPoint: TAccessPoint }) => {
      if (accessPoint.id === this.accessPointId) {
        this.reconnect().then(() => 0);
      }
    });

    this.reconnect().then(() => 0);
  }

  isConnected (): boolean {
    return Boolean(this.socketClient?.emit && this.socketClient?.connected);
  }

  async connect (): Promise<boolean> {
    const { address, mConsulServiceName, token, options } = this;
    const { echo, logger, serviceName } = options;

    const mAddress = `${lBlue}${address}${reset}`;

    echo.info(`Connect to ${cyan}WEB SOCKET${reset} on ${lBlue}${address}${reset}`);

    const opt = {
      query: { fromService: serviceName }, // VVT
      auth: { token },
      extraHeaders: { authorization: token },
    };

    const socketClient = io(address, opt);
    this.socketClient = socketClient;

    return new Promise((resolve) => {
      socketClient.on('connect', () => {
        echo.info(`
====================== Web Socket Sender =======================
Connection established with WEBSOCKET ${mConsulServiceName} on ${mAddress}
================================================================`);
        resolve(true);
      });

      socketClient.on('unauthorized', (reason, callback) => {
        logger.error(`Error on "unauthorized" event while connecting to config service via socket. Reason: ${reason}`);
        resolve(false);
        callback();
      });

      socketClient.on('disconnect', () => {
        logger.warn(`Config service instance ${mConsulServiceName} disconnected`);
        resolve(false);
      });

      socketClient.on('error', (err) => {
        logger.error(err);
        resolve(false);
      });
    });
  }

  async reconnect (force?: boolean): Promise<boolean> {
    const { options: { senderConfig: { port, host } }, lastConfigServiceAddress } = this;
    if (!host || !port) {
      return this.isConnected();
    }
    const address = `http://${host}:${port}`;
    if (force || (lastConfigServiceAddress !== address)) {
      this.lastConfigServiceAddress = address;
      this.address = address;
      return this.connect();
    }
    return false;
  }

  async awaitSocket () {
    if (this.isConnected()) {
      return true;
    }
    const { logger } = this.options;
    const start = Date.now();
    while (!this.isConnected() && (Date.now() - start < AWAIT_SOCKET_TIMEOUT)) {
      if (Date.now() - start < LOOP_SLEEP_MILLIS) {
        logger.silly('Try to connect to the socket...');
      } else {
        logger.warn('Socket is not still connected...');
      }
      await this.reconnect(true);
      await sleep(LOOP_SLEEP_MILLIS);
    }
    return this.isConnected();
  }

  async remoteSocket (rqId: string, ...args: any[]): Promise<{ error?: any, result?: any }> {
    const self = this;
    const { logger } = this.options;
    const error = `NOT connected to the socket. Request id: ${rqId}`;
    if (await this.awaitSocket()) {
      return new Promise((resolve) => {
        if (!self.isConnected()) {
          logger.error(error);
          resolve({ error });
          return;
        }
        args.push((a: any) => {
          resolve(a);
        });
        this.socketClient?.emit(rqId, ...args);
      });
    }
    return { error };
  }

  async sendEvents (recordsComposite: IRecordsComposite): Promise<boolean> {
    const MAX_PACKET_SIZE = 100; // Max number of events in a batch

    const { eventsPacket, first } = recordsComposite;
    if (!eventsPacket.length) {
      return false;
    }
    const { logger } = this.options;

    let stop = false;

    recordsComposite.sentBufferLength = 0;
    recordsComposite.sendCount = 0;
    recordsComposite.last = first;
    while (!stop && eventsPacket.length > 0) {
      const packet = eventsPacket.splice(0, MAX_PACKET_SIZE);
      const pl = packet.length;
      if (pl) {
        /*
        // receiving side signature:
        socket.on(socketRequestId, (request, callback) => {
          callback({ result: true });
        });
        */
        const { error, result } = await this.remoteSocket(this.socketRequestId, packet);
        stop = !result;
        if (stop) {
          if (error) {
            logger.error(error);
          }
          eventsPacket.splice(0, 0, ...packet);
        } else {
          recordsComposite.sendCount += pl;
          recordsComposite.last = packet[pl - 1];
        }
      }
    }
    return true;
  }
}

export default WSSender;
