import { RoomManager } from "./RoomManager";
import { GameWsClientSchema } from "./types";
import type { PlayerInfo } from "./types";
import { logger } from "@shared/lib/logger";
import type { Server, ServerWebSocket } from "bun";

export interface WsConnectionData {
    session: { discordId: string; username: string; role: string };
    rooms: Set<string>;
}

export class GameServer {
    readonly roomManager = new RoomManager();
    private connections = new Map<string, ServerWebSocket<WsConnectionData>>();
    private replacedConnections = new Map<string, ServerWebSocket<WsConnectionData>>();
    private bunServer: Server | null = null;

    constructor() {
        // Subscribe to room events and route them to the right clients

        this.roomManager.emitter.on("room:created", ({ roomId, gameSlug }) => {
            // The creating connection will subscribe itself; just broadcast room list
            this.publishRoomListUpdate();
        });

        this.roomManager.emitter.on("game:started", ({ roomId, spectatorView, playerViews }) => {
            // Send personalised state to each player
            for (const [playerId, view] of playerViews) {
                this.sendToPlayer(playerId, {
                    type: "GAME_STATE",
                    roomId,
                    state: view,
                });
            }
            // Broadcast started event with spectator view to the room channel
            this.publish(`room:${roomId}`, {
                type: "GAME_STARTED",
                roomId,
                state: spectatorView,
            });
        });

        this.roomManager.emitter.on("game:updated", ({ roomId, spectatorView, playerViews }) => {
            // Each player gets their personalised view directly
            for (const [playerId, view] of playerViews) {
                this.sendToPlayer(playerId, {
                    type: "GAME_STATE",
                    roomId,
                    state: view,
                });
            }
            // Spectators/others get the spectator view via pub/sub
            this.publish(`room:${roomId}`, {
                type: "GAME_UPDATE",
                roomId,
                state: spectatorView,
            });
        });

        this.roomManager.emitter.on("game:ended", ({ roomId, winner, reason }) => {
            this.publish(`room:${roomId}`, {
                type: "GAME_ENDED",
                roomId,
                winner,
                reason,
            });
            this.publishRoomListUpdate();
        });

        this.roomManager.emitter.on("player:left", ({ roomId, playerId }) => {
            this.publish(`room:${roomId}`, {
                type: "PLAYER_LEFT",
                roomId,
                playerId,
            });
        });

        this.roomManager.emitter.on("room:list:changed", () => {
            this.publishRoomListUpdate();
        });
    }

    setServer(server: Server): void {
        this.bunServer = server;
    }

