class Comms {
    constructor(userName) {
        if (!userName) {
            console.error('Comms: Cannot initialize Socket.IO without user_name.');
            return;
        }

        this.connectionOpen = false;
        this.socket = null;  // Socket.IO client instance
        this.eventHandlers = {};  // Event handlers for message types

        // Callback collections that will receive various updates.
        this.candleUpdateCallbacks = [];
        this.candleCloseCallbacks = [];
        this.indicatorUpdateCallbacks = [];

        // Initialize the message queue
        this.messageQueue = [];
        this.maxQueueSize = 250;

        // Save the userName
        this.userName = userName;
        this.baseUrl = window.location.origin;

        // EDM (Exchange Data Manager) configuration
        this.edmRestUrl = null;
        this.edmWsUrl = null;
        this.edmEnabled = false;
        this.edmWebSocket = null;

        // Fetch EDM configuration
        this._fetchEdmConfig();

        // Initialize the socket
        this._initializeSocket();
    }

    /**
     * Fetches EDM configuration from the server.
     */
    async _fetchEdmConfig() {
        try {
            const response = await fetch(`${this.baseUrl}/api/edm-config`);
            if (response.ok) {
                const config = await response.json();
                this.edmRestUrl = config.rest_url;
                this.edmWsUrl = config.ws_url;
                this.edmEnabled = config.enabled;
                console.log('EDM config loaded:', config);
            }
        } catch (error) {
            console.warn('Failed to fetch EDM config, using legacy mode:', error);
            this.edmEnabled = false;
        }
    }

    /**
     * Initialize the Socket.IO connection.
     */
    _initializeSocket() {
        // Initialize Socket.IO client with query parameter
        this.socket = io(this.baseUrl, {
            query: { 'user_name': this.userName },
            transports: ['websocket'],  // Optional: Force WebSocket transport
            autoConnect: true,
            reconnection: true,
            reconnectionAttempts: Infinity,
            reconnectionDelay: 1000
        });

        // Handle connection events
        this.socket.on('connect', () => {
            console.log('Socket.IO: Connected to server');
            this.connectionOpen = true;

            // Flush the message queue
            this._flushMessageQueue();
        });

        this.socket.on('disconnect', (reason) => {
            console.log(`Socket.IO: Disconnected from server. Reason: ${reason}`);
            this.connectionOpen = false;
        });

        this.socket.on('connect_error', (error) => {
            console.error('Socket.IO: Connection error:', error);
        });

        // Handle incoming messages
        this.socket.on('message', (data) => {
            if (data.reply === 'connected') {
                console.log('Socket.IO: Connection established:', data.data);
            } else if (data.reply === 'error') {
                console.error('Socket.IO: Authentication error:', data.data);
                // Optionally, handle authentication errors (e.g., redirect to login)
            } else if (data.reply === 'progress') {
                // Handle progress updates specifically
                console.log('Progress update received:', data.data.progress);
                this.emit('backtest_progress', data.data);
            } else {
                // Emit the event to registered handlers
                this.emit(data.reply, data.data);
            }
        });

        // Handle strategy execution events (tick_complete, trade_executed, etc.)
        this.socket.on('strategy_events', (data) => {
            console.log('Strategy events received:', data);
            this.emit('strategy_events', data);
        });

        // Handle trade update events from execution loop
        this.socket.on('trade_update', (data) => {
            console.log('Trade update received:', data);
            this.emit('trade_update', data);
        });
    }

    /**
     * Flushes the message queue by sending all queued messages.
     */
    _flushMessageQueue() {
        // Prioritize control/CRUD messages first, keep only the latest candle_data.
        const queued = this.messageQueue.splice(0);
        const priority = [];
        let latestCandleMessage = null;

        queued.forEach((msg) => {
            if (msg.messageType === 'candle_data') {
                latestCandleMessage = msg;
            } else {
                priority.push(msg);
            }
        });

        const messagesToSend = latestCandleMessage
            ? [...priority, latestCandleMessage]
            : priority;

        messagesToSend.forEach(({ messageType, data }) => {
            this.socket.emit('message', {
                message_type: messageType,
                data: {
                    ...data,
                    user_name: this.userName
                }
            });
            console.log(`Comms: Sent queued message-> ${JSON.stringify({ messageType, data })}`);
        });
    }

