import type { ServerMessage, ClientMessage } from "./protocol";
import type { Session } from "./types";
import { DomainError } from "./errors";
import {
  addChatMessage,
  approveTakeover,
  bankerAdjust,
  bankerForceTransfer,
  createChatGroup,
  createDummyPlayer,
  disconnectPlayer,
  requestTakeover,
  setBlackout,
  snapshotSession,
  startSession,
  endSession,
  transfer,
} from "./domain";
import { getSession, removeSession } from "./store";
import { now } from "./util";
import { notifyChat, notifyTransaction } from "./notifications";

export const STALE_SOCKET_TIMEOUT_MS = 45_000;
const STALE_SOCKET_REAP_INTERVAL_MS = 10_000;

type SocketMeta = {
  sessionId: string;
  playerId: string;
  lastSeenAt: number;
};

const socketsBySession = new Map<string, Set<WebSocket>>();
const socketsByPlayer = new Map<string, Set<WebSocket>>();
let metaBySocket = new WeakMap<WebSocket, SocketMeta>();
const testTimers = new Map<string, ReturnType<typeof setTimeout>>();

function playerSocketKey(sessionId: string, playerId: string): string {
  return `${sessionId}:${playerId}`;
}

function randomInt(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function scheduleTestTransfer(sessionId: string): void {
  const delay = randomInt(3000, 7000);
  const timer = setTimeout(() => {
    testTimers.delete(sessionId);
    runTestTransfer(sessionId);
  }, delay);
  testTimers.set(sessionId, timer);
}

function runTestTransfer(sessionId: string): void {
  const session = getSession(sessionId);
  if (!session || !session.isTest) {
    return;
  }
  if (session.status !== "active") {
    return;
  }
  const players = Array.from(session.players.values()).filter(
    (player) => player.role === "player",
  );
  if (players.length < 2) {
    scheduleTestTransfer(sessionId);
    return;
  }
  let attempts = 0;
  while (attempts < 5) {
    const from = players[randomInt(0, players.length - 1)];
    const to = players[randomInt(0, players.length - 1)];
    if (to.id === from.id) {
      attempts += 1;
      continue;
    }
    const amount = randomInt(10, 250);
    if (from.balance < amount) {
      attempts += 1;
      continue;
    }
    try {
      const transaction = transfer(session, from.id, to.id, amount, null);
      notifyTransaction(session, transaction);
      sendStateToSession(session);
      break;
    } catch {
      attempts += 1;
    }
  }
  scheduleTestTransfer(sessionId);
}

export function startTestSimulation(sessionId: string): void {
  if (testTimers.has(sessionId)) return;
  scheduleTestTransfer(sessionId);
}

export function stopTestSimulation(sessionId: string): void {
  const timer = testTimers.get(sessionId);
  if (timer) {
    clearTimeout(timer);
  }
  testTimers.delete(sessionId);
}

function getSessionSockets(sessionId: string): Set<WebSocket> {
  let set = socketsBySession.get(sessionId);
  if (!set) {
    set = new Set();
    socketsBySession.set(sessionId, set);
  }
  return set;
}

function getPlayerSockets(sessionId: string, playerId: string): Set<WebSocket> {
  const key = playerSocketKey(sessionId, playerId);
  let set = socketsByPlayer.get(key);
  if (!set) {
    set = new Set();
    socketsByPlayer.set(key, set);
  }
  return set;
}

function deleteSocketFromTracking(ws: WebSocket, meta: SocketMeta): {
  sessionSocketCount: number;
  playerSocketCount: number;
} {
  const sessionSockets = socketsBySession.get(meta.sessionId);
  if (sessionSockets) {
    sessionSockets.delete(ws);
    if (sessionSockets.size === 0) {
      socketsBySession.delete(meta.sessionId);
    }
  }

  const playerKey = playerSocketKey(meta.sessionId, meta.playerId);
  const playerSockets = socketsByPlayer.get(playerKey);
  if (playerSockets) {
    playerSockets.delete(ws);
    if (playerSockets.size === 0) {
      socketsByPlayer.delete(playerKey);
    }
  }

  metaBySocket.delete(ws);

  return {
    sessionSocketCount: sessionSockets?.size ?? 0,
    playerSocketCount: playerSockets?.size ?? 0,
  };
}

function touchSocket(ws: WebSocket): SocketMeta | null {
  const meta = metaBySocket.get(ws);
  if (!meta) {
    return null;
  }
  meta.lastSeenAt = now();
  return meta;
}

export function registerSocket(ws: WebSocket, sessionId: string, playerId: string): void {
  const session = getSession(sessionId);
  if (!session) {
    ws.close(1008, "Session not found");
    return;
  }
  const player = session.players.get(playerId);
  if (!player) {
    ws.close(1008, "Player not found");
    return;
  }

  const meta: SocketMeta = { sessionId, playerId, lastSeenAt: now() };
  metaBySocket.set(ws, meta);
  getSessionSockets(sessionId).add(ws);
  getPlayerSockets(sessionId, playerId).add(ws);

  player.connected = true;
  player.isDummy = false;
  player.lastActiveAt = meta.lastSeenAt;

  sendStateToSession(session);
}

export function unregisterSocket(ws: WebSocket): void {
  const meta = metaBySocket.get(ws);
  if (!meta) {
    return;
  }

  const { sessionId, playerId } = meta;
  const { sessionSocketCount, playerSocketCount } = deleteSocketFromTracking(ws, meta);
  const session = getSession(sessionId);

  if (session && playerSocketCount === 0) {
    disconnectPlayer(session, playerId);
    sendStateToSession(session);
  }

  if (sessionSocketCount === 0 && session?.isTest) {
    stopTestSimulation(sessionId);
    removeSession(sessionId);
  }
}

export function reapStaleSockets(referenceNow = now()): void {
  const staleSockets: WebSocket[] = [];

  socketsBySession.forEach((sessionSockets) => {
    sessionSockets.forEach((socket) => {
      const meta = metaBySocket.get(socket);
      if (!meta) {
        return;
      }
      if (referenceNow - meta.lastSeenAt <= STALE_SOCKET_TIMEOUT_MS) {
        return;
      }
      staleSockets.push(socket);
    });
  });

  staleSockets.forEach((socket) => {
    try {
      socket.close(4000, "Connection stale");
    } catch {
      // Ignore close failures.
    }
    unregisterSocket(socket);
  });
}

export function resetWebsocketStateForTests(): void {
  socketsBySession.clear();
  socketsByPlayer.clear();
  testTimers.forEach((timer) => clearTimeout(timer));
  testTimers.clear();
  metaBySocket = new WeakMap<WebSocket, SocketMeta>();
}

export function handleSocketMessage(ws: WebSocket, raw: string | ArrayBuffer): void {
  touchSocket(ws);

  const messageText = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
  let parsed: ClientMessage;
  try {
    parsed = JSON.parse(messageText) as ClientMessage;
  } catch {
    send(ws, { type: "error", message: "Invalid message" });
    return;
  }

  const session = getSession(parsed.sessionId);
  if (!session) {
    send(ws, { type: "error", message: "Session not found" });
    return;
  }

  try {
    handleMessage(ws, session, parsed);
  } catch (error) {
    const message =
      error instanceof DomainError
        ? error.message
        : "Unexpected error while processing request";
    send(ws, { type: "error", message });
    return;
  }

  sendStateToSession(session);
}

function handleMessage(ws: WebSocket, session: Session, message: ClientMessage): void {
  switch (message.type) {
    case "chat_send": {
      const chat = addChatMessage(session, message.playerId, message.body, message.groupId);
      notifyChat(session, chat);
      return;
    }
    case "transfer": {
      const transaction = transfer(
        session,
        message.playerId,
        message.toPlayerId,
        message.amount,
        message.note,
      );
      notifyTransaction(session, transaction);
      return;
    }
    case "banker_adjust": {
      const transaction = bankerAdjust(
        session,
        message.bankerId,
        message.targetId,
        message.amount,
        message.note,
      );
      notifyTransaction(session, transaction);
      return;
    }
    case "banker_force_transfer": {
      const transaction = bankerForceTransfer(
        session,
        message.bankerId,
        message.fromId,
        message.toId,
        message.amount,
        message.note,
      );
      notifyTransaction(session, transaction);
      return;
    }
    case "banker_blackout":
      setBlackout(session, message.bankerId, message.active, message.reason);
      return;
    case "banker_start":
      startSession(session, message.bankerId);
      return;
    case "banker_end":
      endSession(session, message.bankerId);
      return;
    case "banker_create_dummy":
      createDummyPlayer(session, message.bankerId, message.name, message.balance);
      return;
    case "group_create":
      createChatGroup(session, message.playerId, message.name, message.memberIds);
      return;
    case "takeover_request":
      requestTakeover(session, message.playerId, message.dummyId);
      return;
    case "banker_takeover_approve": {
      const assignedId = approveTakeover(
        session,
        message.bankerId,
        message.dummyId,
        message.requesterId,
      );
      notifyTakeoverApproval(session.id, message.requesterId, assignedId);
      return;
    }
    case "ping":
      touchPlayer(session, message.playerId);
      send(ws, { type: "pong" });
      return;
    default:
      return;
  }
}

function touchPlayer(session: Session, playerId: string): void {
  const player = session.players.get(playerId);
  if (player) {
    player.lastActiveAt = now();
  }
}

function sendStateToSession(session: Session): void {
  const sockets = socketsBySession.get(session.id);
  if (!sockets) {
    return;
  }
  const stateMessage: ServerMessage = {
    type: "state",
    session: snapshotSession(session),
  };
  sockets.forEach((socket) => send(socket, stateMessage));
}

export function broadcastSessionState(sessionId: string): void {
  const session = getSession(sessionId);
  if (!session) return;
  sendStateToSession(session);
}

function notifyTakeoverApproval(
  sessionId: string,
  requesterId: string,
  assignedId: string,
): void {
  const sockets = socketsBySession.get(sessionId);
  if (!sockets) {
    return;
  }
  sockets.forEach((socket) => {
    const meta = metaBySocket.get(socket);
    if (!meta) {
      return;
    }
    if (meta.playerId === requesterId) {
      send(socket, { type: "takeover_approved", assignedPlayerId: assignedId });
      meta.playerId = assignedId;
      deleteSocketFromTracking(socket, {
        ...meta,
        playerId: requesterId,
      });
      getSessionSockets(sessionId).add(socket);
      getPlayerSockets(sessionId, assignedId).add(socket);
      metaBySocket.set(socket, meta);
    }
  });
}

function send(ws: WebSocket, message: ServerMessage): void {
  if (ws.readyState !== WebSocket.OPEN) {
    return;
  }
  ws.send(JSON.stringify(message));
}

setInterval(() => {
  reapStaleSockets();
}, STALE_SOCKET_REAP_INTERVAL_MS);
