const http = require('node:http');
const path = require('node:path');

const {
    BRIDGE_PROTOCOL_VERSION,
    BRIDGE_SERVER_VERSION,
    DEFAULT_REMOTE_HOST,
    DEFAULT_REMOTE_PORT,
    PORT_SCAN_RANGE,
    REMOTE_ASSETS_PREFIX,
    REMOTE_BASE_PATH,
    REMOTE_HEALTH_PATH,
    REMOTE_INSTALLED_APPS_API_PATH,
    REMOTE_WS_PATH,
    WS_OPCODE,
} = require('./constants');
const { createBridgeLogger } = require('./logger');
const {
    normalizeRemoteCommandAction,
    normalizeRemoteCommandResult,
} = require('./normalizers');
const { createBridgeState } = require('./bridge-state');
const { validateClientMessage, validateSetValueTarget } = require('./protocol-router');
const { getAdvertisedUrls, serveFile } = require('./server-files');
const { cloneValue, isValidPort, safeString } = require('./utils');
const { closeClient, createAcceptKey, makeFrame, parseFrame, sendJson } = require('./websocket-codec');
import type { BridgeOptions } from './types';

function createBridgeServer(options: BridgeOptions = {}) {
    const preferredPort = Number(options.port || process.env.WAND_REMOTE_PORT || DEFAULT_REMOTE_PORT);
    let port = isValidPort(preferredPort) ? preferredPort : DEFAULT_REMOTE_PORT;
    const maxPort = Number(options.maxPort || process.env.WAND_REMOTE_MAX_PORT || port + PORT_SCAN_RANGE);
    const host = options.host || process.env.WAND_REMOTE_HOST || DEFAULT_REMOTE_HOST;
    const panelRoot = options.panelRoot || path.dirname(__dirname);
    const clients = new Set<any>();
    const log = createBridgeLogger(options);
    let advertisedUrls: string[] = [];
    let setValueHandler: any = null;
    let commandHandler: any = null;
    let listening = false;
    const bridgeState = createBridgeState({
        clients,
        log,
        getServerInfo: () => ({
            advertisedUrls,
            installedAppsApiPath: REMOTE_INSTALLED_APPS_API_PATH,
            listening,
            remoteUrl: globalThis.__wandRemoteBridgeUrl,
        }),
    });

    function setAdvertisedPort(nextPort) {
        port = nextPort;
        advertisedUrls = getAdvertisedUrls(port);
        globalThis.__wandRemoteBridgeUrl = advertisedUrls.find((entry) => !entry.includes('localhost')) || advertisedUrls[0];
    }

    function setHandler(handler) {
        setValueHandler = typeof handler === 'function' ? handler : null;
    }

    function setCommandHandler(handler) {
        commandHandler = typeof handler === 'function' ? handler : null;
    }

    function handleRequest(request, response) {
        const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);

        if (url.pathname === '/' || url.pathname === '') {
            response.writeHead(302, { Location: '/remote/' });
            response.end();
            return;
        }

        if (url.pathname === REMOTE_BASE_PATH.slice(0, -1)) {
            response.writeHead(302, { Location: REMOTE_BASE_PATH });
            response.end();
            return;
        }

        if (url.pathname === REMOTE_BASE_PATH) {
            serveFile(response, path.join(panelRoot, 'index.html'));
            return;
        }

        if (url.pathname === REMOTE_HEALTH_PATH) {
            response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
            response.end(JSON.stringify(bridgeState.buildHealthPayload()));
            return;
        }

        if (url.pathname === REMOTE_INSTALLED_APPS_API_PATH) {
            response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
            response.end(JSON.stringify(bridgeState.buildInstalledAppsDebugPayload(), null, 2));
            return;
        }

        if (url.pathname.startsWith(REMOTE_ASSETS_PREFIX)) {
            serveFile(response, path.join(panelRoot, url.pathname.replace(REMOTE_BASE_PATH, '')));
            return;
        }

        response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
        response.end('Not found');
    }

    async function handleRemoteCommandMessage(client, message) {
        const action = normalizeRemoteCommandAction(message.payload?.action);
        const gameId = typeof message.payload?.gameId === 'string' || typeof message.payload?.gameId === 'number'
            ? String(message.payload.gameId)
            : null;
        const titleId = typeof message.payload?.titleId === 'string' || typeof message.payload?.titleId === 'number'
            ? String(message.payload.titleId)
            : null;

        if (!action) {
            sendJson(client, 'error', {
                code: 'invalid_command',
                message: 'Unknown remote command.',
            }, message.requestId ?? null);
            return;
        }

        const fallback = { action, gameId, titleId };
        if (action === 'launch' && !gameId) {
            sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({
                ok: false,
                error: {
                    code: 'invalid_game',
                    message: 'A game id is required to launch a trainer.',
                },
            }, fallback), message.requestId ?? null);
            return;
        }

        if (!commandHandler) {
            sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({
                ok: false,
                error: {
                    code: 'bridge_not_ready',
                    message: 'The local bridge is not ready to execute remote game commands yet.',
                },
            }, fallback), message.requestId ?? null);
            return;
        }

        try {
            const result = await Promise.resolve(commandHandler({ action, gameId, titleId }));
            sendJson(client, 'remote_command_result', normalizeRemoteCommandResult(result, fallback), message.requestId ?? null);
        } catch (error) {
            sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({
                ok: false,
                error: {
                    code: 'command_failed',
                    message: error instanceof Error ? error.message : 'Failed to execute the remote command.',
                },
            }, fallback), message.requestId ?? null);
        }
    }

    async function handleSetValueMessage(client, message) {
        const currentSnapshot = bridgeState.snapshot;
        const validation = validateSetValueTarget(message, currentSnapshot);
        if (!validation.ok) {
            sendJson(client, 'set_value_result', {
                ok: false,
                trainerId: currentSnapshot?.trainerMeta?.trainer?.trainerId || '',
                target: safeString(message.payload?.target),
                error: validation.error,
            }, message.requestId ?? null);
            return;
        }
        const { target } = validation;

        if (!setValueHandler) {
            sendJson(client, 'set_value_result', {
                ok: false,
                trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
                target,
                error: {
                    code: 'bridge_not_ready',
                    message: 'The local bridge is not ready to write trainer values yet.',
                },
            }, message.requestId ?? null);
            return;
        }

        let result = false;
        try {
            result = await Promise.resolve(setValueHandler({
                trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
                target,
                value: cloneValue(validation.value),
                cheatId: typeof message.payload?.cheatId === 'string' ? message.payload.cheatId : undefined,
            }));
        } catch (error) {
            sendJson(client, 'set_value_result', {
                ok: false,
                trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
                target,
                error: {
                    code: 'set_failed',
                    message: error instanceof Error ? error.message : 'Failed to set trainer value.',
                },
            }, message.requestId ?? null);
            return;
        }

        if (!result) {
            sendJson(client, 'set_value_result', {
                ok: false,
                trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
                target,
                error: {
                    code: 'set_rejected',
                    message: 'The trainer rejected the requested value.',
                },
            }, message.requestId ?? null);
            return;
        }

        sendJson(client, 'set_value_result', {
            ok: true,
            trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
            target,
        }, message.requestId ?? null);
    }

    async function handleClientMessage(client, message) {
        const validation = validateClientMessage(message, client.handshaken);
        if (!validation.ok) {
            sendJson(client, 'error', validation.error, message?.requestId ?? null);
            return;
        }

        if (message?.type === 'hello') {
            client.handshaken = true;
            sendJson(client, 'hello_ack', {
                sessionId: `sess_${Date.now()}`,
                accepted: true,
                serverVersion: BRIDGE_SERVER_VERSION,
                protocolVersion: BRIDGE_PROTOCOL_VERSION,
                remoteUrl: globalThis.__wandRemoteBridgeUrl,
                advertisedUrls,
            }, message.requestId ?? null);
            bridgeState.sendSnapshot(client);
            return;
        }

        if (message?.type === 'remote_command') {
            await handleRemoteCommandMessage(client, message);
            return;
        }

        if (message?.type === 'set_value') {
            await handleSetValueMessage(client, message);
        }
    }

    function bindSocket(socket) {
        const client = {
            socket,
            buffer: Buffer.alloc(0),
            closed: false,
            handshaken: false,
        };

        clients.add(client);

        socket.on('data', async (chunk) => {
            try {
                client.buffer = Buffer.concat([client.buffer, chunk]);

                while (client.buffer.length > 0) {
                    const frame = parseFrame(client.buffer);
                    if (!frame) {
                        return;
                    }

                    client.buffer = client.buffer.subarray(frame.bytesConsumed);

                    if (!frame.fin) {
                        closeClient(client, 1003, 'Fragmented frames are not supported.');
                        return;
                    }

                    if (frame.opcode === WS_OPCODE.CLOSE) {
                        closeClient(client, 1000, 'Closing');
                        return;
                    }

                    if (frame.opcode === WS_OPCODE.PING) {
                        client.socket.write(makeFrame(WS_OPCODE.PONG, frame.payload));
                        continue;
                    }

                    if (frame.opcode !== WS_OPCODE.TEXT) {
                        continue;
                    }

                    await handleClientMessage(client, JSON.parse(frame.payload.toString('utf8')));
                }
            } catch (error) {
                sendJson(client, 'error', {
                    code: 'invalid_message',
                    message: error instanceof Error ? error.message : 'Failed to process client message.',
                });
            }
        });

        socket.on('close', () => {
            client.closed = true;
            clients.delete(client);
        });

        socket.on('end', () => {
            client.closed = true;
            clients.delete(client);
        });

        socket.on('error', (error) => {
            client.closed = true;
            clients.delete(client);
            log('warn', 'WebSocket client error.', error);
        });
    }

    function handleUpgrade(request, socket) {
        const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
        if (url.pathname !== REMOTE_WS_PATH) {
            socket.destroy();
            return;
        }

        const key = request.headers['sec-websocket-key'];
        if (typeof key !== 'string' || !key) {
            socket.destroy();
            return;
        }

        socket.write([
            'HTTP/1.1 101 Switching Protocols',
            'Upgrade: websocket',
            'Connection: Upgrade',
            `Sec-WebSocket-Accept: ${createAcceptKey(key)}`,
            '',
            '',
        ].join('\r\n'));

        bindSocket(socket);
    }

    function listen(nextPort) {
        setAdvertisedPort(nextPort);
        server.listen(port, host);
    }

    setAdvertisedPort(port);
    log('info', `Bridge starting (pid=${process.pid}, panelRoot=${panelRoot}, preferredPort=${port}, host=${host})`);
    globalThis.__wandRemoteBridgeLogFile = log.file;

    const server = http.createServer(handleRequest);
    server.on('upgrade', handleUpgrade);
    server.on('error', (error) => {
        if (!listening && error && error.code === 'EADDRINUSE' && port < maxPort) {
            const nextPort = port + 1;
            log('warn', `Port ${port} is busy, trying ${nextPort}.`);
            listen(nextPort);
            return;
        }

        log('warn', `Bridge server error on ${host}:${port}.`, error);
    });
    server.on('listening', () => {
        listening = true;
        log('info', `Listening on ${globalThis.__wandRemoteBridgeUrl}`);
    });

    listen(port);

    return {
        get advertisedUrls() {
            return advertisedUrls.slice();
        },
        get listening() {
            return listening;
        },
        get remoteUrl() {
            return globalThis.__wandRemoteBridgeUrl;
        },
        close() {
            for (const client of clients) {
                closeClient(client);
            }
            clients.clear();
            bridgeState.clear();
            listening = false;
            server.close();
        },
        setCommandHandler,
        setHandler,
        sync: bridgeState.sync,
        syncGameStatus: bridgeState.syncGameStatus,
        syncInstalledApps: bridgeState.syncInstalledApps,
        valueChanged: bridgeState.valueChanged,
    };
}

module.exports = {
    createBridgeServer,
};
