import { WebSocket } from 'ws';
import { connectedUsers } from './state';
import { ConnectedUser } from './types';

interface WsMessage {
  [key: string]: unknown;
  type: string;
}

export interface BroadcastOptions {
  /** Skip only the sending WebSocket connection. */
  excludeConnectionId?: string;
  /** Skip every open connection for this identity (presence events). */
  excludeIdentityOderId?: string;
}

export function broadcastToServer(serverId: string, message: WsMessage, options?: BroadcastOptions): void {
  console.log(
    `Broadcasting to server ${serverId}, excluding connection ${options?.excludeConnectionId ?? 'none'} ` +
      `identity ${options?.excludeIdentityOderId ?? 'none'}:`,
    message.type
  );

  connectedUsers.forEach((user, connectionId) => {
    if (
      !user.serverIds.has(serverId)
      || connectionId === options?.excludeConnectionId
      || (options?.excludeIdentityOderId && user.oderId === options.excludeIdentityOderId)
      || user.ws.readyState !== WebSocket.OPEN
    ) {
      return;
    }

    try {
      console.log(`  -> Sending to ${user.displayName} (${user.oderId}) via ${connectionId}`);
      user.ws.send(JSON.stringify(message));
    } catch (error) {
      console.warn(`Failed to broadcast ${message.type} to ${user.displayName ?? 'Unknown'} (${user.oderId})`, error);
    }
  });
}

export function notifyServerOwner(ownerId: string, message: WsMessage): void {
  const owner = findUserByOderId(ownerId);

  if (owner) {
    owner.ws.send(JSON.stringify(message));
  }
}

export function getUniqueUsersInServer(serverId: string, excludeOderId?: string): ConnectedUser[] {
  const usersByOderId = new Map<string, ConnectedUser>();

  connectedUsers.forEach((user) => {
    if (user.oderId === excludeOderId || !user.serverIds.has(serverId) || user.ws.readyState !== WebSocket.OPEN) {
      return;
    }

    usersByOderId.set(user.oderId, user);
  });

  return Array.from(usersByOderId.values());
}

export function isOderIdConnectedToServer(oderId: string, serverId: string, excludeConnectionId?: string): boolean {
  return Array.from(connectedUsers.entries()).some(([connectionId, user]) =>
    connectionId !== excludeConnectionId
    && user.oderId === oderId
    && user.serverIds.has(serverId)
    && user.ws.readyState === WebSocket.OPEN
  );
}

export function getServerIdsForOderId(oderId: string, excludeConnectionId?: string): string[] {
  const serverIds = new Set<string>();

  connectedUsers.forEach((user, connectionId) => {
    if (connectionId === excludeConnectionId || user.oderId !== oderId || user.ws.readyState !== WebSocket.OPEN) {
      return;
    }

    user.serverIds.forEach((serverId) => serverIds.add(serverId));
  });

  return Array.from(serverIds);
}

export function notifyUser(oderId: string, message: WsMessage): void {
  const user = findUserByOderId(oderId);

  if (user) {
    user.ws.send(JSON.stringify(message));
  }
}

export function notifyOtherConnectionsForOderId(
  oderId: string,
  message: WsMessage,
  excludeConnectionId?: string
): void {
  connectedUsers.forEach((user, connectionId) => {
    if (
      connectionId === excludeConnectionId
      || user.oderId !== oderId
      || user.ws.readyState !== WebSocket.OPEN
    ) {
      return;
    }

    try {
      user.ws.send(JSON.stringify(message));
    } catch (error) {
      console.warn(`Failed to notify ${user.displayName ?? 'Unknown'} (${user.oderId})`, error);
    }
  });
}

export function findUserByOderId(oderId: string) {
  return findVoiceActiveConnection(oderId) ?? findAnyConnectionForOderId(oderId);
}

export function findVoiceActiveConnection(oderId: string): ConnectedUser | undefined {
  let voiceActiveMatch: ConnectedUser | undefined;

  connectedUsers.forEach((user) => {
    if (user.oderId === oderId && user.voiceActive && user.ws.readyState === WebSocket.OPEN) {
      voiceActiveMatch = user;
    }
  });

  return voiceActiveMatch;
}

export function findAnyConnectionForOderId(oderId: string): ConnectedUser | undefined {
  let match: ConnectedUser | undefined;

  connectedUsers.forEach((user) => {
    if (user.oderId === oderId && user.ws.readyState === WebSocket.OPEN) {
      match = user;
    }
  });

  return match;
}
