import { connectedUsers } from './state';
import { ConnectedUser } from './types';
import {
  broadcastToServer,
  findUserByOderId,
  getServerIdsForOderId,
  getUniqueUsersInServer,
  isOderIdConnectedToServer
} from './broadcast';
import { authorizeWebSocketJoin } from '../services/server-access.service';
import {
  getPluginRequirementsSnapshot,
  PluginSupportError,
  validatePluginEventEnvelope
} from '../services/plugin-support.service';

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

function normalizeDisplayName(value: unknown, fallback = 'User'): string {
  const normalized = typeof value === 'string' ? value.trim() : '';

  return normalized || fallback;
}

function normalizeDescription(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }

  const normalized = value.trim();

  return normalized || undefined;
}

function normalizeProfileUpdatedAt(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
}

function readMessageId(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }

  const normalized = value.trim();

  if (!normalized || normalized === 'undefined' || normalized === 'null') {
    return undefined;
  }

  return normalized;
}

function sendPluginError(user: ConnectedUser, error: unknown, message: WsMessage): void {
  if (error instanceof PluginSupportError) {
    user.ws.send(
      JSON.stringify({
        type: 'plugin_error',
        serverId: typeof message['serverId'] === 'string' ? message['serverId'] : undefined,
        pluginId: typeof message['pluginId'] === 'string' ? message['pluginId'] : undefined,
        eventName: typeof message['eventName'] === 'string' ? message['eventName'] : undefined,
        eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined,
        code: error.code,
        message: error.message
      })
    );

    return;
  }

  console.error('Unhandled plugin websocket error:', error);
  user.ws.send(
    JSON.stringify({
      type: 'plugin_error',
      code: 'INTERNAL_ERROR',
      message: 'Internal server error'
    })
  );
}

/** Sends the current user list for a given server to a single connected user. */
function sendServerUsers(user: ConnectedUser, serverId: string): void {
  const users = getUniqueUsersInServer(serverId, user.oderId).map((cu) => ({
    oderId: cu.oderId,
    displayName: normalizeDisplayName(cu.displayName),
    description: cu.description,
    profileUpdatedAt: cu.profileUpdatedAt,
    status: cu.status ?? 'online'
  }));

  user.ws.send(JSON.stringify({ type: 'server_users', serverId, users }));
}

async function sendPluginRequirements(user: ConnectedUser, serverId: string): Promise<void> {
  try {
    const snapshot = await getPluginRequirementsSnapshot(serverId);

    user.ws.send(
      JSON.stringify({
        type: 'plugin_requirements',
        serverId,
        snapshot
      })
    );
  } catch (error) {
    sendPluginError(user, error, { type: 'plugin_requirements', serverId });
  }
}

function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: string): void {
  const newOderId = readMessageId(message['oderId']) ?? connectionId;
  const newScope = typeof message['connectionScope'] === 'string' ? message['connectionScope'] : undefined;
  const previousDisplayName = normalizeDisplayName(user.displayName);
  const previousDescription = user.description;
  const previousProfileUpdatedAt = user.profileUpdatedAt;

  user.oderId = newOderId;
  user.displayName = normalizeDisplayName(message['displayName'], normalizeDisplayName(user.displayName));

  if (Object.prototype.hasOwnProperty.call(message, 'description')) {
    user.description = normalizeDescription(message['description']);
  }

  if (Object.prototype.hasOwnProperty.call(message, 'profileUpdatedAt')) {
    user.profileUpdatedAt = normalizeProfileUpdatedAt(message['profileUpdatedAt']);
  }

  user.connectionScope = newScope;
  connectedUsers.set(connectionId, user);
  console.log(`User identified: ${user.displayName} (${user.oderId})`);

  if (user.displayName === previousDisplayName && user.description === previousDescription && user.profileUpdatedAt === previousProfileUpdatedAt) {
    return;
  }

  for (const serverId of user.serverIds) {
    broadcastToServer(
      serverId,
      {
        type: 'user_joined',
        oderId: user.oderId,
        displayName: normalizeDisplayName(user.displayName),
        description: user.description,
        profileUpdatedAt: user.profileUpdatedAt,
        status: user.status ?? 'online',
        serverId
      },
      user.oderId
    );
  }
}

