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";

const socketsBySession = new Map<string, Set<WebSocket>>();
const metaBySocket = new WeakMap<WebSocket, { sessionId: string; playerId: string }>();
const testTimers = new Map<string, ReturnType<typeof setTimeout>>();

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)];
    let 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;
}

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;
  }
  player.connected = true;
  player.isDummy = false;
  player.lastActiveAt = now();

  metaBySocket.set(ws, { sessionId, playerId });
  getSessionSockets(sessionId).add(ws);

  sendStateToSession(session);
}

export function unregisterSocket(ws: WebSocket): void {
  const meta = metaBySocket.get(ws);
  if (!meta) {
    return;
  }
  const { sessionId, playerId } = meta;
  const session = getSession(sessionId);
  if (session) {
    disconnectPlayer(session, playerId);
    sendStateToSession(session);
  }
  const set = socketsBySession.get(sessionId);
  if (set) {
    set.delete(ws);
    if (set.size === 0) {
      socketsBySession.delete(sessionId);
      if (session?.isTest) {
        stopTestSimulation(sessionId);
        removeSession(sessionId);
      }
    }
  }
  metaBySocket.delete(ws);
}

export function handleSocketMessage(ws: WebSocket, raw: string | ArrayBuffer): void {
  const messageText = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
  let parsed: ClientMessage;
  try {
    parsed = JSON.parse(messageText) as ClientMessage;
  } catch (error) {
    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(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(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);
      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;
    }
  });
}

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