// XBC Remote Controller - Extension (Proxy)
// This runs inside XSplit Broadcaster as an Extension Plugin

(function () {
    'use strict';

    const xjs = require('xjs');
    const STORAGE_KEY_ENABLED = 'xbcRemoteEnabled';
    const STORAGE_KEY_USE_IP = 'xbcRemoteUseIp';
    const SERVER_PORT = 80;

    let isEnabled = true;
    let useIp = false;
    let lastServerData = null;
    let websocket = null;
    let remoteConnections = new Set();
    let serverCheckInterval = null;
    let lastServerStartHintAt = 0;
    let streamStatsInterval = null;
    let lastStreamStats = null;

    // DOM elements

    const serverStatusDot = document.getElementById('server-status-dot');
    const serverStatusText = document.getElementById('server-status-text');
    const serverInfo = document.getElementById('server-info');
    const serverUrl = document.getElementById('server-url');
    const remoteCount = document.getElementById('remote-count');
    const logOutput = document.getElementById('log-output');
    const clearLogsBtn = document.getElementById('clear-logs-btn');
    const qrContainer = document.getElementById('qr-container');
    const qrUrl = document.getElementById('qr-url');
    const addressToggle = document.getElementById('address-toggle');

    // Initialize
    xjs.ready().then(() => {
        log('Extension initialized. XJS framework ready.', 'success');

        // Disable maximize button on the window, but allow resizing
        // Border flag: bit 0 (border) + bit 1 (caption) + bit 2 (sizing) + bit 3 (minimize) = 1 + 2 + 4 + 8 = 15
        // This disables maximize (bit 4 = 0) while keeping border, caption, sizing, and minimize
        xjs.ExtensionWindow.setBorder(15);

        // Load saved settings
        loadSettings();

        // Set up UI event listeners
        setupEventListeners();

        // Check server and connect
        checkServerAndConnect();

        // Periodically check server status
        serverCheckInterval = setInterval(() => {
            if (isEnabled && (!websocket || websocket.readyState !== WebSocket.OPEN)) {
                checkServerAndConnect();
            }
        }, 10000); // Check every 10 seconds
    }).catch(err => {
        log('Failed to initialize XJS: ' + err.message, 'error');
    });

    function loadSettings() {
        // Load enabled state (default: true)
        const savedEnabled = localStorage.getItem(STORAGE_KEY_ENABLED);
        isEnabled = savedEnabled !== null ? savedEnabled === 'true' : true;

        // Load Use IP state (default: false)
        const savedUseIp = localStorage.getItem(STORAGE_KEY_USE_IP);
        useIp = savedUseIp === 'true';
        if (addressToggle) {
            addressToggle.checked = useIp;
        }
    }

    function setupEventListeners() {
        clearLogsBtn.addEventListener('click', () => {
            logOutput.innerHTML = '';
        });

        if (addressToggle) {
            addressToggle.addEventListener('change', () => {
                useIp = addressToggle.checked;
                localStorage.setItem(STORAGE_KEY_USE_IP, useIp.toString());
                log(`Address mode changed: ${useIp ? 'Using IP' : 'Using Hostname'}`, 'info');

                if (lastServerData) {
                    fetchAndDisplayQRCode(lastServerData.ip, lastServerData.hostname, lastServerData.port);
                }
            });
        }
    }




    function checkServerAndConnect() {
        // If already connected or connecting, don't check again
        if (websocket && (websocket.readyState === WebSocket.OPEN || websocket.readyState === WebSocket.CONNECTING)) {
            return;
        }

        // Try to connect directly - WebSocket will fail fast if server is down
        connectToServer();
    }

    function connectToServer() {
        if (websocket && websocket.readyState === WebSocket.OPEN) {
            return;
        }

        updateServerStatus('connecting', 'Connecting...');

        const wsUrl = `ws://localhost:${SERVER_PORT}/ws`;
        websocket = new WebSocket(wsUrl);

        websocket.onopen = () => {
            log('Connected to relay server on port ' + SERVER_PORT, 'success');
            updateServerStatus('connected', 'Connected');
            serverInfo.style.display = 'block';

            // Set up message handler before registering (to receive server IP)
            websocket.onmessage = (event) => {
                try {
                    const message = JSON.parse(event.data);
                    handleServerMessage(message);
                } catch (err) {
                    log('Failed to parse server message: ' + err.message, 'error');
                }
            };

            // Register as Proxy with the relay server
            // Server will respond with server IP in the registration response
            sendToServer({
                type: 'register',
                role: 'proxy'
            });
        };

        websocket.onerror = (error) => {
            // Don't log error on initial connection attempt - it's expected if server isn't running
            // The onclose handler will update the UI
            // Only log if we get an error while connected
            if (websocket && websocket.readyState === WebSocket.OPEN) {
                log('WebSocket error: ' + (error.message || 'Connection error'), 'error');
                updateServerStatus('disconnected', 'Connection Error');
            }
            serverInfo.style.display = 'none';
        };

        websocket.onclose = (event) => {
            // Only log if we were previously connected
            if (event.wasClean) {
                log('Disconnected from relay server', 'info');
            }
            updateServerStatus('disconnected', 'Disconnected');
            serverInfo.style.display = 'none';
            // Clear QR code when disconnected
            if (qrContainer) {
                qrContainer.innerHTML = '<div class="qr-loading">Waiting for server connection...</div>';
            }
            if (qrUrl) {
                qrUrl.textContent = '';
            }
            maybeLogServerStartHint();

            // Attempt to reconnect after 3 seconds (if enabled)
            setTimeout(() => {
                if (isEnabled) {
                    checkServerAndConnect();
                }
            }, 3000);
        };
    }

    function disconnectFromServer() {
        if (websocket) {
            websocket.close();
            websocket = null;
        }
    }

    function sendToServer(data) {
        if (websocket && websocket.readyState === WebSocket.OPEN) {
            websocket.send(JSON.stringify(data));
        }
    }

    async function fetchAndDisplayQRCode(serverIP, serverHostname, port) {
        if (!qrContainer || !qrUrl) return;

        qrContainer.innerHTML = '<div class="qr-loading">Loading QR code...</div>';
        qrUrl.textContent = '';

        try {
            // We fetch the QR code from the local server
            // Use localhost for the API call, but the server will generate the QR for the correct address
            const apiUrl = `http://localhost:${port}/api/qr-code?useIp=${useIp}`;
            const response = await fetch(apiUrl);

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            const data = await response.json();

            if (data.qrCode) {
                qrContainer.innerHTML = `<img src="${data.qrCode}" alt="QR Code" style="max-width: 100%; display: block; margin: 0 auto;">`;
                qrUrl.textContent = data.url || `http://${useIp ? serverIP : serverHostname}:${port}`;
            } else {
                throw new Error('Invalid QR code data');
            }
        } catch (err) {
            console.error('Failed to load QR code:', err);
            qrContainer.innerHTML = '<div class="qr-error">Failed to load QR code</div>';
            qrUrl.textContent = '';
        }
    }

    function handleServerMessage(message) {
        switch (message.type) {
            case 'registered':
                if (message.role === 'proxy') {
                    log('Registered as proxy with server', 'success');

                    // Store server data for re-fetches
                    lastServerData = {
                        ip: message.serverIP,
                        hostname: message.serverHostname,
                        port: Number(message.port || SERVER_PORT)
                    };

                    // Display server URL from server response - show both hostname and IP
                    if (message.serverHostname || message.serverIP) {
                        const port = lastServerData.port;
                        const portSuffix = port === 80 ? '' : `:${port}`;

                        // Build URL display with hostname (for Safari) and IP fallback
                        if (message.serverHostname && message.serverIP) {
                            serverUrl.innerHTML = `http://${message.serverHostname}${portSuffix}<br><span style="font-size: 0.85em; color: #888;">Fallback: http://${message.serverIP}${portSuffix}</span>`;
                        } else if (message.serverHostname) {
                            serverUrl.textContent = `http://${message.serverHostname}${portSuffix}`;
                        } else {
                            serverUrl.textContent = `http://${message.serverIP}${portSuffix}`;
                        }
                        // Fetch and display QR code (server will use preference)
                        fetchAndDisplayQRCode(lastServerData.ip, lastServerData.hostname, port);
                    } else {
                        // Fallback: try to get IP via WebRTC (may not work in all browsers)
                        getLocalIP().then(ip => {
                            if (ip) {
                                serverUrl.textContent = `http://${ip}`;
                            } else {
                                serverUrl.textContent = `http://<your-ip>`;
                            }
                        });
                    }

                    // If there are already remotes connected, start stream stats updates
                    // (This handles the case where remotes connected before the extension)
                    if (remoteConnections.size > 0) {
                        log('Remotes already connected, starting stream stats updates', 'info');
                        startStreamStatsUpdates();
                    }
                }
                break;

            case 'remote-connected':
                remoteConnections.add(message.remoteId);
                updateRemoteCount();
                log('Remote connected: ' + message.remoteId + ' (Total: ' + remoteConnections.size + ')', 'info');
                // Start stream stats updates if not already running
                startStreamStatsUpdates();
                // Send current stats immediately if available
                if (lastStreamStats) {
                    log('Sending cached stream stats to new remote', 'info');
                    // Note: We don't have debug data for cached stats, but that's okay
                    broadcastStreamStats(lastStreamStats, null);
                } else {
                    // Fetch stats immediately for new remote
                    getStreamStats().then(result => {
                        lastStreamStats = result.stats;
                        broadcastStreamStats(result.stats, result._debug);
                    });
                }
                break;

            case 'remote-disconnected':
                remoteConnections.delete(message.remoteId);
                updateRemoteCount();
                log('Remote disconnected: ' + message.remoteId, 'info');
                // Stop stream stats updates if no remotes connected
                if (remoteConnections.size === 0) {
                    stopStreamStatsUpdates();
                }
                break;

            case 'remote-message':
                // Handle command from remote
                if (!isEnabled) {
                    log('Remote command rejected: plugin is disabled', 'error');
                    sendCommandResponse(message.id, { error: 'Plugin is disabled' });
                    return;
                }

                try {
                    const command = JSON.parse(message.data);
                    handleRemoteCommand(command, message.id);
                } catch (err) {
                    log('Error processing remote command: ' + err.message, 'error');
                    sendCommandResponse(message.id, { error: err.message });
                }
                break;

            case 'error':
                log('Server error: ' + message.message, 'error');
                break;

            default:
                log('Unknown message type from server: ' + message.type, 'error');
        }
    }

    function updateServerStatus(state, text) {
        serverStatusDot.className = 'status-dot ' + state;
        serverStatusText.textContent = text;
    }

    function updateRemoteCount() {
        remoteCount.textContent = remoteConnections.size;
    }

    function getLocalIP() {
        // Attempt to get local IP via WebRTC (won't work in all environments)
        return new Promise((resolve) => {
            const RTCPeerConnection = window.RTCPeerConnection ||
                window.mozRTCPeerConnection ||
                window.webkitRTCPeerConnection;

            if (!RTCPeerConnection) {
                resolve(null);
                return;
            }

            const pc = new RTCPeerConnection({ iceServers: [] });
            pc.createDataChannel('');
            pc.onicecandidate = (event) => {
                if (event.candidate) {
                    const candidate = event.candidate.candidate;
                    const match = candidate.match(/([0-9]{1,3}(\.[0-9]{1,3}){3})/);
                    if (match) {
                        const ip = match[1];
                        if (!ip.startsWith('127.') && !ip.startsWith('169.254.')) {
                            pc.close();
                            resolve(ip);
                        }
                    }
                }
            };
            pc.createOffer().then(offer => pc.setLocalDescription(offer));

            // Timeout after 2 seconds
            setTimeout(() => {
                pc.close();
                resolve(null);
            }, 2000);
        });
    }

    function maybeLogServerStartHint() {
        const now = Date.now();
        // Throttle to avoid spamming logs during reconnect attempts.
        if (now - lastServerStartHintAt < 30000) {
            return;
        }
        lastServerStartHintAt = now;

        log('Server not detected. Start it manually to enable remote control.', 'info');
        log('Run: server/launch-server.bat', 'info');
        log('Note: port 80 may require Administrator privileges and/or Windows Firewall approval.', 'info');
    }

    function handleRemoteCommand(command, commandId) {
        log('Received command: ' + command.action, 'info');

        xjs.ready().then(() => {
            if (command.action === 'startRecording') {
                return startRecording();
            } else if (command.action === 'stopRecording') {
                return stopRecording();
            } else if (command.action === 'pauseRecording') {
                return pauseRecording();
            } else if (command.action === 'unpauseRecording') {
                return unpauseRecording();
            } else if (command.action === 'stopAllStreams') {
                return stopAllStreams();
            } else if (command.action === 'stopSingleStream') {
                return stopSingleStream(command.streamName, command.outputIndex);
            } else if (command.action === 'getScenes') {
                return getScenes();
            } else if (command.action === 'getSceneThumbnail') {
                return getSceneThumbnail(command.sceneId);
            } else if (command.action === 'setScene') {
                return setScene(command.sceneId);
            } else if (command.action === 'getStatus') {
                return getStatus();
            } else if (command.action === 'getStreamStats') {
                // Return cached stats immediately, then update in background
                if (lastStreamStats) {
                    return Promise.resolve({ stats: lastStreamStats });
                }
                // If no cached stats, fetch them
                return getStreamStats().then(result => {
                    lastStreamStats = result.stats;
                    return result;
                });
            } else if (command.action === 'getOutputList') {
                return getOutputList();
            } else if (command.action === 'startStream') {
                return startStream(command.outputName, command.outputIndex);
            } else if (command.action === 'getSceneItems') {
                return getSceneItems(command.sceneId);
            } else if (command.action === 'setItemVisible') {
                return setItemVisible(command.itemId, command.visible);
            } else {
                throw new Error('Unknown command: ' + command.action);
            }
        }).then((result) => {
            log('Command executed successfully: ' + command.action, 'success');
            const response = { success: true, action: command.action };
            if (result) {
                Object.assign(response, result);
            }
            sendCommandResponse(commandId, response);
        }).catch(err => {
            log('Command failed: ' + err.message, 'error');
            sendCommandResponse(commandId, { error: err.message, action: command.action });
        });
    }

    function sendCommandResponse(commandId, response) {
        sendToServer({
            type: 'proxy-response',
            commandId: commandId,
            data: JSON.stringify(response)
        });
    }

    function startRecording() {
        // Try Output.startLocalRecording() static method first (simplest approach)
        if (xjs.Output && typeof xjs.Output.startLocalRecording === 'function') {
            log('Using Output.startLocalRecording()', 'info');
            return xjs.Output.startLocalRecording();
        }

        // Try ChannelManager API (if it exists)
        if (xjs.ChannelManager && typeof xjs.ChannelManager.startRecording === 'function') {
            log('Using ChannelManager.startRecording()', 'info');
            return xjs.ChannelManager.startRecording();
        }

        // Fallback: Try Output API with instance methods
        if (xjs.Output && typeof xjs.Output.getOutputList === 'function') {
            log('Using Output.getOutputList() and instance methods', 'info');
            return xjs.Output.getOutputList().then(outputs => {
                if (!outputs || outputs.length === 0) {
                    throw new Error('No outputs available. Please configure recording in XSplit first.');
                }

                log('Found ' + outputs.length + ' output(s)', 'info');

                // Try to find recording output and start it
                for (let output of outputs) {
                    const name = output.getName ? output.getName() : (output.name || '');
                    log('Checking output: ' + name, 'info');

                    // Try startBroadcast (for broadcast outputs that can also record)
                    if (output.startBroadcast && typeof output.startBroadcast === 'function') {
                        log('Starting output via startBroadcast: ' + name, 'info');
                        return output.startBroadcast();
                    }
                }

                throw new Error('No startable output found. Please configure recording in XSplit.');
            });
        }

        throw new Error('Recording API not available. XJS Output module not found.');
    }

    function stopRecording() {
        // Try Output.stopLocalRecording() static method first (simplest approach)
        if (xjs.Output && typeof xjs.Output.stopLocalRecording === 'function') {
            log('Using Output.stopLocalRecording()', 'info');
            return xjs.Output.stopLocalRecording();
        }

        // Try ChannelManager API (if it exists)
        if (xjs.ChannelManager && typeof xjs.ChannelManager.stopRecording === 'function') {
            log('Using ChannelManager.stopRecording()', 'info');
            return xjs.ChannelManager.stopRecording();
        }

        // Fallback: Try Output API with instance methods
        if (xjs.Output && typeof xjs.Output.getOutputList === 'function') {
            log('Using Output.getOutputList() and instance methods', 'info');
            return xjs.Output.getOutputList().then(outputs => {
                if (!outputs || outputs.length === 0) {
                    throw new Error('No outputs available.');
                }

                // Try to find recording output and stop it
                for (let output of outputs) {
                    const name = output.getName ? output.getName() : (output.name || '');

                    // Try stopBroadcast (for broadcast outputs)
                    if (output.stopBroadcast && typeof output.stopBroadcast === 'function') {
                        log('Stopping output via stopBroadcast: ' + name, 'info');
                        return output.stopBroadcast();
                    }
                }

                throw new Error('No stoppable output found.');
            });
        }

        throw new Error('Recording API not available. XJS Output module not found.');
    }

    function pauseRecording() {
        if (xjs.Output && typeof xjs.Output.pauseLocalRecording === 'function') {
            log('Using Output.pauseLocalRecording()', 'info');
            return xjs.Output.pauseLocalRecording();
        }
        throw new Error('Pause recording API not available.');
    }

    function unpauseRecording() {
        if (xjs.Output && typeof xjs.Output.unpauseLocalRecording === 'function') {
            log('Using Output.unpauseLocalRecording()', 'info');
            return xjs.Output.unpauseLocalRecording();
        }
        throw new Error('Unpause recording API not available.');
    }

    function stopAllStreams() {
        // Stop all streaming outputs
        if (xjs.Output && typeof xjs.Output.getOutputList === 'function') {
            log('Using Output.getOutputList() to stop all streams', 'info');
            return xjs.Output.getOutputList().then(outputs => {
                if (!outputs || outputs.length === 0) {
                    throw new Error('No outputs available.');
                }

                log('Found ' + outputs.length + ' output(s), attempting to stop all streams', 'info');

                // Get names first to log them
                const namePromises = outputs.map((output, idx) => {
                    return output.getName().then(name => {
                        log('Output ' + idx + ': "' + name + '"', 'info');
                        return { output, name, index: idx };
                    }).catch(err => {
                        log('Output ' + idx + ': Failed to get name - ' + err.message, 'error');
                        return { output, name: 'Unknown', index: idx };
                    });
                });

                return Promise.all(namePromises).then(outputInfos => {
                    // Stop all outputs that have stopBroadcast method
                    const stopPromises = [];
                    let stoppedCount = 0;
                    let failedCount = 0;

                    outputInfos.forEach(info => {
                        if (info.output.stopBroadcast && typeof info.output.stopBroadcast === 'function') {
                            log('Attempting to stop output: "' + info.name + '"', 'info');
                            stopPromises.push(
                                info.output.stopBroadcast()
                                    .then(() => {
                                        stoppedCount++;
                                        log('✓ Successfully stopped: "' + info.name + '"', 'success');
                                        return { success: true, name: info.name };
                                    })
                                    .catch(err => {
                                        failedCount++;
                                        log('✗ Failed to stop "' + info.name + '": ' + err.message, 'error');
                                        return { success: false, name: info.name, error: err.message };
                                    })
                            );
                        } else {
                            log('Output "' + info.name + '" does not have stopBroadcast method', 'info');
                        }
                    });

                    if (stopPromises.length === 0) {
                        throw new Error('No outputs with stopBroadcast method found. Make sure streams are running.');
                    }

                    log('Waiting for ' + stopPromises.length + ' stop operation(s) to complete...', 'info');

                    // Wait for all stop operations to complete
                    return Promise.all(stopPromises).then(results => {
                        const successCount = results.filter(r => r.success).length;
                        const failCount = results.filter(r => !r.success).length;

                        const message = 'Stop operations completed: ' + successCount + ' succeeded, ' + failCount + ' failed';
                        log(message, successCount > 0 ? 'success' : 'error');

                        return {
                            message: message,
                            stopped: successCount,
                            failed: failCount,
                            results: results
                        };
                    });
                });
            });
        }

        throw new Error('Output API not available. XJS Output module not found.');
    }

    function stopSingleStream(streamName, outputIndex) {
        // Stop a specific stream by name and output index
        if (!streamName) {
            throw new Error('Stream name is required.');
        }

        if (xjs.Output && typeof xjs.Output.getOutputList === 'function') {
            log('=== STOP SINGLE STREAM ===', 'info');
            log('Requested stream: "' + streamName + '"', 'info');
            log('Requested output index: ' + outputIndex, 'info');

            return xjs.Output.getOutputList().then(outputs => {
                if (!outputs || outputs.length === 0) {
                    throw new Error('No outputs available.');
                }

                log('Found ' + outputs.length + ' output(s) total', 'info');

                // Get the base name (without &output or &amp;output parameter)
                // Remove both encoded and unencoded versions
                let baseName = streamName
                    .replace(/&amp;output[=:]\d+/g, '')
                    .replace(/&output[=:]\d+/g, '')
                    .replace(/\s*#\d+$/g, '') // Remove " #1" or " #2" suffix if present
                    .trim();

                log('Base name extracted: "' + baseName + '"', 'info');

                // Try multiple matching strategies
                return Promise.all(outputs.map((output, idx) => {
                    return output.getName().then(name => {
                        log('Output[' + idx + ']: "' + name + '"', 'info');

                        // Strategy 1: Exact match on base name
                        if (name === baseName) {
                            log('  → EXACT MATCH found at index ' + idx, 'success');
                            return { output, name, index: idx, matchType: 'exact' };
                        }

                        // Strategy 2: Case-insensitive match
                        if (name.toLowerCase() === baseName.toLowerCase()) {
                            log('  → Case-insensitive match at index ' + idx, 'success');
                            return { output, name, index: idx, matchType: 'case-insensitive' };
                        }

                        // Strategy 3: Partial match (contains)
                        if (name.includes(baseName) || baseName.includes(name)) {
                            log('  → Partial match at index ' + idx, 'success');
                            return { output, name, index: idx, matchType: 'partial' };
                        }

                        log('  → No match', 'info');
                        return null;
                    }).catch(err => {
                        log('Output[' + idx + ']: Failed to get name - ' + err.message, 'error');
                        return null;
                    });
                })).then(matches => {
                    // Filter out nulls and find best match
                    const validMatches = matches.filter(m => m !== null);

                    log('Found ' + validMatches.length + ' potential match(es)', 'info');

                    if (validMatches.length === 0) {
                        const availableOutputs = outputs.map((o, i) => 'Output[' + i + ']').join(', ');
                        throw new Error('Stream not found: "' + streamName + '". Available outputs: ' + availableOutputs);
                    }

                    // If outputIndex is specified, try to match by index
                    let matchedOutput = null;

                    if (outputIndex !== undefined && outputIndex !== null) {
                        // First try exact index match
                        matchedOutput = validMatches.find(m => m.index === outputIndex);
                        if (matchedOutput) {
                            log('Matched by output index: ' + outputIndex, 'success');
                        }
                    }

                    // If no match by index, use first match (prefer exact over partial)
                    if (!matchedOutput) {
                        const exactMatch = validMatches.find(m => m.matchType === 'exact');
                        const caseInsensitiveMatch = validMatches.find(m => m.matchType === 'case-insensitive');
                        matchedOutput = exactMatch || caseInsensitiveMatch || validMatches[0];
                        log('Using best match: "' + matchedOutput.name + '" (type: ' + matchedOutput.matchType + ')', 'success');
                    }

                    if (!matchedOutput.output.stopBroadcast || typeof matchedOutput.output.stopBroadcast !== 'function') {
                        throw new Error('Output "' + matchedOutput.name + '" does not have stopBroadcast method');
                    }

                    log('Calling stopBroadcast() on "' + matchedOutput.name + '"...', 'info');

                    return matchedOutput.output.stopBroadcast().then(() => {
                        log('✓ Successfully stopped: "' + matchedOutput.name + '"', 'success');
                        return { message: 'Stopped stream: ' + matchedOutput.name };
                    }).catch(err => {
                        log('✗ stopBroadcast() failed for "' + matchedOutput.name + '": ' + err.message, 'error');
                        throw new Error('Failed to stop "' + matchedOutput.name + '": ' + err.message);
                    });
                });
            }).catch(err => {
                log('ERROR in stopSingleStream: ' + err.message, 'error');
                throw err;
            });
        }

        throw new Error('Output API not available. XJS Output module not found.');
    }

    function getOutputList() {
        if (!xjs.Output || typeof xjs.Output.getOutputList !== 'function') {
            throw new Error('Output API not available.');
        }

        log('Fetching output list...', 'info');

        return xjs.Output.getOutputList().then(outputs => {
            if (!outputs || outputs.length === 0) {
                log('No outputs found', 'info');
                return { outputs: [] };
            }

            log('Found ' + outputs.length + ' output(s)', 'info');

            // Get names for all outputs
            const outputPromises = outputs.map((output, index) => {
                return output.getName().then(name => {
                    return {
                        index: index,
                        name: name
                    };
                }).catch(err => {
                    log('Error getting output name for index ' + index + ': ' + err.message, 'error');
                    return {
                        index: index,
                        name: 'Unknown'
                    };
                });
            });

            return Promise.all(outputPromises).then(outputNames => {
                log('Retrieved ' + outputNames.length + ' output name(s)', 'info');
                return { outputs: outputNames };
            });
        }).catch(err => {
            log('Error getting output list: ' + err.message, 'error');
            throw err;
        });
    }

    function startStream(outputName, outputIndex) {
        if (!outputName) {
            throw new Error('Output name is required.');
        }

        // Only support Canvas 1 (outputIndex 0) for now
        // XJS framework predates dual canvas support
        if (outputIndex !== 0) {
            throw new Error('Only Canvas 1 (outputIndex 0) is supported.');
        }

        if (!xjs.Output || typeof xjs.Output.getOutputList !== 'function') {
            throw new Error('Output API not available.');
        }

        return xjs.Output.getOutputList().then(outputs => {
            if (!outputs || outputs.length === 0) {
                throw new Error('No outputs available.');
            }

            // Find matching output by name
            return Promise.all(outputs.map((output, idx) => {
                return output.getName().then(name => {
                    // Match by exact name
                    if (name === outputName) {
                        return { output, name, index: idx, matchType: 'exact' };
                    }

                    // Case-insensitive match
                    if (name.toLowerCase() === outputName.toLowerCase()) {
                        return { output, name, index: idx, matchType: 'case-insensitive' };
                    }

                    // Partial match
                    if (name.includes(outputName) || outputName.includes(name)) {
                        return { output, name, index: idx, matchType: 'partial' };
                    }

                    return null;
                }).catch(err => {
                    log('Failed to get output name: ' + err.message, 'error');
                    return null;
                });
            })).then(matches => {
                // Filter out nulls and find best match
                const validMatches = matches.filter(m => m !== null);

                if (validMatches.length === 0) {
                    throw new Error('Output not found: "' + outputName + '"');
                }

                // Use best match (prefer exact over case-insensitive over partial)
                const exactMatch = validMatches.find(m => m.matchType === 'exact');
                const caseInsensitiveMatch = validMatches.find(m => m.matchType === 'case-insensitive');
                const matchedOutput = exactMatch || caseInsensitiveMatch || validMatches[0];

                if (!matchedOutput.output.startBroadcast || typeof matchedOutput.output.startBroadcast !== 'function') {
                    throw new Error('Output "' + matchedOutput.name + '" does not have startBroadcast method');
                }

                // Use the standard Output.startBroadcast() method with base name (no output parameter)
                // XJS framework predates dual canvas support, so we use the base output name
                return matchedOutput.output.startBroadcast({ suppressPrestreamDialog: true }).then(() => {
                    return { message: 'Started stream: ' + matchedOutput.name };
                }).catch(err => {
                    throw new Error('Failed to start "' + matchedOutput.name + '": ' + err.message);
                });
            }).catch(err => {
                log('ERROR in startStream: ' + err.message, 'error');
                throw err;
            });
        }).catch(err => {
            log('ERROR in startStream: ' + err.message, 'error');
            throw err;
        });
    }

    function getScenes() {
        if (!xjs.Scene) {
            throw new Error('Scene API not available.');
        }

        log('Fetching scenes...', 'info');

        // Initialize scene pool and get all scenes
        return xjs.Scene._initializeScenePoolAsync().then(count => {
            const scenePromises = [];

            // Get each scene by index
            for (let i = 0; i < count; i++) {
                scenePromises.push(
                    xjs.Scene.getBySceneIndex(i).then(scene => {
                        return Promise.all([
                            scene.getName(),
                            scene.getSceneIndex(),
                            scene.getSceneUid ? scene.getSceneUid() : Promise.resolve(null)
                        ]).then(([name, index, uid]) => {
                            return {
                                id: uid || `scene-${index}`,
                                index: index,
                                name: name || `Scene ${index + 1}`
                            };
                        }).catch(err => {
                            log('Error getting scene ' + i + ': ' + err.message, 'error');
                            return null;
                        });
                    }).catch(err => {
                        log('Error accessing scene ' + i + ': ' + err.message, 'error');
                        return null;
                    })
                );
            }

            return Promise.all(scenePromises).then(scenes => {
                const validScenes = scenes.filter(s => s !== null);
                log('Found ' + validScenes.length + ' scene(s)', 'info');
                return { scenes: validScenes };
            });
        });
    }

    function getSceneThumbnail(sceneId) {
        if (!xjs.Thumbnail || !xjs.Thumbnail.getSceneThumbnail) {
            throw new Error('Thumbnail API not available.');
        }

        if (!xjs.Scene) {
            throw new Error('Scene API not available.');
        }

        log('Getting thumbnail for scene: ' + sceneId, 'info');

        // Try to find scene by UID first, then by index
        let scenePromise;

        if (sceneId.match(/^\{[A-F0-9-]+\}$/i)) {
            // Scene UID format
            scenePromise = xjs.Scene.getBySceneUid(sceneId);
        } else if (sceneId.startsWith('scene-')) {
            // Extract index from scene-{index} format
            const index = parseInt(sceneId.replace('scene-', ''), 10);
            if (!isNaN(index)) {
                scenePromise = xjs.Scene.getBySceneIndex(index);
            } else {
                throw new Error('Invalid scene ID format: ' + sceneId);
            }
        } else {
            // Try as numeric index (0-based)
            const index = parseInt(sceneId, 10);
            if (!isNaN(index) && index >= 0) {
                scenePromise = xjs.Scene.getBySceneIndex(index);
            } else {
                throw new Error('Invalid scene ID format: ' + sceneId);
            }
        }

        return scenePromise.then(scene => {
            return xjs.Thumbnail.getSceneThumbnail(scene);
        }).then(thumbnail => {
            log('Got thumbnail for scene: ' + sceneId, 'info');
            return { thumbnail: thumbnail };
        }).catch(err => {
            log('Error getting thumbnail for scene ' + sceneId + ': ' + err.message, 'error');
            throw err;
        });
    }

    function setScene(sceneId) {
        if (!xjs.Scene) {
            throw new Error('Scene API not available.');
        }

        log('Setting active scene: ' + sceneId, 'info');

        // Try to find scene by UID first, then by index
        if (sceneId.startsWith('scene-')) {
            // Extract index from scene-{index} format
            const index = parseInt(sceneId.replace('scene-', ''), 10);
            if (!isNaN(index)) {
                return xjs.Scene.getBySceneIndex(index).then(scene => {
                    return xjs.Scene.setActiveScene(scene);
                });
            }
        }

        // Try by UID
        if (sceneId.match(/^\{[A-F0-9-]+\}$/i)) {
            return xjs.Scene.getBySceneUid(sceneId).then(scene => {
                return xjs.Scene.setActiveScene(scene);
            });
        }

        // Try as numeric index (1-based for getById)
        const sceneNum = parseInt(sceneId, 10);
        if (!isNaN(sceneNum) && sceneNum > 0) {
            return xjs.Scene.setActiveScene(sceneNum);
        }

        throw new Error('Invalid scene ID format: ' + sceneId);
    }

    function getStatus() {
        const statusPromises = [];

        // Get current scene
        if (xjs.Scene) {
            statusPromises.push(
                xjs.Scene.getActiveScene().then(scene => {
                    return Promise.all([
                        scene.getName(),
                        scene.getSceneIndex(),
                        scene.getSceneUid ? scene.getSceneUid() : Promise.resolve(null)
                    ]).then(([name, index, uid]) => {
                        return {
                            currentScene: {
                                id: uid || `scene-${index}`,
                                index: index,
                                name: name || `Scene ${index + 1}`
                            }
                        };
                    });
                }).catch(err => {
                    log('Error getting active scene: ' + err.message, 'error');
                    return { currentScene: null };
                })
            );
        } else {
            statusPromises.push(Promise.resolve({ currentScene: null }));
        }

        // Get recording state (simplified - we'll just return unknown for now)
        // In a real implementation, you might check StreamInfo or Output state
        statusPromises.push(Promise.resolve({ recording: 'unknown' }));

        return Promise.all(statusPromises).then(results => {
            const status = {};
            results.forEach(result => {
                Object.assign(status, result);
            });
            return { status: status };
        });
    }

    function getStreamStats() {
        if (!xjs.StreamInfo) {
            log('StreamInfo API not available', 'error');
            return Promise.resolve({ stats: [] });
        }

        // Helpers
        function promiseWithTimeout(promise, timeoutMs, fallbackValue) {
            return Promise.race([
                promise,
                new Promise(resolve => setTimeout(() => resolve(fallbackValue), timeoutMs))
            ]);
        }

        function normalizeInternalNameForStats(internalName, outputIndex) {
            let name = String(internalName || '');
            // recstat in newer XBC can contain &amp;output:0 - convert to &amp;output=0
            name = name.replace(/&amp;output:/g, '&amp;output=');
            name = name.replace(/&output:/g, '&output=');

            // Ensure output param exists in *encoded* form, because App.get expects encoded names
            if (!name.includes('&amp;output=')) {
                name = name + '&amp;output=' + String(outputIndex);
            }
            return name;
        }

        function tryGetGlobalProperty(propName, timeoutMs) {
            // Prefer xjs.App.getGlobalProperty if available, else try low-level exec
            if (xjs.App && typeof xjs.App.getGlobalProperty === 'function') {
                return promiseWithTimeout(xjs.App.getGlobalProperty(propName), timeoutMs, null);
            }
            if (typeof xjs.exec === 'function') {
                return promiseWithTimeout(xjs.exec('GetGlobalProperty', propName), timeoutMs, null);
            }
            return Promise.resolve(null);
        }

        function snippet(val, maxLen) {
            const s = val === null || val === undefined ? '' : String(val);
            if (s.length <= maxLen) return s;
            return s.slice(0, maxLen) + '…';
        }

        function unique(arr) {
            return Array.from(new Set(arr.filter(Boolean)));
        }

        function encodeAmp(str) {
            return String(str || '').replace(/&/g, '&amp;');
        }

        // Try a list of candidate names against App.get(prop + name) until one returns non-null/non-undefined
        function tryAppGetWithCandidates(prefix, candidates, timeoutMs = 2000) {
            if (!(xjs.App && typeof xjs.App.get === 'function')) return Promise.resolve(null);
            let chain = Promise.resolve(null);
            candidates.forEach((cand, idx) => {
                chain = chain.then(res => {
                    if (res !== null && res !== undefined && res !== '') {
                        log(prefix + ' found match at candidate ' + (idx + 1) + ': ' + cand + ' = ' + snippet(String(res), 50), 'success');
                        return res;
                    }
                    const key = prefix + cand;
                    log('Trying ' + prefix + ' candidate ' + (idx + 1) + '/' + candidates.length + ': ' + key, 'info');
                    return promiseWithTimeout(xjs.App.get(key), timeoutMs, null).then(val => {
                        if (val !== null && val !== undefined && val !== '') {
                            log(prefix + ' SUCCESS with candidate ' + (idx + 1) + ': ' + cand + ' = ' + snippet(String(val), 50), 'success');
                            return val;
                        }
                        log(prefix + ' candidate ' + (idx + 1) + ' returned empty/null: ' + cand, 'info');
                        // Try xjs.exec as fallback for this candidate
                        if (typeof xjs.exec === 'function') {
                            try {
                                const execKey = prefix + cand;
                                const execResult = xjs.exec('GetGlobalProperty', execKey);
                                if (execResult !== null && execResult !== undefined && execResult !== '') {
                                    log(prefix + ' SUCCESS with candidate ' + (idx + 1) + ' via xjs.exec: ' + cand + ' = ' + snippet(String(execResult), 50), 'success');
                                    return execResult;
                                }
                            } catch (err) {
                                // Ignore exec errors, continue to next candidate
                            }
                        }
                        return val;
                    });
                });
            });
            return chain.then(res => {
                if (res === null || res === undefined || res === '') {
                    log(prefix + ' all ' + candidates.length + ' candidates failed', 'error');
                }
                return res;
            });
        }

        log('Fetching active stream channels...', 'info');

        // Get both outputs and stream channels to match them
        const outputsPromise = xjs.Output ? xjs.Output.getOutputList().catch(() => []) : Promise.resolve([]);
        const channelsPromise = xjs.StreamInfo.getActiveStreamChannels();

        return Promise.all([outputsPromise, channelsPromise]).then(([outputs, channels]) => {
            log('getStreamStats: Found ' + (channels ? channels.length : 0) + ' channel(s) and ' + (outputs ? outputs.length : 0) + ' output(s)', 'info');

            if (!channels || channels.length === 0) {
                log('No active stream channels found', 'info');
                // Get output names for debug even when no channels
                const outputNamesPromise = Promise.all((outputs || []).map((out, idx) =>
                    out.getName().then(name => ({ index: idx, name: name })).catch(() => ({ index: idx, name: 'Unknown' }))
                ));

                return outputNamesPromise.then(outputNames => {
                    // Also try to get bandwidthusage-all for debug
                    if (xjs.App && typeof xjs.App.getGlobalProperty === 'function') {
                        return xjs.App.getGlobalProperty('bandwidthusage-all').then(result => {
                            try {
                                const rawBandwidthUsage = JSON.parse(result);
                                return {
                                    stats: [],
                                    _debug: {
                                        bandwidthUsageAll: rawBandwidthUsage,
                                        channelCount: 0,
                                        outputCount: outputs ? outputs.length : 0,
                                        channelNames: [],
                                        outputNames: outputNames,
                                        note: 'No active channels found'
                                    }
                                };
                            } catch (e) {
                                return {
                                    stats: [],
                                    _debug: {
                                        bandwidthUsageAll: { error: e.message, raw: result },
                                        channelCount: 0,
                                        outputCount: outputs ? outputs.length : 0,
                                        channelNames: [],
                                        outputNames: outputNames,
                                        note: 'No active channels found'
                                    }
                                };
                            }
                        }).catch(() => {
                            return {
                                stats: [],
                                _debug: {
                                    bandwidthUsageAll: { error: 'Failed to fetch' },
                                    channelCount: 0,
                                    outputCount: outputs ? outputs.length : 0,
                                    channelNames: [],
                                    outputNames: outputNames,
                                    note: 'No active channels found'
                                }
                            };
                        });
                    } else {
                        return {
                            stats: [],
                            _debug: {
                                bandwidthUsageAll: { error: 'getGlobalProperty not available' },
                                channelCount: 0,
                                outputCount: outputs ? outputs.length : 0,
                                channelNames: [],
                                outputNames: outputNames,
                                note: 'No active channels found'
                            }
                        };
                    }
                });
            }

            log('Found ' + channels.length + ' active channel(s) and ' + outputs.length + ' output(s)', 'info');

            // Get all channel names and output names for debug
            const channelNamesPromise = Promise.all(channels.map(ch =>
                ch.getName().catch(() => 'Unknown')
            ));
            const outputNamesPromise = Promise.all(outputs.map((out, idx) =>
                out.getName().then(name => ({ index: idx, name: name })).catch(() => ({ index: idx, name: 'Unknown' }))
            ));

            // Log all channel names and output names to debug matching
            channels.forEach((ch, idx) => {
                ch.getName().then(name => {
                    log('StreamInfo channel ' + (idx + 1) + ' name: "' + name + '"', 'info');
                }).catch(() => { });
            });

            outputs.forEach((out, idx) => {
                out.getName().then(name => {
                    log('Output ' + (idx + 1) + ' name: "' + name + '"', 'info');
                }).catch(() => { });
            });

            // Try to match channels with outputs for better naming
            // In multi-output mode, we might need to use output names or indices
            const statsPromises = channels.map((channel, index) => {
                // Get channel name - use internal _name if available for API calls
                return channel.getName().then(channelName => {
                    // Also get the internal name (used by XJS API calls)
                    // channel._name is the raw HTML-encoded name from recstat (e.g., "Local Streaming&amp;output:0")
                    const internalName = channel._name || channelName;
                    const baseName = channelName.split('&')[0].trim();
                    log('Fetching stats for channel: "' + channelName + '" (internal: "' + internalName + '", _name: "' + (channel._name || 'undefined') + '", index: ' + index + ')', 'info');

                    // Extract output index from name if present (e.g., "&output:0" -> 0, "&output=0" -> 0)
                    // For XBC v2, outputs are 0-indexed: output 1 = "&output=0", output 2 = "&output=1"
                    const outputMatch = channelName.match(/&output[=:](\d+)/);
                    let outputIndex = outputMatch ? parseInt(outputMatch[1]) : index;

                    // Ensure outputIndex is 0 or 1 (for the two outputs per scene)
                    if (outputIndex < 0) outputIndex = 0;
                    if (outputIndex > 1) outputIndex = index % 2;

                    // Check if channel name already has output parameter in correct format
                    const hasOutputParam = channelName.includes('&output=') || channelName.includes('&output:');

                    // Find matching output name to use for API calls
                    // The channel name "YouTubeLive - MiBaDK&output:0" should match output "YouTubeLive - MiBaDK"
                    let matchedOutputName = null;

                    // Try to find matching output
                    const outputMatchPromises = outputs.map((out, idx) => {
                        return out.getName().then(outName => {
                            if (outName === baseName ||
                                (idx === outputIndex && outName.includes(baseName)) ||
                                (baseName.includes(outName.split(' - ')[0]))) {
                                log('Matched channel "' + channelName + '" with output "' + outName + '" (index ' + idx + ')', 'info');
                                if (!matchedOutputName) {
                                    matchedOutputName = outName;
                                }
                            }
                            return null;
                        }).catch(() => null);
                    });

                    // Wait for output matching, then proceed with stats
                    return Promise.all(outputMatchPromises).then(() => {
                        // Use matched output name if found, otherwise use base name from channel
                        const baseStreamName = matchedOutputName || baseName;

                        // If channel name already has output parameter, use it directly
                        // Otherwise, append "&output=0" or "&output=1" to get full stream name and stats
                        let nameToUse;
                        if (hasOutputParam && channelName.includes('&output=')) {
                            // Channel name already has &output= format, use it
                            nameToUse = channelName;
                            log('Using channel name directly (already has output param): "' + nameToUse + '"', 'info');
                        } else if (hasOutputParam && channelName.includes('&output:')) {
                            // Channel name has &output: format, convert to &output= format
                            nameToUse = channelName.replace(/&output:/g, '&output=');
                            log('Converted channel name from &output: to &output=: "' + nameToUse + '"', 'info');
                        } else {
                            // Append output parameter
                            nameToUse = baseStreamName + '&output=' + outputIndex;
                            log('Appended output parameter to base name: "' + nameToUse + '" (output index: ' + outputIndex + ')', 'info');
                        }

                        // Store these in outer scope for error handlers
                        const finalBaseStreamName = baseStreamName;
                        const finalNameToUse = nameToUse;
                        const finalOutputIndex = outputIndex;
                        const internalNameForStats = normalizeInternalNameForStats(internalName, finalOutputIndex);
                        log('Internal name for App.get stats: "' + internalNameForStats + '"', 'info');

                        // Build candidate names (encoded/decoded, colon/equals) to try for App.get stats
                        const candidates = unique([
                            internalNameForStats,
                            encodeAmp(internalNameForStats),
                            internalName,
                            encodeAmp(internalName),
                            channelName,
                            encodeAmp(channelName),
                            baseStreamName + '&output=' + finalOutputIndex,
                            encodeAmp(baseStreamName + '&output=' + finalOutputIndex),
                            baseStreamName + '&output:' + finalOutputIndex,
                            encodeAmp(baseStreamName + '&output:' + finalOutputIndex),
                            baseStreamName
                        ]);

                        // Attempt bandwidthusage-all via GetGlobalProperty (may be blocked in new XBC)
                        const bandwidthUsagePromise = tryGetGlobalProperty('bandwidthusage-all', 1500).then(result => {
                            if (!result) return null;
                            try {
                                const usage = JSON.parse(result);

                                // Match strategy:
                                // 1) exact match on ChannelName after normalizing &output: -> &output=
                                // 2) match by OutputIdx vs outputIndex and ChannelName base
                                let match = null;
                                const normalizedUsage = usage.map(u => {
                                    if (!u || !u.ChannelName) return u;
                                    return Object.assign({}, u, {
                                        ChannelName: String(u.ChannelName).replace(/&amp;output:/g, '&amp;output=').replace(/&output:/g, '&output=')
                                    });
                                });

                                const candList = unique([
                                    ...candidates,
                                    ...candidates.map(c => c.replace(/&amp;output:/g, '&amp;output=').replace(/&output:/g, '&output='))
                                ]);

                                for (const cand of candList) {
                                    match = normalizedUsage.find(u => u && u.ChannelName === cand);
                                    if (match) break;
                                }

                                if (!match) {
                                    // Try match by OutputIdx and baseName
                                    match = normalizedUsage.find(u => {
                                        if (!u) return false;
                                        const idxMatch = String(u.OutputIdx || u.outputIdx || u.outputindex || '') === String(finalOutputIndex);
                                        const baseMatch = u.ChannelName === finalBaseStreamName || (finalBaseStreamName && u.ChannelName && u.ChannelName.indexOf(finalBaseStreamName) >= 0);
                                        return idxMatch && baseMatch;
                                    });
                                }

                                return match || null;
                            } catch (e) {
                                return null;
                            }
                        });

                        // These require bandwidthusage-all; keep as 0 if unavailable
                        const streamInfoGopDropsPromise = Promise.resolve(0);
                        const streamInfoBandwidthPromise = Promise.resolve(0);

                        // Wait for all promises with a timeout to ensure we always return stats
                        const statsPromise = Promise.all([
                            bandwidthUsagePromise,
                            streamInfoGopDropsPromise,
                            streamInfoBandwidthPromise
                        ]).then(([bandwidthUsageMatch, streamGopDrops, streamBandwidth]) => {
                            // Get stats from bandwidthusage-all
                            let finalGopDrops = streamGopDrops;
                            let finalBandwidth = streamBandwidth;

                            if (bandwidthUsageMatch) {
                                // Extract stats from bandwidthusage-all
                                // AvgBitrate appears to be in kilobits, convert to Megabits by dividing by 1000
                                const rawBandwidth = bandwidthUsageMatch.AvgBitrate ||
                                    bandwidthUsageMatch.Bitrate ||
                                    bandwidthUsageMatch.Bandwidth ||
                                    bandwidthUsageMatch.AvgBandwidth || 0;
                                // Convert from kilobits to Megabits (divide by 1000)
                                finalBandwidth = rawBandwidth / 1000;

                                finalGopDrops = bandwidthUsageMatch.Dropped ||
                                    bandwidthUsageMatch.Drops ||
                                    bandwidthUsageMatch.GOPDrops ||
                                    bandwidthUsageMatch.GopDrops || 0;

                                // Log all available fields for debugging
                                log('bandwidthusage-all match fields: ' + Object.keys(bandwidthUsageMatch).join(', '), 'info');
                                log('bandwidthusage-all match full object: ' + JSON.stringify(bandwidthUsageMatch), 'info');

                                log('Using bandwidthusage-all data: bandwidth=' + finalBandwidth + ' Mbps, gopDrops=' + finalGopDrops, 'success');
                            } else {
                                log('No bandwidthusage-all match found/available', 'info');
                            }

                            // Use the full stream name with output parameter for the stats
                            const finalName = nameToUse;
                            log('Final stats for "' + finalName + '": gopDrops=' + finalGopDrops + ', bandwidth=' + finalBandwidth + ' Mbps', 'info');

                            // Ensure all values are serializable - only keep bandwidth and GOP drops
                            const statsObj = {
                                name: String(finalName || 'Unknown'),
                                baseName: String(baseStreamName || 'Unknown'), // Base name without output parameter for display
                                outputIndex: Number(outputIndex) || 0, // 0 for output 1, 1 for output 2
                                gopDrops: Number(finalGopDrops) || 0,
                                bandwidth: Number(finalBandwidth) || 0
                            };

                            log('Created stats object with keys: ' + Object.keys(statsObj).join(', '), 'info');
                            log('Stats object values: name=' + statsObj.name + ', bandwidth=' + statsObj.bandwidth + ', gopDrops=' + statsObj.gopDrops, 'info');

                            // Add debug data (ensure it's serializable)
                            try {
                                statsObj._debug = {
                                    channelName: String(channelName || ''),
                                    internalName: String(internalName || ''),
                                    internalNameForStats: String(internalNameForStats || ''),
                                    baseName: String(baseName || ''),
                                    matchedOutputName: matchedOutputName ? String(matchedOutputName) : null,
                                    nameToUse: String(nameToUse || ''),
                                    outputIndex: Number(outputIndex) || 0,
                                    bandwidthUsageMatch: bandwidthUsageMatch ? JSON.parse(JSON.stringify(bandwidthUsageMatch)) : null
                                };
                            } catch (e) {
                                log('Error creating debug data: ' + e.message, 'error');
                                statsObj._debug = { error: 'Failed to serialize debug data' };
                            }

                            log('Returning stats object with ' + Object.keys(statsObj).length + ' properties', 'info');
                            return statsObj;
                        }).catch(err => {
                            log('Error in stats promise chain: ' + err.message, 'error');
                            log('Error stack: ' + (err.stack || 'No stack'), 'error');
                            // Return minimal stats even on error
                            return {
                                name: String(finalNameToUse || finalBaseStreamName || 'Unknown'),
                                baseName: String(finalBaseStreamName || 'Unknown'),
                                outputIndex: Number(finalOutputIndex) || 0,
                                gopDrops: 0,
                                bandwidth: 0,
                                _debug: {
                                    error: 'Stats promise chain failed: ' + err.message
                                }
                            };
                        });

                        // Add timeout to ensure we always return something
                        return Promise.race([
                            statsPromise,
                            new Promise(resolve => {
                                setTimeout(() => {
                                    log('Stats promise timed out, returning minimal stats', 'error');
                                    resolve({
                                        name: String(finalNameToUse || finalBaseStreamName || 'Unknown'),
                                        baseName: String(finalBaseStreamName || 'Unknown'),
                                        outputIndex: Number(finalOutputIndex) || 0,
                                        gopDrops: 0,
                                        bandwidth: 0,
                                        _debug: {
                                            error: 'Stats promise timed out'
                                        }
                                    });
                                }, 10000); // 10 second timeout (increased from 5s to allow more time for bandwidth usage data)
                            })
                        ]);
                    }).catch(err => {
                        log('Error getting channel name or stats: ' + err.message, 'error');
                        log('Error stack: ' + (err.stack || 'No stack'), 'error');
                        // Return a minimal stats object with error info instead of null
                        return {
                            name: 'Error',
                            baseName: 'Error',
                            outputIndex: index,
                            gopDrops: 0,
                            bandwidth: 0,
                            _debug: {
                                error: err.message,
                                errorStack: err.stack || 'No stack'
                            }
                        };
                    });
                }).catch(err => {
                    log('Error getting channel name or stats: ' + err.message, 'error');
                    log('Error stack: ' + (err.stack || 'No stack'), 'error');
                    // Return a minimal stats object with error info instead of null
                    return {
                        name: 'Error',
                        baseName: 'Error',
                        outputIndex: index,
                        gopDrops: 0,
                        bandwidth: 0,
                        _debug: {
                            error: err.message,
                            errorStack: err.stack || 'No stack'
                        }
                    };
                });
            });

            // IMPORTANT: statsPromises is an array of Promises; resolve it before continuing
            return Promise.all([Promise.all(statsPromises), channelNamesPromise, outputNamesPromise]).then(([stats, channelNames, outputNames]) => {
                // Log all stats before filtering
                log('Raw stats before filtering: ' + stats.length + ' items', 'info');
                stats.forEach((stat, idx) => {
                    if (stat === null) {
                        log('Stat ' + (idx + 1) + ': null', 'info');
                    } else if (typeof stat !== 'object') {
                        log('Stat ' + (idx + 1) + ': not an object, type=' + typeof stat, 'info');
                    } else {
                        log('Stat ' + (idx + 1) + ': keys=' + Object.keys(stat).join(', ') + ', name=' + (stat.name || 'no name'), 'info');
                    }
                });

                // Filter out nulls, invalid objects, and duplicate _shareoutcc streams
                const validStats = stats.filter(s => {
                    if (s === null) return false;
                    if (typeof s !== 'object') return false;
                    const keys = Object.keys(s);
                    if (keys.length === 0) {
                        log('Filtering out stat with no keys', 'info');
                        return false;
                    }
                    // Filter out _shareoutcc variants (duplicates)
                    if (s.name && s.name.includes('_shareoutcc')) {
                        log('Filtering out _shareoutcc duplicate: ' + s.name, 'info');
                        return false;
                    }
                    return true;
                });
                log('Successfully fetched stats for ' + validStats.length + ' stream(s) out of ' + stats.length + ' total', 'info');

                // Log each valid stat object to debug
                validStats.forEach((stat, idx) => {
                    log('Valid stat ' + (idx + 1) + ': name=' + stat.name + ', keys: ' + Object.keys(stat).join(', '), 'info');
                });

                // Also get raw data for debugging (global property + recstat snippets)
                const appGetAvailable = xjs.App && typeof xjs.App.get === 'function';
                const recstatRawPromise = appGetAvailable ? promiseWithTimeout(xjs.App.get('recstat'), 1500, null) : Promise.resolve(null);
                const recstat0RawPromise = appGetAvailable ? promiseWithTimeout(xjs.App.get('recstat:0'), 1500, null) : Promise.resolve(null);
                const recstat1RawPromise = appGetAvailable ? promiseWithTimeout(xjs.App.get('recstat:1'), 1500, null) : Promise.resolve(null);

                return Promise.all([
                    tryGetGlobalProperty('bandwidthusage-all', 1500),
                    recstatRawPromise,
                    recstat0RawPromise,
                    recstat1RawPromise
                ]).then(([bandwidthUsageAllRaw, recstatRaw, recstat0Raw, recstat1Raw]) => {
                    let parsedBandwidthUsage = null;
                    if (bandwidthUsageAllRaw) {
                        try {
                            parsedBandwidthUsage = JSON.parse(bandwidthUsageAllRaw);
                        } catch (e) {
                            parsedBandwidthUsage = { error: e.message, raw: snippet(bandwidthUsageAllRaw, 2000) };
                        }
                    } else {
                        parsedBandwidthUsage = { error: 'getGlobalProperty not available' };
                    }

                    return {
                        stats: validStats,
                        _debug: {
                            bandwidthUsageAll: parsedBandwidthUsage,
                            channelCount: channels.length,
                            outputCount: outputs.length,
                            channelNames: channelNames,
                            outputNames: outputNames,
                            recstatRaw: snippet(recstatRaw, 2000),
                            recstat0Raw: snippet(recstat0Raw, 2000),
                            recstat1Raw: snippet(recstat1Raw, 2000)
                        }
                    };
                }).catch(err => {
                    return {
                        stats: validStats,
                        _debug: {
                            bandwidthUsageAll: { error: 'debug fetch failed', message: err.message },
                            channelCount: channels.length,
                            outputCount: outputs.length,
                            channelNames: channelNames,
                            outputNames: outputNames
                        }
                    };
                });
            });
        }).catch(err => {
            log('Error fetching stream channels: ' + err.message, 'error');
            log('Error stack: ' + (err.stack || 'No stack'), 'error');
            return {
                stats: [],
                _debug: {
                    error: err.message,
                    errorStack: err.stack || 'No stack',
                    channelCount: 0,
                    outputCount: 0,
                    channelNames: [],
                    outputNames: []
                }
            };
        });
    }

    function startStreamStatsUpdates() {
        if (streamStatsInterval) {
            log('Stream stats updates already running', 'info');
            return; // Already running
        }

        log('Starting live stream stats updates', 'info');

        // Update every 2 seconds for live stats
        streamStatsInterval = setInterval(() => {
            if (remoteConnections.size > 0 && isEnabled) {
                getStreamStats().then(result => {
                    lastStreamStats = result.stats;
                    log('Broadcasting stream stats: ' + result.stats.length + ' stream(s)', 'info');
                    log('Debug data available: ' + (result._debug ? 'yes' : 'no'), 'info');
                    if (result._debug) {
                        log('Debug channel count: ' + (result._debug.channelCount || 0) + ', output count: ' + (result._debug.outputCount || 0), 'info');
                    }
                    broadcastStreamStats(result.stats, result._debug);
                }).catch(err => {
                    log('Error fetching stream stats in interval: ' + err.message, 'error');
                    log('Error stack: ' + (err.stack || 'No stack'), 'error');
                    // Send error info to remotes
                    broadcastStreamStats([], {
                        error: err.message,
                        errorStack: err.stack || 'No stack',
                        timestamp: new Date().toISOString()
                    });
                });
            }
        }, 2000);

        // Do initial fetch immediately
        getStreamStats().then(result => {
            lastStreamStats = result.stats;
            log('Initial stream stats: ' + result.stats.length + ' stream(s)', 'info');
            log('Debug data available: ' + (result._debug ? 'yes' : 'no'), 'info');
            if (result._debug) {
                log('Debug channel count: ' + (result._debug.channelCount || 0) + ', output count: ' + (result._debug.outputCount || 0), 'info');
            }
            broadcastStreamStats(result.stats, result._debug);
        }).catch(err => {
            log('Error in initial stream stats fetch: ' + err.message, 'error');
            log('Error stack: ' + (err.stack || 'No stack'), 'error');
            // Send error info to remotes
            broadcastStreamStats([], {
                error: err.message,
                errorStack: err.stack || 'No stack',
                timestamp: new Date().toISOString()
            });
        });
    }

    function stopStreamStatsUpdates() {
        if (streamStatsInterval) {
            clearInterval(streamStatsInterval);
            streamStatsInterval = null;
        }
    }

    function broadcastStreamStats(stats, debugData) {
        if (remoteConnections.size === 0) {
            log('No remotes connected, skipping stream stats broadcast', 'info');
            return;
        }
        if (!websocket || websocket.readyState !== WebSocket.OPEN) {
            log('WebSocket not open, cannot broadcast stream stats', 'error');
            return;
        }

        // Ensure stats are serializable
        const serializableStats = (stats || []).map(stat => {
            try {
                // Test serialization
                JSON.stringify(stat);
                return stat;
            } catch (e) {
                log('Error serializing stat: ' + e.message + ', stat keys: ' + Object.keys(stat).join(', '), 'error');
                // Return a minimal serializable version
                return {
                    name: stat.name || 'Unknown',
                    baseName: stat.baseName || 'Unknown',
                    outputIndex: stat.outputIndex || 0,
                    time: stat.time || 0,
                    drops: stat.drops || 0,
                    gopDrops: stat.gopDrops || 0,
                    renderedFrames: stat.renderedFrames || 0,
                    bandwidth: stat.bandwidth || 0,
                    _debug: { error: 'Serialization failed' }
                };
            }
        });

        // Ensure debug data is serializable
        let serializableDebug = null;
        if (debugData) {
            try {
                // Deep clone to ensure it's serializable
                serializableDebug = JSON.parse(JSON.stringify(debugData));
            } catch (e) {
                log('Error serializing debug data: ' + e.message, 'error');
                serializableDebug = {
                    error: 'Failed to serialize debug data',
                    errorMessage: e.message
                };
            }
        } else {
            serializableDebug = { note: 'No debug data provided' };
        }

        // Broadcast to all remotes via server
        log('Sending stream stats to ' + remoteConnections.size + ' remote(s), ' + serializableStats.length + ' stat(s)', 'info');
        log('Debug data keys: ' + (serializableDebug ? Object.keys(serializableDebug).join(', ') : 'none'), 'info');
        try {
            const message = {
                type: 'broadcast-stream-stats',
                stats: serializableStats,
                _debug: serializableDebug
            };
            // Test serialization of entire message
            const testSerialization = JSON.stringify(message);
            log('Message serialization test: ' + (testSerialization ? 'success, length: ' + testSerialization.length : 'failed'), 'info');
            sendToServer(message);
        } catch (e) {
            log('Error serializing stream stats message: ' + e.message, 'error');
            log('Error stack: ' + (e.stack || 'No stack'), 'error');
        }
    }

    function getSceneItems(sceneId) {
        if (!xjs.Scene) {
            throw new Error('Scene API not available.');
        }

        log('Fetching items for scene: ' + sceneId, 'info');

        // Find the scene by ID
        let scenePromise;

        if (sceneId.match(/^\{[A-F0-9-]+\}$/i)) {
            // Scene UID format
            scenePromise = xjs.Scene.getBySceneUid(sceneId);
        } else if (sceneId.startsWith('scene-')) {
            // Extract index from scene-{index} format
            const index = parseInt(sceneId.replace('scene-', ''), 10);
            if (!isNaN(index)) {
                scenePromise = xjs.Scene.getBySceneIndex(index);
            } else {
                throw new Error('Invalid scene ID format: ' + sceneId);
            }
        } else {
            // Try as numeric scene ID
            const sceneNum = parseInt(sceneId, 10);
            if (!isNaN(sceneNum)) {
                scenePromise = xjs.Scene.getBySceneIndex(sceneNum);
            } else {
                throw new Error('Invalid scene ID format: ' + sceneId);
            }
        }

        return scenePromise.then(scene => {
            return scene.getItems();
        }).then(items => {
            log('Found ' + items.length + ' item(s) in scene', 'info');

            // Get details for each item
            const itemPromises = items.map(item => {
                // Helper to safely get visibility
                // For AudioItem, use isMute() since AudioItem doesn't have isVisible method
                const getVisibility = () => {
                    if (typeof item.isVisible === 'function') {
                        return item.isVisible().catch(() => true);
                    }

                    // AudioItem doesn't have isVisible - use isMute instead
                    // visible = !muted (unmuted audio is "visible", muted audio is "hidden")
                    const isAudioItem = typeof item.isMute === 'function';

                    if (isAudioItem) {
                        return item.isMute().then(muted => !muted).catch(() => true);
                    }

                    // Fallback: assume visible if method doesn't exist
                    return Promise.resolve(true);
                };

                return Promise.all([
                    item.getId(),
                    item.getName().catch(() => 'Unknown'),
                    item.getCustomName().catch(() => null),
                    getVisibility(),
                    item.getType().catch(() => null)
                ]).then(([id, name, customName, visible, type]) => {
                    return {
                        id: id,
                        name: name,
                        customName: customName,
                        visible: visible,
                        type: type
                    };
                }).catch(err => {
                    log('Error getting item details: ' + err.message, 'error');
                    return null;
                });
            });

            return Promise.all(itemPromises).then(itemDetails => {
                const validItems = itemDetails.filter(item => item !== null);
                log('Retrieved details for ' + validItems.length + ' item(s)', 'info');
                return { items: validItems };
            });
        }).catch(err => {
            log('Error getting scene items: ' + err.message, 'error');
            throw err;
        });
    }

    function setItemVisible(itemId, visible) {
        if (!xjs.Scene) {
            throw new Error('Scene API not available.');
        }

        log('Setting item visibility: ' + itemId + ' to ' + visible, 'info');

        // Search for the item across all scenes
        return xjs.Scene.searchItemsById(itemId).then(item => {
            if (!item) {
                throw new Error('Item not found: ' + itemId);
            }

            // Check if the item has setVisible method (most item types have it via ItemTransition mixin)
            if (typeof item.setVisible === 'function') {
                log('Using item.setVisible() method', 'info');
                return item.setVisible(visible).then(() => {
                    log('Successfully set item visibility via setVisible()', 'success');
                    return {
                        success: true,
                        itemId: itemId,
                        visible: visible,
                        message: 'Item visibility updated'
                    };
                });
            }

            // AudioItem doesn't have ItemTransition mixin, so it doesn't have setVisible
            // For audio items, use setMute instead - hiding = muting, showing = unmuting
            // Note: This is the only control available for audio items in XJS framework
            const isAudioItem = typeof item.setMute === 'function';

            if (isAudioItem) {
                log('Detected AudioItem (no setVisible available), using setMute() instead', 'info');
                const shouldMute = !visible; // visible=true -> unmute, visible=false -> mute

                return item.setMute(shouldMute).then(() => {
                    log('Successfully set audio item mute=' + shouldMute + ' (visible=' + visible + ')', 'success');
                    return {
                        success: true,
                        itemId: itemId,
                        visible: visible,
                        muted: shouldMute,
                        isAudioItem: true,
                        message: 'Audio item mute state updated (XJS AudioItem has no visibility control)'
                    };
                });
            }

            // Fallback error - item has neither setVisible nor setMute
            throw new Error('Item type does not support visibility toggle (no setVisible or setMute method)');
        }).catch(err => {
            log('Error setting item visibility: ' + err.message, 'error');
            throw err;
        });
    }

    function log(message, level = 'info') {
        // Ensure logOutput exists before trying to append
        if (!logOutput) {
            console.log(`[${level.toUpperCase()}] ${message}`);
            return;
        }

        try {
            const timestamp = new Date().toLocaleTimeString();
            const entry = document.createElement('div');
            entry.className = 'log-entry ' + level;
            entry.textContent = `[${timestamp}] ${message}`;
            logOutput.appendChild(entry);
            logOutput.scrollTop = logOutput.scrollHeight;
        } catch (err) {
            console.error('Error logging:', err);
            console.log(`[${level.toUpperCase()}] ${message}`);
        }
    }

})();