    _enqueueMessage(messageType, data) {
        if (messageType === 'candle_data') {
            // Candle data is high-frequency. Keep only the latest unsent candle update.
            const existingIndex = this.messageQueue.findIndex(msg => msg.messageType === 'candle_data');
            if (existingIndex !== -1) {
                this.messageQueue[existingIndex] = { messageType, data };
                return;
            }
        }

        this.messageQueue.push({ messageType, data });

        // Prevent unbounded queue growth while disconnected.
        while (this.messageQueue.length > this.maxQueueSize) {
            const candleIndex = this.messageQueue.findIndex(msg => msg.messageType === 'candle_data');
            if (candleIndex !== -1) {
                this.messageQueue.splice(candleIndex, 1);
            } else {
                this.messageQueue.shift();
            }
        }
    }

    /**
     * Register a callback function to receive updates for a specific target.
     * @param {string} target - The target for the callback ('candle_update', 'candle_close', or 'indicator_update').
     * @param {function} callbackFunc - The callback function to register.
     */
    registerCallback(target, callbackFunc) {
        const callbackMap = {
            'candle_update': this.candleUpdateCallbacks,
            'candle_close': this.candleCloseCallbacks,
            'indicator_update': this.indicatorUpdateCallbacks
        };

        const callbackList = callbackMap[target];
        if (callbackList) {
            callbackList.push(callbackFunc);
        } else {
            console.log('Comms: Invalid target for callback');
        }
    }

    /**
     * Register an event handler for a specific message type.
     * @param {string} messageType - The type of the message to handle.
     * @param {function} handler - The handler function to register.
     */
    on(messageType, handler) {
        if (!this.eventHandlers[messageType]) {
            this.eventHandlers[messageType] = [];
        }
        this.eventHandlers[messageType].push(handler);
    }

    /**
     * Emit an event to all registered handlers.
     * @param {string} messageType - The type of the message.
     * @param {Object} data - The data to pass to the handlers.
     */
    emit(messageType, data) {
        const handlers = this.eventHandlers[messageType];
        if (handlers) {
            handlers.forEach(handler => handler(data));
        }
    }

    /* Callback declarations */

    candleUpdate(newCandle) {
        for (const callback of this.candleUpdateCallbacks) {
            callback(newCandle);
        }
    }

    candleClose(newCandle) {
        this.sendToApp('candle_data', newCandle);

        for (const callback of this.candleCloseCallbacks) {
            callback(newCandle);
        }
    }

    indicatorUpdate(data) {
        for (const callback of this.indicatorUpdateCallbacks) {
            callback(data);
        }
    }

    /**
     * Fetches the price history data for a user.
     * Uses EDM (Exchange Data Manager) exclusively.
     * @param {string} userName - The name of the user.
     * @param {Object} chartView - Chart view settings {exchange, market, timeframe}.
     * @returns {Promise<Array|null>} - A promise that resolves
     * to the fetched price history data, or null if an error occurs.
     */
    async getPriceHistory(userName, chartView = null) {
        if (!this.edmRestUrl) {
            // Wait for EDM config if not yet loaded
            await this._waitForEdmConfig();
        }

        if (!chartView) {
            // Fetch chart view from backend if not provided
            chartView = await this._getChartView(userName);
        }

        try {
            const candles = await this._fetchFromEdm(chartView);
            if (candles && candles.length > 0) {
                console.log(`Received ${candles.length} candles from EDM`);
                return candles;
            }
            console.warn('No candles returned from EDM');
            return [];
        } catch (error) {
            console.error('EDM fetch failed:', error);
            return null;
        }
    }

    /**
     * Wait for EDM configuration to be loaded.
     * @returns {Promise<void>}
     */
    async _waitForEdmConfig() {
        let attempts = 0;
        while (!this.edmRestUrl && attempts < 10) {
            await new Promise(resolve => setTimeout(resolve, 100));
            attempts++;
        }
        if (!this.edmRestUrl) {
            throw new Error('EDM configuration not available');
        }
    }

