// @ts-nocheck
import { captureException } from '@sentry/browser';
import { io, ManagerOptions, Socket, SocketOptions } from 'socket.io-client';
import { ulid } from 'ulid';

import { logEvent } from 'src/config/logging';
import { EVENTS_SOCKETS } from 'src/constants/sockets';
import { EventName } from 'src/types/amplitude';

import { tokenManager } from './tokens';

const CONFIG_SOCKETS: Partial<ManagerOptions & SocketOptions> = {
  transports: ['websocket'],
  reconnection: true,
  secure: true,
  reconnectionDelay: 100,
  reconnectionDelayMax: 500,
};

export type ExtendedCallback = (data, requestId: number) => void;

export class SocketConnection {
  public socket: Nullable<Socket> = null;

  // this variable is used just for logging purposes
  public socketId: string = '';

  // Queue for event listeners added before socket connection
  private pendingEventListeners: Array<{
    event: string;
    callback: ExtendedCallback;
  }> = [];

  constructor() {
    if (SocketConnection.instance instanceof SocketConnection) {
      return SocketConnection.instance;
    }
    SocketConnection.instance = this;
  }

  startSocket = (token = '') => {
    if (this.socket) {
      // Try to connect manually if the socket is not connected
      if (!this.socket.connected) {
        this.socket.connect();
        logEvent(EventName.SOCKET_RECONNECTED, { socketId: this.socketId });
        return;
      }

      this.authenticate(token);
      return;
    }

    this.socket = io(import.meta.env.VITE_SOCKET_URL, CONFIG_SOCKETS);

    this.socket.on('connect', (attempt: number) => {
      logEvent(EventName.SOCKET_CONNECTED, {
        attempt,
        socketId: this.socket.id,
      });
      const { accessToken } = tokenManager.getCorrectToken();
      this.socketId = this.socket.id;
      this.authenticate(accessToken);

      // Apply any pending event listeners that were queued before connection
      this.applyPendingEventListeners();
    });

    this.socket.on('ping', () => {
      logEvent(EventName.SOCKET_PING, {
        socketId: this.socketId,
      });
    });

    this.socket.on('pong', latency => {
      logEvent(EventName.SOCKET_PONG, {
        socketId: this.socketId,
        latency,
      });
    });

    this.socket.on('reconnect', (attempt: number) => {
      logEvent(EventName.SOCKET_RECONNECTED, {
        attempt,
        socketId: this.socketId,
      });
      this.socketId = '';
      // Apply pending listeners on reconnect too
      this.applyPendingEventListeners();
    });

    this.socket.on('disconnect', (reason: string, details?: string) => {
      logEvent(EventName.SOCKET_DISCONNECTED, {
        reason,
        details,
        socketId: this.socketId,
      });
      this.socketId = '';
    });

    this.socket.on('error', error => {
      captureException(error);
    });

    this.socket.on('reconnect_error', error => {
      captureException(error);
    });

    this.socket.on('reconnect_failed', () => {
      const error = new Error('Socket reconnection failed');
      captureException(error);
    });
  };

  applyPendingEventListeners = () => {
    if (this.pendingEventListeners.length > 0 && this.socket) {
      this.pendingEventListeners.forEach(({ event, callback }) => {
        this.socket.on(event, callback);
      });

      // Clear the queue after applying
      this.pendingEventListeners = [];
    }
  };

  authenticate = async token => {
    const isTokenValid = tokenManager.isTokenValid();
    if (isTokenValid) {
      this.emitEvent(EVENTS_SOCKETS.ONLINE, {
        token,
        clientRequestId: ulid(),
      });
      logEvent(EventName.SOCKET_AUTHENTICATED, { socketId: this.socket.id });
    } else {
      try {
        const accessToken = await tokenManager.getValidAccessToken();

        this.emitEvent(EVENTS_SOCKETS.ONLINE, {
          token: accessToken,
          clientRequestId: ulid(),
        });
        logEvent(EventName.SOCKET_JWT_REFRESH, { socketId: this.socket.id });
      } catch (e) {
        logEvent(EventName.SOCKET_JWT_REFRESH_ERROR, {
          socketId: this.socket.id,
          error: e,
        });
      }
    }
  };

  listenEvent = (event, cb: ExtendedCallback) => {
    if (this.socket && this.socket.connected) {
      this.socket.on(event, cb);
    } else {
      // Queue the event listener if socket is not connected yet
      this.pendingEventListeners.push({ event, callback: cb });
    }
  };

  listenEventList = (eventList, cb: ExtendedCallback) => {
    eventList.forEach(event => this.listenEvent(event, cb));
  };

  emitEvent = (event, data) => {
    this.socket && this.socket.emit(event, data);
  };

  closeEvent = (event, cb: ExtendedCallback) => {
    if (this.socket) {
      this.socket.removeEventListener(event, cb);
    }

    // Also remove from pending queue if it exists there
    this.pendingEventListeners = this.pendingEventListeners.filter(
      listener => !(listener.event === event && listener.callback === cb),
    );
  };

  closeEventList = (eventList, cb: ExtendedCallback) => {
    eventList.forEach(event => this.closeEvent(event, cb));
  };

  stopSocket = () => {
    if (this.socket) {
      this.socket.disconnect();

      // Clear pending event listeners when stopping the socket
      this.pendingEventListeners = [];
    }
  };

  restartSocket = token => this.startSocket(token);

  emitNotifyEvent = (event, data) => {
    this.socket && this.socket.emit('notify_general', { event, ...data });
  };
}
