/**
 * WebSocket Server Implementation
 * Provides real-time updates for simulator data and events
 */
import { Server } from "socket.io";
import { createLogger } from "../core/index.js";
let logger; // Will be initialized when needed
class WebSocketServer {
    io = null;
    httpServer;
    clients;
    simulatorSubscriptions; // simulatorId -> Set of socket IDs
    broadcastHook;
    constructor(httpServer) {
        // Initialize logger if not already done
        if (!logger) {
            logger = createLogger("WebSocket");
        }
        this.httpServer = httpServer;
        this.clients = new Map();
        this.simulatorSubscriptions = new Map();
    }
    initialize(opts) {
        if (this.io) {
            if (opts?.broadcastHook) {
                this.broadcastHook = opts.broadcastHook;
                logger.debug("WebSocket broadcast hook updated");
            }
            return;
        }
        this.io = new Server(this.httpServer, {
            cors: {
                origin: "*", // Allow all origins for local development
                methods: ["GET", "POST"],
            },
            transports: ["websocket", "polling"],
        });
        if (opts?.broadcastHook) {
            this.broadcastHook = opts.broadcastHook;
        }
        this.setupEventHandlers();
        logger.info("WebSocket server initialized");
    }
    /**
     * Get the underlying Socket.IO server instance
     */
    getIO() {
        return this.io;
    }
    setupEventHandlers() {
        if (!this.io)
            return;
        this.io.on("connection", (socket) => {
            // Only log in debug mode to avoid spam
            if (process.env.LOG_LEVEL?.toLowerCase() === "debug") {
                logger.debug(`Client connected: ${socket.id}`);
            }
            this.clients.set(socket.id, {
                id: socket.id,
                connectedAt: new Date(),
                subscriptions: new Set(),
            });
            // Send initial connection confirmation
            socket.emit("connected", {
                id: socket.id,
                timestamp: Date.now(),
            });
            // Handle simulator subscriptions
            socket.on("subscribe:simulator", (simulatorId) => {
                this.subscribeToSimulator(socket, simulatorId);
            });
            socket.on("unsubscribe:simulator", (simulatorId) => {
                this.unsubscribeFromSimulator(socket, simulatorId);
            });
            // Handle disconnection
            socket.on("disconnect", () => {
                // Only log in debug mode to avoid spam
                if (process.env.LOG_LEVEL?.toLowerCase() === "debug") {
                    logger.debug(`Client disconnected: ${socket.id}`);
                }
                this.handleDisconnect(socket);
            });
            // Handle errors
            socket.on("error", (_error) => {
                logger.error(`Socket error for ${socket.id}:`, _error);
            });
        });
    }
    subscribeToSimulator(socket, simulatorId) {
        const client = this.clients.get(socket.id);
        if (!client)
            return;
        // Add to client's subscriptions
        client.subscriptions.add(simulatorId);
        // Add to simulator's subscriber list
        if (!this.simulatorSubscriptions.has(simulatorId)) {
            this.simulatorSubscriptions.set(simulatorId, new Set());
        }
        const subscribers = this.simulatorSubscriptions.get(simulatorId);
        if (subscribers) {
            subscribers.add(socket.id);
        }
        // Join socket.io room for efficient broadcasting
        socket.join(`simulator:${simulatorId}`);
        logger.debug(`Client ${socket.id} subscribed to simulator ${simulatorId}`);
    }
    unsubscribeFromSimulator(socket, simulatorId) {
        const client = this.clients.get(socket.id);
        if (!client)
            return;
        // Remove from client's subscriptions
        client.subscriptions.delete(simulatorId);
        // Remove from simulator's subscriber list
        const subscribers = this.simulatorSubscriptions.get(simulatorId);
        if (subscribers) {
            subscribers.delete(socket.id);
            if (subscribers.size === 0) {
                this.simulatorSubscriptions.delete(simulatorId);
            }
        }
        // Leave socket.io room
        socket.leave(`simulator:${simulatorId}`);
        logger.debug(`Client ${socket.id} unsubscribed from simulator ${simulatorId}`);
    }
    handleDisconnect(socket) {
        const client = this.clients.get(socket.id);
        if (!client)
            return;
        // Clean up all subscriptions
        client.subscriptions.forEach((simulatorId) => {
            const subscribers = this.simulatorSubscriptions.get(simulatorId);
            if (subscribers) {
                subscribers.delete(socket.id);
                if (subscribers.size === 0) {
                    this.simulatorSubscriptions.delete(simulatorId);
                }
            }
        });
        // Remove client
        this.clients.delete(socket.id);
    }
    /**
     * Broadcast an event to all connected clients
     */
    broadcast(event, data) {
        if (!this.io)
            return;
        // Allow tests/consumers to observe outbound broadcasts
        if (this.broadcastHook) {
            try {
                this.broadcastHook(event, data);
            }
            catch (_err) {
                // ignore hook errors
            }
        }
        // Create typed message based on event
        let message;
        // Timestamp removed - not needed in simplified message structure
        switch (event) {
            case "simulator:started":
            case "simulator:stopped":
            case "simulator:update":
                message = {
                    type: event,
                    data: data,
                };
                break;
            case "template:created":
            case "template:updated":
            case "template:deleted":
                message = {
                    type: event,
                    data: data,
                };
                break;
            case "simulator:data":
                message = {
                    type: "data:update",
                    data: {
                        simulatorId: data.simulatorId,
                        values: data.values || { [data.address]: data.value },
                    },
                };
                break;
            default:
                message = {
                    type: event,
                    data: data,
                };
        }
        // Broadcast to all clients
        this.io.emit(event, message.data);
        // If simulator-specific, also send to room
        if (data.simulatorId) {
            this.io.to(`simulator:${data.simulatorId}`).emit(event, message.data);
        }
        logger.debug(`Broadcast ${event} to ${this.clients.size} clients`);
    }
    /**
     * Send event to specific simulator subscribers
     */
    broadcastToSimulator(simulatorId, event, data) {
        if (!this.io)
            return;
        const message = {
            type: "data:update",
            data: {
                simulatorId,
                values: data,
            },
        };
        this.io.to(`simulator:${simulatorId}`).emit(event, message.data);
        const subscriberCount = this.simulatorSubscriptions.get(simulatorId)?.size || 0;
        logger.debug(`Broadcast ${event} to ${subscriberCount} subscribers of simulator ${simulatorId}`);
    }
    /**
     * Get statistics about connected clients
     */
    getStats() {
        const stats = {
            totalClients: this.clients.size,
            totalSubscriptions: 0,
            simulatorSubscriptions: {},
        };
        // Count total subscriptions
        this.clients.forEach((client) => {
            stats.totalSubscriptions += client.subscriptions.size;
        });
        // Count per-simulator subscriptions
        this.simulatorSubscriptions.forEach((subscribers, simulatorId) => {
            stats.simulatorSubscriptions[simulatorId] = subscribers.size;
        });
        return stats;
    }
    /**
     * Get list of connected clients
     */
    getClients() {
        return Array.from(this.clients.values());
    }
    /**
     * Check if server is initialized
     */
    isInitialized() {
        return this.io !== null;
    }
    /**
     * Shutdown the WebSocket server
     */
    async shutdown() {
        if (this.io) {
            // Disconnect all clients
            this.io.disconnectSockets();
            // Close the server
            await new Promise((resolve) => {
                this.io.close(() => {
                    logger.info("WebSocket server shut down");
                    resolve();
                });
            });
            this.io = null;
        }
        // Clear all data
        this.clients.clear();
        this.simulatorSubscriptions.clear();
    }
}
// Singleton instance
let wsServer = null;
/**
 * Create WebSocket server instance
 */
export function createWebSocketServer(httpServer, opts) {
    // Initialize logger if not already done
    if (!logger) {
        logger = createLogger("WebSocket");
    }
    if (!wsServer || opts?.reset) {
        wsServer = new WebSocketServer(httpServer);
        wsServer.initialize({ broadcastHook: opts?.broadcastHook });
    }
    else if (opts?.broadcastHook) {
        // Reconfigure hook without rebuilding the IO server
        wsServer.initialize({ broadcastHook: opts.broadcastHook });
    }
    return wsServer;
}
/**
 * Get existing WebSocket server instance
 */
export function getWebSocketServer() {
    return wsServer;
}
/**
 * Export the class for type usage
 */
export { WebSocketServer };
//# sourceMappingURL=websocketServer.js.map