import {
  Logger,
  ParseIntPipe,
  UseFilters,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Namespace, Socket } from 'socket.io';
import { LicenseeService } from '../licensee/licensee.service';
import { MessageDto } from '../message/dto/message.dto';
import { Message } from '../message/message.entity';
import { WsExceptionFilter } from '../shared/exception-filters/ws.exception-filter';
import {
  ERedisEvent,
  RedisService,
} from '../shared/services/redis/redis.service';
import { Ticket } from './ticket.entity';

const LICENSEE_ID_QUERY_PARAM = 'licenseeId';

@UsePipes(new ValidationPipe())
@UseFilters(new WsExceptionFilter())
@WebSocketGateway({
  namespace: 'ticket',
})
export class TicketGateway implements OnGatewayConnection, OnGatewayDisconnect {
  private readonly logger = new Logger('TicketGateway');

  @WebSocketServer() namespace: Namespace;

  constructor(
    private redisService: RedisService,
    private licenseeService: LicenseeService
  ) {
    this.addRedisEventListeners();
  }

  private addRedisEventListeners() {
    this.redisService
      .on(ERedisEvent.EMIT_SOCKET_MESSAGE)
      .subscribe(({ data }) =>
        this.emitTicketMessage(data.licenseeId, data.messageId, false)
      );

    this.redisService
      .on(ERedisEvent.EMIT_SOCKET_MESSAGE_CHANGED)
      .subscribe(({ data }) =>
        this.emitTicketMessage(data.licenseeId, data.messageId, true)
      );
  }

  handleConnection(@ConnectedSocket() socket: Socket) {
    const licenseeId = socket.handshake.query[LICENSEE_ID_QUERY_PARAM];

    if (licenseeId === undefined) {
      socket.emit('exception', {
        status: 'error',
        message: '`licenseeId` must be provided in the query!',
      });
      socket.disconnect();
      return;
    }

    socket.join(licenseeId.toString());
    this.licenseeService.connected(+licenseeId);
    this.logger.debug(`New connection. licenseeId=${licenseeId}`);
  }

  handleDisconnect(@ConnectedSocket() socket: Socket) {
    const licenseeId = socket.handshake.query[LICENSEE_ID_QUERY_PARAM];
    this.licenseeService.disconnected(+licenseeId);
    this.logger.debug(`licenseeId=${licenseeId} disconnected`);
  }

  async emitTicketMessage(
    licenseeId: number,
    messageId: number,
    bChanged: boolean
  ) {
    if (!this.isLicenseeConnected(licenseeId)) {
      return;
    }

    const eventName = bChanged ? 'Message changed' : 'Message';
    this.logger.debug(
      `${eventName} has been emitted for [licenseeId=${licenseeId}]`
    );

    const message = await Message.findOne(messageId);

    this.namespace
      .to(licenseeId.toString())
      .emit(
        bChanged ? 'message_changed' : 'message',
        await MessageDto.createFromEntity(message)
      );
  }

  @SubscribeMessage('read')
  async handleRead(@MessageBody(ParseIntPipe) ticketId: number) {
    this.logger.debug(`Read has been received for Ticket[id=${ticketId}]`);

    const ticket = await Ticket.findOne(ticketId);

    if (ticket === undefined) {
      this.logger.error(
        `Failed to handle "socket.read.received" for Ticket[id=${ticketId}]: No Ticket found`
      );
      return;
    }

    await ticket.refreshLastQueriedOn();
  }

  private isLicenseeConnected(licenseeId: number): boolean {
    const rooms = this.namespace.adapter.rooms;
    return rooms[licenseeId] !== undefined && rooms[licenseeId].length !== 0;
  }
}