    handleOpen(ws: ServerWebSocket<WsConnectionData>): void {
        const discordId = ws.data.session.discordId;
        const existing = this.connections.get(discordId);
        if (existing && existing !== ws) {
            this.replacedConnections.set(discordId, existing);
        }
        this.connections.set(discordId, ws);
        ws.send(JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: this.roomManager.listRooms() }));
    }

    handleMessage(ws: ServerWebSocket<WsConnectionData>, raw: unknown): void {
        const parsed = GameWsClientSchema.safeParse(raw);
        if (!parsed.success) {
            ws.send(JSON.stringify({ type: "ERROR", message: "Invalid message format" }));
            return;
        }

        const msg = parsed.data;
        const { discordId, username, role } = ws.data.session;

        switch (msg.type) {
            case "CREATE_ROOM": {
                const result = this.roomManager.createRoom(msg.gameType, discordId);
                if (!result.ok) {
                    ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
                    return;
                }
                ws.subscribe(`room:${result.roomId}`);
                ws.data.rooms.add(result.roomId);
                ws.send(JSON.stringify({ type: "ROOM_CREATED", roomId: result.roomId, gameSlug: msg.gameType }));
                logger.info("web", `Room created: ${result.roomId} (${msg.gameType}) by ${discordId}`);
                break;
            }

            case "JOIN_ROOM": {
                const result = this.roomManager.joinRoom(msg.roomId, discordId, msg.preferAs, msg.role ?? role);
                if (!result.ok) {
                    ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
                    return;
                }

                ws.subscribe(`room:${msg.roomId}`);
                ws.data.rooms.add(msg.roomId);

                const room = this.roomManager.getRoom(msg.roomId);
                const roomStatus = room?.status ?? "waiting";

                // Determine the current state to send to this client
                let state: unknown = undefined;
                if (room && room.status === "playing") {
                    state = result.joinedAs === "spectator"
                        ? this.roomManager.getSpectatorView(msg.roomId)
                        : this.roomManager.getPlayerView(msg.roomId, discordId);
                }

                // Build player/spectator lists for JOIN_RESULT
                const resolveInfo = (ids: string[]): PlayerInfo[] =>
                    ids.map(id => ({
                        discordId: id,
                        username: this.connections.get(id)?.data.session.username ?? id,
                    }));

                const players = resolveInfo(room?.players ?? []);
                const spectators = resolveInfo(Array.from(room?.spectators ?? new Set()));

                // Notify replaced connection in the same room (multi-tab detection)
                const replacedWs = this.replacedConnections.get(discordId);
                if (replacedWs && replacedWs.data.rooms.has(msg.roomId)) {
                    replacedWs.send(JSON.stringify({ type: "SESSION_REPLACED", roomId: msg.roomId }));
                    replacedWs.data.rooms.delete(msg.roomId);
                    replacedWs.unsubscribe(`room:${msg.roomId}`);
                }
                this.replacedConnections.delete(discordId);

                // Respond with JOIN_RESULT
                ws.send(JSON.stringify({
                    type: "JOIN_RESULT",
                    roomId: msg.roomId,
                    joinedAs: result.joinedAs,
                    roomStatus,
                    players,
                    spectators,
                    state,
                }));

                // Notify other room members
                const playerInfo: PlayerInfo = { discordId, username };
                this.publish(`room:${msg.roomId}`, {
                    type: "PLAYER_JOINED",
                    roomId: msg.roomId,
                    player: playerInfo,
                    joinedAs: result.joinedAs,
                });

                logger.info("web", `${discordId} joined room ${msg.roomId} as ${result.joinedAs}`);
                break;
            }

            case "LEAVE_ROOM": {
                this.roomManager.leaveRoom(msg.roomId, discordId);
                ws.unsubscribe(`room:${msg.roomId}`);
                ws.data.rooms.delete(msg.roomId);
                this.publish(`room:${msg.roomId}`, {
                    type: "PLAYER_LEFT",
                    roomId: msg.roomId,
                    playerId: discordId,
                });
                break;
            }

            case "GAME_ACTION": {
                const result = this.roomManager.handleAction(msg.roomId, discordId, msg.action);
                if (!result.ok) {
                    ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
                    return;
                }
                // game:updated event handler will dispatch views to players and spectators
                break;
            }

            case "FILL_ROOM": {
                if (role !== "admin") {
                    ws.send(JSON.stringify({ type: "ERROR", message: "Only admins can fill a room for solo testing" }));
                    return;
                }
                const result = this.roomManager.fillRoom(msg.roomId, discordId);
                if (!result.ok) {
                    ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
                    return;
                }
                logger.info("web", `Admin ${discordId} filled room ${msg.roomId} for solo testing`);
                break;
            }
        }
    }

    handleClose(ws: ServerWebSocket<WsConnectionData>): void {
        // If this is a replaced (displaced) connection, just clean up the registry and stop
        for (const [id, prevWs] of this.replacedConnections) {
            if (prevWs === ws) {
                this.replacedConnections.delete(id);
                return;
            }
        }

        for (const roomId of ws.data.rooms) {
            this.roomManager.leaveRoom(roomId, ws.data.session.discordId);
            this.publish(`room:${roomId}`, {
                type: "PLAYER_LEFT",
                roomId,
                playerId: ws.data.session.discordId,
            });
        }
        this.connections.delete(ws.data.session.discordId);
        this.publishRoomListUpdate();
    }

    private publish(channel: string, message: unknown): void {
        this.bunServer?.publish(channel, JSON.stringify(message));
    }

    private sendToPlayer(discordId: string, message: unknown): void {
        this.connections.get(discordId)?.send(JSON.stringify(message));
    }

    private publishRoomListUpdate(): void {
        this.publish("lobby", { type: "ROOM_LIST_UPDATE", rooms: this.roomManager.listRooms() });
    }
}

export const gameServer = new GameServer();