async function handleJoinServer(user: ConnectedUser, message: WsMessage, connectionId: string): Promise<void> {
  const sid = readMessageId(message['serverId']);

  if (!sid)
    return;

  const authorization = await authorizeWebSocketJoin(sid, user.oderId);

  if (!authorization.allowed) {
    user.ws.send(
      JSON.stringify({
        type: 'access_denied',
        serverId: sid,
        reason: authorization.reason
      })
    );

    return;
  }

  const isNewConnectionMembership = !user.serverIds.has(sid);
  const isNewIdentityMembership = isNewConnectionMembership && !isOderIdConnectedToServer(user.oderId, sid, connectionId);

  user.serverIds.add(sid);
  user.viewedServerId = sid;
  connectedUsers.set(connectionId, user);
  console.log(
    `User ${normalizeDisplayName(user.displayName)} (${user.oderId}) joined server ${sid} ` +
      `(newConnection=${isNewConnectionMembership}, newIdentity=${isNewIdentityMembership})`
  );

  sendServerUsers(user, sid);
  await sendPluginRequirements(user, sid);

  if (isNewIdentityMembership) {
    broadcastToServer(
      sid,
      {
        type: 'user_joined',
        oderId: user.oderId,
        displayName: normalizeDisplayName(user.displayName),
        description: user.description,
        profileUpdatedAt: user.profileUpdatedAt,
        status: user.status ?? 'online',
        serverId: sid
      },
      user.oderId
    );
  }
}

async function handleViewServer(user: ConnectedUser, message: WsMessage, connectionId: string): Promise<void> {
  const viewSid = readMessageId(message['serverId']);

  if (!viewSid)
    return;

  if (!user.serverIds.has(viewSid)) {
    return;
  }

  user.viewedServerId = viewSid;
  connectedUsers.set(connectionId, user);
  console.log(`User ${normalizeDisplayName(user.displayName)} (${user.oderId}) viewing server ${viewSid}`);

  sendServerUsers(user, viewSid);
  await sendPluginRequirements(user, viewSid);
}

function handleLeaveServer(user: ConnectedUser, message: WsMessage, connectionId: string): void {
  const leaveSid = readMessageId(message['serverId']) ?? user.viewedServerId;

  if (!leaveSid)
    return;

  user.serverIds.delete(leaveSid);

  if (user.viewedServerId === leaveSid)
    user.viewedServerId = undefined;

  connectedUsers.set(connectionId, user);

  const remainingServerIds = getServerIdsForOderId(user.oderId, connectionId);

  if (remainingServerIds.includes(leaveSid)) {
    return;
  }

  broadcastToServer(
    leaveSid,
    {
      type: 'user_left',
      oderId: user.oderId,
      displayName: normalizeDisplayName(user.displayName),
      serverId: leaveSid,
      serverIds: remainingServerIds
    },
    user.oderId
  );
}

function forwardRtcMessage(user: ConnectedUser, message: WsMessage): void {
  const targetUserId = readMessageId(message['targetUserId']) ?? '';

  console.log(`Forwarding ${message.type} from ${user.oderId} to ${targetUserId}`);

  const targetUser = findUserByOderId(targetUserId);

  if (targetUser) {
    targetUser.ws.send(JSON.stringify({ ...message, fromUserId: user.oderId }));
    console.log(`Successfully forwarded ${message.type} to ${targetUserId}`);
  } else {
    console.log(
      `Target user ${targetUserId} not found. Connected users:`,
      Array.from(connectedUsers.values()).map((cu) => ({ oderId: cu.oderId, displayName: cu.displayName }))
    );
  }
}

function handleChatMessage(user: ConnectedUser, message: WsMessage): void {
  const chatSid = (message['serverId'] as string | undefined) ?? user.viewedServerId;

  if (chatSid && user.serverIds.has(chatSid)) {
    broadcastToServer(chatSid, {
      type: 'chat_message',
      serverId: chatSid,
      message: message['message'],
      senderId: user.oderId,
      senderName: user.displayName,
      timestamp: Date.now()
    });
  }
}

function handleTyping(user: ConnectedUser, message: WsMessage): void {
  const typingSid = (message['serverId'] as string | undefined) ?? user.viewedServerId;
  const channelId = typeof message['channelId'] === 'string' && message['channelId'].trim() ? message['channelId'].trim() : 'general';

  if (typingSid && user.serverIds.has(typingSid)) {
    broadcastToServer(
      typingSid,
      {
        type: 'user_typing',
        serverId: typingSid,
        channelId,
        oderId: user.oderId,
        displayName: user.displayName
      },
      user.oderId
    );
  }
}

const VALID_STATUSES = new Set([
  'online',
  'away',
  'busy',
  'offline'
]);

function handleStatusUpdate(user: ConnectedUser, message: WsMessage, connectionId: string): void {
  const status = typeof message['status'] === 'string' ? message['status'] : undefined;

  if (!status || !VALID_STATUSES.has(status))
    return;

  user.status = status as ConnectedUser['status'];
  connectedUsers.set(connectionId, user);
  console.log(`User ${normalizeDisplayName(user.displayName)} (${user.oderId}) status -> ${status}`);

  for (const serverId of user.serverIds) {
    broadcastToServer(
      serverId,
      {
        type: 'status_update',
        oderId: user.oderId,
        status
      },
      user.oderId
    );
  }
}