    /**
     * Get the current chart view settings from the backend.
     * @param {string} userName - The name of the user.
     * @returns {Promise<Object>} - Chart view settings.
     */
    async _getChartView(userName) {
        try {
            const response = await fetch(`${this.baseUrl}/api/chart-view`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ user_name: userName })
            });
            if (response.ok) {
                return await response.json();
            }
        } catch (error) {
            console.warn('Failed to fetch chart view:', error);
        }
        // Return defaults if fetch fails
        return { exchange: 'binance', market: 'BTC/USDT', timeframe: '1h' };
    }

    /**
     * Fetches candle data directly from EDM.
     * @param {Object} chartView - Chart view settings {exchange, market, timeframe}.
     * @param {string} sessionId - Optional EDM session ID for authenticated exchanges.
     * @returns {Promise<Array>} - Array of candle objects.
     */
    async _fetchFromEdm(chartView, sessionId = null) {
        if (!chartView.exchange || !chartView.market || !chartView.timeframe) {
            throw new Error('Missing required chart view parameters: exchange, market, timeframe');
        }

        const params = new URLSearchParams({
            exchange: chartView.exchange,
            symbol: chartView.market,
            timeframe: chartView.timeframe,
            limit: '1000'
        });

        // Add session_id for authenticated exchange access
        if (sessionId) {
            params.append('session_id', sessionId);
        }

        const url = `${this.edmRestUrl}/candles?${params}`;
        console.log('Fetching candles from EDM:', url);

        const response = await fetch(url, { mode: 'cors' });

        if (!response.ok) {
            const text = await response.text();
            throw new Error(`EDM error ${response.status}: ${text}`);
        }

        const data = await response.json();
        const candles = data.candles || [];

        // EDM already sends time in seconds, no conversion needed
        return candles.map(c => ({
            time: c.time,
            open: c.open,
            high: c.high,
            low: c.low,
            close: c.close,
            volume: c.volume
        }));
    }

    /**
     * Fetches list of available exchanges from EDM.
     * @returns {Promise<Array<string>>} - Array of exchange names.
     */
    async getAvailableExchanges() {
        if (!this.edmRestUrl) {
            await this._waitForEdmConfig();
        }

        try {
            const response = await fetch(`${this.edmRestUrl}/exchanges`, { mode: 'cors' });
            if (!response.ok) {
                throw new Error(`EDM error: ${response.status}`);
            }
            const data = await response.json();
            return data.exchanges || [];
        } catch (error) {
            console.error('Failed to fetch exchanges from EDM:', error);
            return [];
        }
    }

    /**
     * Fetches list of symbols for an exchange from EDM.
     * @param {string} exchange - The exchange name.
     * @returns {Promise<Array<string>>} - Array of symbol names.
     */
    async getExchangeSymbols(exchange) {
        if (!this.edmRestUrl) {
            await this._waitForEdmConfig();
        }

        try {
            const response = await fetch(`${this.edmRestUrl}/exchanges/${exchange}/symbols`, { mode: 'cors' });
            if (!response.ok) {
                throw new Error(`EDM error: ${response.status}`);
            }
            const data = await response.json();
            return data.symbols || [];
        } catch (error) {
            console.error(`Failed to fetch symbols for ${exchange} from EDM:`, error);
            return [];
        }
    }

    /**
     * Fetches the Indicator data for a specific user.
     * @param {string} userName - The name of the user.
     * @returns {Promise<Object|null>} - A promise that resolves
     * to the fetched Indicator data, or null if an error occurs.
     */
    async getIndicatorData(userName) {
        try {
            const response = await fetch(`${this.baseUrl}/api/indicator_init`, {
                credentials: 'same-origin',
                mode: 'cors',
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ "user_name": userName })
            });

            const data = await response.json();
            return data;
        } catch (error) {
            console.error('Error fetching indicator data:', error);
            return null;
        }
    }

    /**
     * Sends a request to delete an indicator.
     * @param {string} indicatorName - The name of the indicator to be deleted.
     * @returns {Promise<Object>} - The response from the server.
     */
    async deleteIndicator(indicatorName) {
        try {
            const response = await fetch('/settings', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ setting: 'delete_indicator', indicator: indicatorName })
            });
            return await response.json();
        } catch (error) {
            console.error('Error deleting indicator:', error);
            return { success: false };
        }
    }

    /**
     * Sends a request to update an indicator's properties.
     * @param {Object} indicatorData - An object containing the updated properties of the indicator.
     * @returns {Promise<Object>} - The response from the server.
     */
    async updateIndicator(indicatorData) {
        try {
            const response = await fetch('/settings', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    setting: 'edit_indicator',
                    indicator: indicatorData
                })
            });
            return await response.json();
        } catch (error) {
            console.error('Error updating indicator:', error);
            return { success: false };
        }
    }

    /**
     * Sends a request to create a new indicator.
     * @param {Object} indicatorData - An object containing the properties of the new indicator.
     * @returns {Promise<Object>} - The response from the server.
     */
    async submitIndicator(indicatorData) {
        try {
            const response = await fetch('/settings', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    setting: 'new_indicator',
                    indicator: indicatorData
                })
            });
            return await response.json();
        } catch (error) {
            console.error('Error creating indicator:', error);
            return { success: false };
        }
    }

    /**
     * Sends a message to the application server via Socket.IO.
     * @param {string} messageType - The type of the message.
     * @param {Object} data - The data to be sent with the message.
     */
    sendToApp(messageType, data) {
        const messageData = {
            message_type: messageType,
            data: {
                ...data,
                user_name: this.userName
            }
        };

        console.log('Comms: Sending->', JSON.stringify(messageData));

        if (this.connectionOpen && this.socket) {
            this.socket.emit('message', messageData);
        } else {
            // Not an error; message will be queued
            console.warn('Socket.IO connection is not open. Queuing message.');
            // Queue the message to be sent once connected
            this._enqueueMessage(messageType, data);
            console.warn(`Comms: Queued message-> ${JSON.stringify({ messageType, data })} (Connection not open)`);
        }
    }

    /**
     * Set up a WebSocket connection for receiving candlestick data via EDM.
     * @param {string} interval - The interval of the candlestick data.
     * @param {string} tradingPair - The trading pair to subscribe to.
     * @param {string} exchangeName - The exchange name.
     */
    setExchangeCon(interval, tradingPair, exchangeName) {
        // Close existing connection
        if (this.edmWebSocket) {
            this.edmWebSocket.close();
            this.edmWebSocket = null;
        }

        if (!exchangeName) {
            console.error('Exchange name is required for WebSocket connection');
            return;
        }

        this._connectEdmWebSocket(interval, tradingPair, exchangeName);
    }

    /**
     * Connect to EDM WebSocket for candle streaming.
     * @param {string} interval - The interval of the candlestick data.
     * @param {string} tradingPair - The trading pair to subscribe to.
     * @param {string} exchangeName - The exchange name.
     */
    async _connectEdmWebSocket(interval, tradingPair, exchangeName) {
        if (!this.edmWsUrl) {
            await this._waitForEdmConfig();
        }

        console.log(`Connecting to EDM WebSocket: ${this.edmWsUrl}`);
        this.edmWebSocket = new WebSocket(this.edmWsUrl);

        this.edmWebSocket.onopen = () => {
            console.log('Connected to EDM WebSocket');
            // Subscribe to candle stream
            const subscription = {
                action: 'subscribe',
                exchange: exchangeName,
                symbol: tradingPair,
                timeframe: interval
            };
            this.edmWebSocket.send(JSON.stringify(subscription));
            console.log('Subscribed to EDM candle stream:', subscription);
        };

        this.edmWebSocket.onmessage = (event) => {
            try {
                const message = JSON.parse(event.data);

                // EDM uses 'action' field, handle both for compatibility
                const messageType = message.action || message.type;

                if (messageType === 'candle') {
                    const candle = message.data;
                    const newCandle = {
                        time: candle.time,  // EDM sends time in seconds
                        open: parseFloat(candle.open),
                        high: parseFloat(candle.high),
                        low: parseFloat(candle.low),
                        close: parseFloat(candle.close),
                        vol: parseFloat(candle.volume),
                        symbol: tradingPair,
                        exchange: exchangeName
                    };
                    this.candleUpdate(newCandle);

                    if (candle.closed) {
                        this.candleClose(newCandle);
                    }
                } else if (messageType === 'subscribed') {
                    console.log('EDM subscription confirmed:', message);
                } else if (messageType === 'error') {
                    console.error('EDM WebSocket error:', message.message);
                }
            } catch (error) {
                console.error('Error parsing EDM message:', error);
            }
        };

        this.edmWebSocket.onclose = (event) => {
            console.log('Disconnected from EDM WebSocket:', event.code, event.reason);
            // Attempt to reconnect after delay
            if (this._shouldReconnect) {
                console.log('Attempting to reconnect to EDM WebSocket...');
                setTimeout(() => {
                    this._connectEdmWebSocket(interval, tradingPair, exchangeName);
                }, 5000);
            }
        };

        this.edmWebSocket.onerror = (error) => {
            console.error('EDM WebSocket error:', error);
        };

        // Enable auto-reconnect
        this._shouldReconnect = true;
    }

    /**
     * Disconnect from EDM WebSocket.
     */
    disconnectWebSocket() {
        this._shouldReconnect = false;
        if (this.edmWebSocket) {
            this.edmWebSocket.close();
            this.edmWebSocket = null;
        }
    }
}
