import WebSocket from 'ws';
import { INotificationGateway } from '../../application/ports/notification-gateway';
import { ILogger } from '../../application/ports/logger';

interface IdentifyMessage {
  type: 'identify';
  deviceId: string;
}

const isIdentifyMessage = (data: unknown): data is IdentifyMessage => {
  if (typeof data !== 'object' || data === null) {
    return false;
  }
  const message = data as Record<string, unknown>;
  return message.type === 'identify' && typeof message.deviceId === 'string' && message.deviceId.length > 0;
};

/**
 * WebSocket implementation of the notification gateway.
 *
 * Responsibilities:
 *   - Accept raw WebSocket connections.
 *   - Wait for an `identify` message containing a deviceId.
 *   - Maintain a map of connected deviceIds to sockets.
 *   - Deliver JSON messages to connected devices.
 *
 * This class does NOT decide which devices should receive a message or what
 * the message content should be; that belongs to the application layer.
 */
export class WebSocketNotificationGateway implements INotificationGateway {
  private readonly sockets = new Map<string, WebSocket>();

  constructor(private readonly logger: ILogger) {}

  handleConnection(socket: WebSocket): void {
    socket.on('message', (raw) => {
      try {
        const data = JSON.parse(raw.toString());
        if (isIdentifyMessage(data)) {
          this.registerSocket(data.deviceId, socket);
          this.sendRaw(socket, { type: 'identified', deviceId: data.deviceId });
          return;
        }

        this.sendRaw(socket, { type: 'error', message: 'Expected identify message first' });
        socket.close(1002, 'Expected identify message first');
      } catch (error) {
        this.logger.warn('[WebSocket] Invalid message received', { error });
        this.sendRaw(socket, { type: 'error', message: 'Invalid JSON' });
      }
    });

    socket.on('close', () => {
      this.removeSocket(socket);
    });

    socket.on('error', (error) => {
      this.logger.warn('[WebSocket] Socket error', { error });
    });

    // Give the client a short window to identify before closing the socket.
    // This is defense-in-depth; well-behaved clients identify immediately.
    const identifyTimeout = setTimeout(() => {
      if (!this.findDeviceIdBySocket(socket)) {
        this.sendRaw(socket, { type: 'error', message: 'Identification timeout' });
        socket.close(1002, 'Identification timeout');
      }
    }, 30_000);

    socket.once('close', () => {
      clearTimeout(identifyTimeout);
    });
  }

  sendToDevice(deviceId: string, message: unknown): void {
    const socket = this.sockets.get(deviceId);
    if (!socket || socket.readyState !== WebSocket.OPEN) {
      return;
    }

    this.sendRaw(socket, message);
  }

  isDeviceConnected(deviceId: string): boolean {
    const socket = this.sockets.get(deviceId);
    return socket !== undefined && socket.readyState === WebSocket.OPEN;
  }

  private registerSocket(deviceId: string, socket: WebSocket): void {
    const existing = this.sockets.get(deviceId);
    if (existing && existing !== socket) {
      existing.close(1000, 'Replaced by new connection');
    }

    this.sockets.set(deviceId, socket);
    this.logger.debug('[WebSocket] Device identified', { deviceId });
  }

  private removeSocket(socket: WebSocket): void {
    const deviceId = this.findDeviceIdBySocket(socket);
    if (deviceId) {
      this.sockets.delete(deviceId);
      this.logger.debug('[WebSocket] Device disconnected', { deviceId });
    }
  }

  private findDeviceIdBySocket(socket: WebSocket): string | undefined {
    for (const [deviceId, registeredSocket] of this.sockets.entries()) {
      if (registeredSocket === socket) {
        return deviceId;
      }
    }
    return undefined;
  }

  private sendRaw(socket: WebSocket, message: unknown): void {
    if (socket.readyState !== WebSocket.OPEN) {
      return;
    }

    try {
      socket.send(JSON.stringify(message));
    } catch (error) {
      this.logger.warn('[WebSocket] Failed to send message', { error });
    }
  }
}