function handleServerIconAvailable(user: ConnectedUser, message: WsMessage, connectionId: string): void {
  const serverId = readMessageId(message['serverId']);
  const iconUpdatedAt = typeof message['iconUpdatedAt'] === 'number' && Number.isFinite(message['iconUpdatedAt']) ? message['iconUpdatedAt'] : 0;

  if (!serverId || iconUpdatedAt <= 0 || !user.serverIds.has(serverId)) {
    return;
  }

  const availableIcons = user.serverIconUpdatedAtByServerId ?? new Map<string, number>();

  availableIcons.set(serverId, iconUpdatedAt);
  user.serverIconUpdatedAtByServerId = availableIcons;
  connectedUsers.set(connectionId, user);
}

function handleServerIconSyncRequest(user: ConnectedUser, message: WsMessage): void {
  const serverId = readMessageId(message['serverId']);
  const localUpdatedAt = typeof message['iconUpdatedAt'] === 'number' && Number.isFinite(message['iconUpdatedAt']) ? message['iconUpdatedAt'] : 0;

  if (!serverId) {
    return;
  }

  const users = getUniqueUsersInServer(serverId, user.oderId)
    .filter((candidate) => (candidate.serverIconUpdatedAtByServerId?.get(serverId) ?? 0) > localUpdatedAt)
    .map((candidate) => ({
      oderId: candidate.oderId,
      displayName: normalizeDisplayName(candidate.displayName),
      description: candidate.description,
      profileUpdatedAt: candidate.profileUpdatedAt,
      status: candidate.status ?? 'online'
    }));

  if (users.length === 0) {
    return;
  }

  user.ws.send(JSON.stringify({ type: 'server_icon_sync_peers', serverId, users }));
}

async function handlePluginEvent(user: ConnectedUser, message: WsMessage): Promise<void> {
  const serverId = readMessageId(message['serverId']) ?? user.viewedServerId;
  const pluginId = readMessageId(message['pluginId']);
  const eventName = readMessageId(message['eventName']);

  if (!serverId || !pluginId || !eventName || !user.serverIds.has(serverId)) {
    user.ws.send(
      JSON.stringify({
        type: 'plugin_error',
        serverId,
        pluginId,
        eventName,
        eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined,
        code: 'INVALID_PLUGIN_EVENT',
        message: 'Plugin event is missing required fields or server membership'
      })
    );

    return;
  }

  try {
    await validatePluginEventEnvelope({
      type: 'plugin_event',
      serverId,
      pluginId,
      eventName,
      eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined,
      payload: message['payload'],
      sourcePluginUserId: typeof message['sourcePluginUserId'] === 'string' ? message['sourcePluginUserId'] : undefined
    });

    broadcastToServer(
      serverId,
      {
        type: 'plugin_event',
        serverId,
        pluginId,
        eventName,
        eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined,
        payload: message['payload'],
        sourcePluginUserId: typeof message['sourcePluginUserId'] === 'string' ? message['sourcePluginUserId'] : undefined,
        sourceUserId: user.oderId,
        emittedAt: Date.now()
      },
      user.oderId
    );
  } catch (error) {
    sendPluginError(user, error, message);
  }
}

export async function handleWebSocketMessage(connectionId: string, message: WsMessage): Promise<void> {
  const user = connectedUsers.get(connectionId);

  if (!user)
    return;

  user.lastPong = Date.now();
  connectedUsers.set(connectionId, user);

  switch (message.type) {
    case 'keepalive':
      break;

    case 'identify':
      handleIdentify(user, message, connectionId);
      break;

    case 'join_server':
      await handleJoinServer(user, message, connectionId);
      break;

    case 'view_server':
      await handleViewServer(user, message, connectionId);
      break;

    case 'leave_server':
      handleLeaveServer(user, message, connectionId);
      break;

    case 'offer':
    case 'answer':
    case 'ice_candidate':
    case 'server_icon_peer_request':
    case 'server_icon_peer_data':
      forwardRtcMessage(user, message);
      break;

    case 'chat_message':
      handleChatMessage(user, message);
      break;

    case 'typing':
      handleTyping(user, message);
      break;

    case 'status_update':
      handleStatusUpdate(user, message, connectionId);
      break;

    case 'server_icon_available':
      handleServerIconAvailable(user, message, connectionId);
      break;

    case 'server_icon_sync_request':
      handleServerIconSyncRequest(user, message);
      break;

    case 'plugin_event':
      await handlePluginEvent(user, message);
      break;

    default:
      console.log('Unknown message type:', message.type);
  }
}
