// javascript
let ws = null; // will be created by connectWebSocket()
const pending = new Map(); // id -> { resolve, reject, timer }
const handlers = new Map(); // eventType -> handler(data) -> (value|Promise)

let timer = null;

// helper to attach message/error/close handlers to a socket
function setupSocketHandlers(socket) {
    socket.onmessage = (event) => {
        console.debug("SERVER MESSAGE:", event.data);
        let msg;
        try {
            msg = JSON.parse(event.data);
        } catch (e) {
            console.debug("Non-JSON message from server:", event.data, e);
            return;
        }

        const id = msg.id;
        const eventType = msg.event;
        const status = msg.status;
        const data = msg.data;

        if (id && typeof status === "string") {
            const entry = pending.get(id);
            if (!entry) return;
            clearTimeout(entry.timer);
            pending.delete(id);

            if (status === "success") {
                entry.resolve(data === undefined ? {} : data);
            } else {
                entry.reject(new Error(msg.error || "Server returned error"));
            }
            return;
        }

        if (id && eventType) {
            const handler = handlers.get(eventType);
            const sendResponse = (result) => {
                const response = {id: id, event: eventType, status: result};
                if (socket && socket.readyState === WebSocket.OPEN) {
                    socket.send(JSON.stringify(response));
                } else {
                    console.warn("Cannot send response, websocket not open");
                }
            };

            if (!handler) {
                // no handler: respond with an error object in data so server can fail it
                console.warn("No handler for event:", eventType);
                sendResponse({error: "No handler for event: " + eventType});
                return;
            }

            try {
                Promise.resolve(handler(data === undefined ? {} : data))
                    .then(_ => sendResponse("success"))
                    .catch(_ => sendResponse("error"));
            } catch (err) {
                sendResponse("error");
            }
        }
    };

    socket.onerror = (error) => {
        console.error("WebSocket Error:", error);
        if (timer) clearInterval(timer);
        for (const [id, entry] of pending.entries()) {
            clearTimeout(entry.timer);
            entry.reject(new Error("WebSocket error/closed"));
            pending.delete(id);
        }
        if (socket.readyState === WebSocket.OPEN) socket.close(1000, "Unexpected error.");
    };

    socket.onclose = (event) => {
        if (timer) clearInterval(timer);
        for (const [id, entry] of pending.entries()) {
            clearTimeout(entry.timer);
            entry.reject(new Error("WebSocket closed"));
            pending.delete(id);
        }
        if (event.wasClean) {
            console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
        } else {
            console.warn('Connection died unexpectedly.');
        }
        location.href = "/mainmenu";
    };
}

// connect/disconnect helpers
function connectWebSocket(url = null) {
    if (!url) {
        const loc = window.location;
        const protocol = loc.protocol === "https:" ? "wss:" : "ws:";
        url = protocol + "//" + loc.host + "/websocket";
    }
    if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
    if (ws && ws.readyState === WebSocket.CONNECTING) {
        // already connecting - return a promise that resolves on open
        return new Promise((resolve, reject) => {
            const prevOnOpen = ws.onopen;
            const prevOnError = ws.onerror;
            ws.onopen = (ev) => {
                if (prevOnOpen) prevOnOpen(ev);
                resolve();
            };
            ws.onerror = (err) => {
                if (prevOnError) prevOnError(err);
                reject(err);
            };
        });
    }

    ws = new WebSocket(url);
    setupSocketHandlers(ws);

    return new Promise((resolve, reject) => {
        ws.onopen = () => {
            console.log("WebSocket connection established!");
            // start heartbeat
            timer = setInterval(() => {
                if (ws && ws.readyState === WebSocket.OPEN) {
                    sendEventAndWait("ping", {}).then(
                        () => console.debug("PING RESPONSE RECEIVED"),
                    ).catch(
                        (err) => console.warn("PING ERROR:", err.message),
                    );
                    console.debug("PING SENT");
                }
            }, 5000);
            resolve();
        };

        ws.onerror = (err) => {
            reject(err);
        };
    });
}

function disconnectWebSocket(code = 1000, reason = "Client disconnect") {
    if (timer) {
        clearInterval(timer);
        timer = null;
    }
    if (ws) {
        try {
            ws.close(code, reason);
        } catch (e) {
        }
        ws = null;
    }
}

function sendEvent(eventType, eventData) {
    if (!ws || ws.readyState !== WebSocket.OPEN) {
        console.warn("WebSocket is not open. Unable to send message.");
        return;
    }
    const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
    const message = {id: id, event: eventType, data: eventData};
    ws.send(JSON.stringify(message));
    console.debug("SENT:", message);
}

function sendEventAndWait(eventType, eventData, timeoutMs = 10000) {
    if (!ws || ws.readyState !== WebSocket.OPEN) {
        return Promise.reject(new Error("WebSocket is not open"));
    }
    const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
    const message = {id: id, event: eventType, data: eventData};
    const p = new Promise((resolve, reject) => {
        const timerId = setTimeout(() => {
            if (pending.has(id)) {
                pending.delete(id);
                reject(new Error(`No response within ${timeoutMs}ms for id=${id}`));
            }
        }, timeoutMs);
        pending.set(id, {resolve, reject, timer: timerId});
    });
    ws.send(JSON.stringify(message));
    console.debug("SENT (await):", message);
    return p;
}

function onEvent(eventType, handler) {
    handlers.set(eventType, handler);
}

globalThis.sendEvent = sendEvent;
globalThis.sendEventAndWait = sendEventAndWait;
globalThis.onEvent = onEvent;
globalThis.connectWebSocket = connectWebSocket;
globalThis.disconnectWebSocket = disconnectWebSocket;
globalThis.isWebSocketConnected = () => !!ws && ws.readyState === WebSocket.OPEN;
