import { SocketRoute, type RedWebSocket, type RedWebRequest } from 'redweb';
import { match } from './contract.js';
import { Games } from './game.js';

export function matchRoute(account: (request: Pick<RedWebRequest, 'headers'>) => string | undefined,
    origins: (origin: string | undefined, request: import('node:http').IncomingMessage) => boolean) {
    const games = new Games();
    const members = new Map<RedWebSocket, { room: string; account: string }>();
    const identity = (socket: RedWebSocket) => {
        const current = account(socket.context!.request);
        if (!current || current !== socket.context!.principal) {
            socket.close(1008, 'Sign in again.');
            throw new Error('Sign in again.');
        }
        return current;
    };
    async function publish(room: string, sender?: RedWebSocket, requestId?: string) {
        const peers = [...members].filter(([, member]) => member.room === room);
        const valid = peers.filter(([socket, member]) => {
            if (socket.readyState === 1 && account(socket.context!.request) === member.account) return true;
            socket.close(1008, 'Sign in again.');
            return false;
        });
        const online = [...new Set(valid.map(([, member]) => member.account))];
        await Promise.all(valid.map(([socket, member]) => match.send(socket, 'state', {
            ...games.resume(room, member.account).snapshot(member.account), online, room,
        }, socket === sender ? { requestId } : undefined)));
    }
    async function attempt(socket: RedWebSocket, requestId: string | undefined, operation: () => string) {
        try { await publish(operation(), socket, requestId); }
        catch (error) { await match.send(socket, 'notice', { text: (error as Error).message }, { requestId }); }
    }
    function enter(socket: RedWebSocket, room: string, resume: boolean) {
        const user = identity(socket);
        const previous = members.get(socket);
        if (previous && previous.room !== room) throw new Error('Reload before joining another room.');
        if (resume) games.resume(room, user);
        else games.join(room, user);
        members.set(socket, { room, account: user });
        return room;
    }
    const Join = match.handler('join', (socket, { room }, message) => attempt(socket, message.requestId, () => enter(socket, room, false)));
    const Resume = match.handler('resume', (socket, { room }, message) => attempt(socket, message.requestId, () => enter(socket, room, true)));
    const Move = match.handler('move', (socket, { cell, revision }, message) => attempt(socket, message.requestId, () => {
        const user = identity(socket);
        const member = members.get(socket);
        if (!member) throw new Error('Join a room first.');
        games.resume(member.room, user).move(user, cell, revision);
        return member.room;
    }));

    return class MatchRoute extends SocketRoute {
        constructor() {
            super({ path: '/match', handlers: [Join, Move, Resume], protocol: match.protocol,
                admission: { authenticate: request => account(request) ?? false, origins },
                orderedMessages: true, allowDuplicateConnections: true, logger: null,
                heartbeat: { intervalMs: 15000, timeoutMs: 10000 },
                websocketOptions: { maxPayload: 4096 },
                limits: { maxConnections: 100, maxPendingMessages: 16, maxBufferedBytes: 65536,
                    messageRate: { capacity: 20, refillPerSecond: 5, action: 'disconnect' } },
            });
        }
        async connectionCloseCallback(socket: RedWebSocket) {
            const member = members.get(socket);
            members.delete(socket);
            if (member) await publish(member.room);
        }
    };
}
