'use strict';

const http = require('node:http');
const express = require('express');
const {Server} = require('socket.io');

const port = parseIntegerEnv('PORT', 3000);
const socketPath = process.env.SOCKET_IO_PATH || '/socket.io';
const frontendOrigin = (process.env.FRONTEND_ORIGIN || '*')
    .split(',')
    .map((origin) => origin.trim())
    .filter(Boolean);

const app = express();
app.use(express.json({limit: '1mb'}));

app.get('/health', (_req, res) => {
    res.json({
        ok: true,
        service: 'websocket',
        socketPath,
        timestamp: new Date().toISOString(),
    });
});

app.get('/ws/health', (_req, res) => {
    res.json({ok: true, timestamp: new Date().toISOString()});
});

const server = http.createServer(app);

const io = new Server(server, {
    path: socketPath,
    cors: frontendOrigin.includes('*')
        ? {origin: true, credentials: true}
        : {origin: frontendOrigin, credentials: true},
});

io.on('connection', (socket) => {
    console.log(`[socket] connected ${socket.id}`);

    socket.emit('server:ready', {
        socketId: socket.id,
        timestamp: new Date().toISOString(),
    });

    socket.on('room:join', (payload, ack) => {
        const room = toRoomName(payload?.room ?? payload?.gameId);
        if (!room) {
            ackError(ack, 'Invalid room or gameId.');
            return;
        }

        socket.join(room);
        ackOk(ack, {room});
    });

    socket.on('room:leave', (payload, ack) => {
        const room = toRoomName(payload?.room ?? payload?.gameId);
        if (!room) {
            ackError(ack, 'Invalid room or gameId.');
            return;
        }

        socket.leave(room);
        ackOk(ack, {room});
    });

    socket.on('room:event', (payload, ack) => {
        const room = toRoomName(payload?.room ?? payload?.gameId);
        if (!room) {
            ackError(ack, 'Invalid room or gameId.');
            return;
        }

        const eventName = typeof payload?.event === 'string' && payload.event.trim() !== ''
            ? payload.event.trim()
            : 'room:event';

        io.to(room).emit(eventName, payload?.data ?? {});
        ackOk(ack, {room, event: eventName});
    });

    socket.on('game:throw', (payload, ack) => {
        emitGameEvent('game:throw', payload, ack);
    });

    socket.on('game:turn', (payload, ack) => {
        emitGameEvent('game:turn', payload, ack);
    });

    socket.on('game:state', (payload, ack) => {
        emitGameEvent('game:state', payload, ack);
    });

    socket.on('disconnect', (reason) => {
        console.log(`[socket] disconnected ${socket.id} (${reason})`);
    });
});

server.listen(port, () => {
    console.log(
        `[websocket] listening on :${port} (path=${socketPath})`,
    );
});

const shutdown = () => {
    console.log('[websocket] shutting down');
    io.close(() => {
        server.close(() => process.exit(0));
    });
};

process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);

function parseIntegerEnv(name, fallback) {
    const raw = process.env[name];
    const value = Number.parseInt(raw ?? `${fallback}`, 10);
    if (Number.isNaN(value) || value <= 0) {
        throw new Error(`Invalid ${name}: ${raw}`);
    }

    return value;
}

function normalizeBaseUrl(url) {
    return url.replace(/\/+$/, '');
}

function toRoomName(value) {
    if (typeof value !== 'string') {
        return '';
    }

    const room = value.trim();
    if (room === '' || room.length > 128) {
        return '';
    }

    return room;
}

function toGameRoomName(gameId) {
    const normalizedId = typeof gameId === 'string' ? gameId.trim() : '';
    if (normalizedId === '' || normalizedId.length > 128) {
        return '';
    }

    return `game:${normalizedId}`;
}

function ackOk(ack, payload) {
    if (typeof ack === 'function') {
        ack({ok: true, ...payload});
    }
}

function ackError(ack, message) {
    if (typeof ack === 'function') {
        ack({ok: false, error: message});
    }
}

function emitGameEvent(eventName, payload, ack) {
    const room = toGameRoomName(payload?.gameId);
    if (!room) {
        ackError(ack, 'Invalid gameId.');
        return;
    }

    io.to(room).emit(eventName, payload ?? {});
    ackOk(ack, {
        room,
        event: eventName,
        gameId: payload?.gameId ?? null,
    });
}
