/*
    ! /servers/websocket/server.js – Central Websocket Server Manager
    ? This module orchestrates the websocket lifecycle, handles connections, heartbeats and actions
*/

// % import nodejs packages
const path = require('path');
const fs = require('fs');
const http = require('http');
const WebSocket = require('ws');

// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..');
const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js'));
const connectionManager = require(path.resolve(dirs.websocketDir, 'connections.js'));

// % declare action directories
const wsActionsDir = path.resolve(dirs.websocketDir, 'actions');

// % declare reference for actions
const actions = {};

// % declare references for the websocket server
const ip = process.env.INTERNAL_HOST_IP;
const port = process.env.INTERNAL_WEBSOCKET_PORT;
let server = null;

// $ load all websocket actions recursively
// ? The loader is loading all actions with a dot notation
// ?? Usage: actionName || folder.actionName || folder.folder2.actionName ....
async function loadActions(dir = wsActionsDir, prefix = '') {
    // ~ reset the action container to ensure a clean state during reloads
    for (const key in actions) delete actions[key];

    // % read all content synchronously
    const files = fs.readdirSync(dir);

    // ~ loop through each file or folder found in the directory
    for (const file of files) {
        // % construct the absolute path to the current item
        const fullPath = path.resolve(dir, file);

        // ~ retrieve file system metadata
        const stat = fs.statSync(fullPath);

        // ~ check if item is a folder
        if (stat.isDirectory()) {
            // ~ recurse into subdirs and append the folder name to the prefix
            loadActions(fullPath, prefix + file + '.');
        } else if (file.endsWith('.js')) {
            // ~ generate a unique action name by stripping the file extension and include the prefix
            const actionName = prefix + file.replace('.js', '');

            // ~ invalidate the require cache to allow loading the updated code
            delete require.cache[require.resolve(fullPath)];

            // ~ load the fresh module version into the actions registry
            actions[actionName] = require(fullPath);
        }
    }
}

// $ function to start the websocket server
async function start() {
    // ~ initially load all websocket actions
    await loadActions();

    try {
        // ~ set up a new websocket server
        const wss = new WebSocket.Server({
            host: ip,
            port: port
        });

        wss.on('connection', async (socket, req) => {
            // ~ authorize connection
            const url = new URL(req.url, `http://${req.headers.host}`);
            const key = url.searchParams.get('key');
            const codename = url.searchParams.get('codename');
            if (key !== process.env.INTERNAL_SECRET) {
                socket.close(1008, 'Unauthorized');
                return;
            }

            // ~ get connection id and establish connection
            const id = await connectionManager.add(socket, codename);
            connectionManager.sendTo(codename, id);

            // ~ register websocket messages
            socket.on('message', async (data) => {
                // ~ parse json data
                const msg = JSON.parse(data);

                // % get user id and validate
                const valid = connectionManager.get(id);
                if (valid.socket.readyState !== valid.socket.OPEN) {
                    connectionManager.remove(id);
                    return;
                };

                // % declare action type (e.g. folder.actionName)
                const type = msg.type;
                if (!type) {
                    connectionManager.send(id, 'Action value is empty. Please declare an action value.');
                    return;
                };

                // ~ set action handler
                const actionHandler = actions[msg.type];
                if (!actionHandler) {
                    connectionManager.send(id, `Action ${actionHandler} not found.`);
                    return;
                }

                // ~ send action to handler
                try {
                    await actionHandler(connectionManager, msg.data, id);
                } catch(e) {
                    console.error('Error while trying to execute the websocket action!', e);
                    return;
                }
            });
        });

        // ~ start the heartbeat system with the connection manager
        connectionManager.startBeat();
    } catch(e) {
        console.error('Error while trying to start the websocket server', e);
        connectionManager.stopBeat();
    }
}

// § export websocket server and functions
module.exports = { start, loadActions };