/**
 * LogPanel Component
 * Handles the display and interaction with the research log panel
 * Used by both progress.js and results.js
 */
(function() {
    // XSS protection for values rendered via innerHTML
    // bearer:disable javascript_lang_manual_html_sanitization
    const escapeHtmlFallback = (str) => String(str || '').replace(/[&<>"']/g, (m) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);
    const escapeHtml = window.escapeHtml || escapeHtmlFallback;

    // Shared log helpers extracted to utils/log-helpers.js for testability
    const {
        checkLogVisibility,
        hashString,
        normalizeMessage,
        normalizeTimestamps,
    } = window.LdrLogHelpers;

    // Maximum number of log entries to keep in DOM to prevent unbounded growth.
    // Seeded from window.LDR_LOG_LIMITS (set in base.html from Python's
    // HISTORY_LOGS_DEFAULT_LIMIT in constants.py) so this DOM cap and the
    // shared pagination default come from one source instead of drifting.
    // Falls back to 500 if the injection is missing (e.g. in a unit-test
    // harness without templates).
    const MAX_LOG_ENTRIES = window.LDR_LOG_LIMITS?.default ?? 500;

    // Shared state for log panel
    window._logPanelState ||= {
        expanded: false,
        queuedLogs: [],
        logCount: 0,
        initialized: false, // Track initialization state
        connectedResearchId: null, // Track which research we're connected to
        currentFilter: 'all', // Track current filter type
        autoscroll: true, // Track whether autoscroll is enabled.
    };

    /**
     * Initialize the log panel
     * @param {string} researchId - Optional research ID to load logs for
     */
    function initializeLogPanel(researchId = null) {
        // Check if already initialized
        if (window._logPanelState.initialized) {
            SafeLogger.log('Log panel already initialized, checking if research ID has changed');

            // If we're already connected to this research, do nothing
            if (window._logPanelState.connectedResearchId === researchId) {
                SafeLogger.log('Already connected to research ID:', researchId);
                return;
            }

            // If the research ID has changed, we'll update our connection
            SafeLogger.log('Research ID changed from', window._logPanelState.connectedResearchId, 'to', researchId);
            window._logPanelState.connectedResearchId = researchId;

            // Reset per-research state for the new research. queuedLogs is
            // cleared because any queued entries belong to the previous
            // research_id and would mis-attribute to the new one. expanded
            // is synced from the DOM (not reset to false) so a panel the
            // user had open for research N stays open for research N+1 —
            // otherwise new socket entries would queue invisibly until the
            // user manually re-toggled.
            window._logPanelState.queuedLogs = [];
            window._logPanelState.currentFilter = 'all';

            const logPanelContentEl = document.getElementById('log-panel-content') ||
                                       document.getElementById('logPanel');
            window._logPanelState.expanded = logPanelContentEl
                ? !logPanelContentEl.classList.contains('collapsed')
                : false;

            // Reset filter buttons visual state
            const filterBtns = document.querySelectorAll('.ldr-log-filter .ldr-filter-buttons button');
            filterBtns.forEach(btn => btn.classList.remove('ldr-selected'));
            const allBtn = Array.from(filterBtns).find(btn => btn.textContent.toLowerCase() === 'all');
            if (allBtn) allBtn.classList.add('ldr-selected');

            // Clear container of the previous research's log entries (they
            // are stale for the new research) and reset the loaded marker
            // so the next expand triggers a fresh fetch. Then bail out:
            // toggle/visibility handlers from the first init still apply
            // and re-running the rest of init would either duplicate
            // handlers or wipe socket entries that have already arrived
            // for the new research.
            const consoleLogContainer = document.getElementById('console-log-container');
            if (consoleLogContainer) {
                consoleLogContainer.innerHTML = '<div class="ldr-empty-log-message">No logs available. Expand panel to load logs.</div>';
            }
            if (logPanelContentEl) {
                delete logPanelContentEl.dataset.loaded;
            }
            return;
        }

        // Add callback for log download button.
        const downloadButton = document.getElementById('log-download-button');
        if (downloadButton) {
            downloadButton.addEventListener('click', downloadLogs);
        }

        SafeLogger.log('Initializing shared log panel, research ID:', researchId);

        // Check if we're on a research-specific page (progress, results)
        const isResearchPage = window.location.pathname.includes('/progress/') ||
                              window.location.pathname.includes('/results/') ||
                              window.location.pathname.includes('/chat/') ||
                              document.getElementById('research-progress') ||
                              document.getElementById('research-results');

        // Get all log panels on the page (there might be duplicates)
        const logPanels = document.querySelectorAll('.ldr-collapsible-log-panel');

        if (logPanels.length > 1) {
            SafeLogger.warn(`Found ${logPanels.length} log panels, removing duplicates`);

            // Keep only the first one and remove others
            for (let i = 1; i < logPanels.length; i++) {
                SafeLogger.log(`Removing duplicate log panel #${i}`);
                logPanels[i].remove();
            }
        } else if (logPanels.length === 0) {
            SafeLogger.error('No log panel found in the DOM!');
            return;
        }

        // Get log panel elements with both old and new names for compatibility
        let logPanelToggle = document.getElementById('log-panel-toggle');
        let logPanelContent = document.getElementById('log-panel-content');

        // Fallback to the old element IDs if needed
        if (!logPanelToggle) logPanelToggle = document.getElementById('logToggle');
        if (!logPanelContent) logPanelContent = document.getElementById('logPanel');

        if (!logPanelToggle || !logPanelContent) {
            SafeLogger.warn('Log panel elements not found, skipping initialization');
            return;
        }

        // Clear loaded flag so logs are re-fetched for the new research ID
        if (window._logPanelState.initialized) {
            delete logPanelContent.dataset.loaded;
        }

        const autoscrollButton = document.querySelector('#log-autoscroll-button');

        // Handle visibility based on page type
        if (!isResearchPage) {
            SafeLogger.log('Not on a research-specific page, hiding log panel');

            // Hide the log panel on non-research pages
            const panel = logPanelContent.closest('.ldr-collapsible-log-panel');
            if (panel) {
                panel.style.display = 'none';
            } else if (logPanelContent.parentElement) {
                logPanelContent.parentElement.style.display = 'none';
            } else {
                logPanelContent.style.display = 'none';
            }
            return;
        }
        // Ensure log panel is visible on research pages
        SafeLogger.log('On a research page, ensuring log panel is shown');
        const panel = logPanelContent.closest('.ldr-collapsible-log-panel');
        if (panel) {
            panel.style.display = 'flex';
        }

        SafeLogger.log('Log panel elements found, setting up handlers');

        // Mark as initialized to prevent double initialization
        window._logPanelState.initialized = true;

        // Check for CSS issue - if the panel's computed style has display:none, the panel won't be visible
        const computedStyle = window.getComputedStyle(logPanelContent);
        SafeLogger.log('Log panel CSS visibility:', {
            display: computedStyle.display,
            visibility: computedStyle.visibility,
            height: computedStyle.height,
            overflow: computedStyle.overflow
        });

        // Ensure the panel is visible in the DOM
        if (computedStyle.display === 'none') {
            SafeLogger.warn('Log panel has display:none - forcing display:flex');
            logPanelContent.style.display = 'flex';
        }

        // Ensure we have a console log container
        const consoleLogContainer = document.getElementById('console-log-container');
        if (!consoleLogContainer) {
            SafeLogger.error('Console log container not found, logs will not be displayed');
        } else {
            // Add placeholder message
            consoleLogContainer.innerHTML = '<div class="ldr-empty-log-message">No logs available. Expand panel to load logs.</div>';
        }

        // Abort previous event handlers to prevent stacking on re-init
        if (window._logPanelState._handlersAbort) {
            window._logPanelState._handlersAbort.abort();
        }
        const handlersAbort = new AbortController();
        window._logPanelState._handlersAbort = handlersAbort;

        // Set up toggle click handler
        logPanelToggle.addEventListener('click', function() {
            SafeLogger.log('Log panel toggle clicked');

            // Toggle collapsed state
            logPanelContent.classList.toggle('collapsed');
            logPanelToggle.classList.toggle('collapsed');

            const collapsed = logPanelContent.classList.contains('collapsed');
            logPanelToggle.setAttribute('aria-expanded', String(!collapsed));

            const toggleIcon = logPanelToggle.querySelector('.ldr-toggle-icon');
            if (toggleIcon && !collapsed) {
                // Load logs if not already loaded. dataset.loaded is set by
                // loadLogsForResearch only on a successful non-empty fetch,
                // so an earlier empty response does not suppress retries.
                // Read the id live from _logPanelState rather than the closure:
                // on /chat/ pages the panel is first initialized with a null id
                // (the URL carries a session id, not a research id) and the real
                // id only arrives later via window.logPanel.initialize(), whose
                // re-init path updates connectedResearchId without rebinding this
                // handler. Using the stale closure id meant chat pages never
                // loaded historical logs when the panel was expanded.
                const activeResearchId = researchId || window._logPanelState.connectedResearchId;
                if (!logPanelContent.dataset.loaded && activeResearchId) {
                    SafeLogger.log('First expansion of log panel, loading logs');
                    loadLogsForResearch(activeResearchId);
                }

                // Process any queued logs
                if (window._logPanelState.queuedLogs.length > 0) {
                    SafeLogger.log(`Processing ${window._logPanelState.queuedLogs.length} queued logs`);
                    window._logPanelState.queuedLogs.forEach(logEntry => {
                        addLogEntryToPanel(logEntry, false);
                    });
                    window._logPanelState.queuedLogs = [];
                }
            }

            // Default to showing the autoscroll button.
            if (autoscrollButton !== null) {
                autoscrollButton.style.display = 'inline';
            }

            const logPanel = document.querySelector('.ldr-collapsible-log-panel');
            const isProgressPage = document.querySelector('#research-progress') !== null;
            if (logPanel !== null) {
                logPanel.classList.toggle('ldr-expanded', !collapsed && isProgressPage);
            }
            if (!collapsed && logPanel !== null && isProgressPage) {
                logPanel.style.height = '';
                // Start with autoscroll on when expanding.
                window._logPanelState.autoscroll = false;
                toggleAutoscroll();
            } else if (logPanel !== null) {
                // Use the default height.
                logPanel.style.height = 'auto';
                // Hide the autoscroll button since it doesn't make
                // sense in this context.
                if (autoscrollButton !== null) {
                    autoscrollButton.style.display = 'none';
                }
            }

            // Track expanded state
            window._logPanelState.expanded = !collapsed;
        }, { signal: handlersAbort.signal });

        if (autoscrollButton) {
            // Set up autoscroll handler for the log panel. When autoscroll is
            // enabled, it will automatically scroll as new logs are added.
            autoscrollButton.addEventListener('click', toggleAutoscroll, { signal: handlersAbort.signal });
        }

        // Set up filter button click handlers
        const filterButtons = document.querySelectorAll('.ldr-log-filter .ldr-filter-buttons button');
        filterButtons.forEach(button => {
            button.addEventListener('click', function() {
                const type = this.textContent.toLowerCase();
                SafeLogger.log(`Filtering logs by type: ${type}`);

                // Update active state
                filterButtons.forEach(btn => btn.classList.remove('ldr-selected'));
                this.classList.add('ldr-selected');

                // Apply filtering
                filterLogsByType(type);
            }, { signal: handlersAbort.signal });
        });

        // Start with panel collapsed and fix initial chevron direction
        logPanelContent.classList.add('collapsed');
        const initialToggleIcon = logPanelToggle.querySelector('.ldr-toggle-icon');
        if (initialToggleIcon) {
            initialToggleIcon.className = 'fas fa-chevron-right ldr-toggle-icon';
        }

        // Initialize the log count
        const logIndicators = document.querySelectorAll('.ldr-log-indicator');
        if (logIndicators.length > 0) {
            // Set count on all indicators
            logIndicators.forEach(indicator => {
                indicator.textContent = '0';
            });

            // Skip the API call when there is no researchId (e.g. on a
            // freshly-loaded /chat/ page before a research has started).
            // URLBuilder.historyLogCount(null) would otherwise produce a
            // /history/log_count/null request that 404s on every load.
            if (researchId) {
                // Fetch the log count from the API and update the indicators
                fetch(URLBuilder.historyLogCount(researchId))
                    .then(response => response.json())
                    .then(data => {
                        SafeLogger.log('Log count data:', data);
                        if (data && typeof data.total_logs === 'number') {
                            logIndicators.forEach(indicator => {
                                indicator.textContent = data.total_logs;
                            });
                        } else {
                            SafeLogger.error('Invalid log count data received from API');
                        }
                    })
                    .catch(error => {
                        SafeLogger.error('Error fetching log count:', error);
                    });
            }
        } else {
            SafeLogger.warn('No log indicators found for initialization');
        }

        // Check CSS display property of the log panel
        const logPanel = document.querySelector('.ldr-collapsible-log-panel');
        if (logPanel) {
            const panelStyle = window.getComputedStyle(logPanel);
            SafeLogger.log('Log panel CSS display:', panelStyle.display);

            if (panelStyle.display === 'none') {
                SafeLogger.warn('Log panel has CSS display:none - forcing display:flex');
                logPanel.style.display = 'flex';
            }
        }

        // Pre-fetch logs in the background so an opened panel has historical
        // entries ready, and so the API races (empty response in 0-100ms
        // window after research start) self-heal once entries exist.
        // dataset.loaded is set inside loadLogsForResearch only on success;
        // an empty response leaves it unset so a later toggle re-fetches.
        if (researchId && !logPanelContent.dataset.loaded) {
            loadLogsForResearch(researchId);
        }

        // Pre-load logs if hash includes #logs
        // timing comparison on URL hash, not secrets
        // bearer:disable javascript_lang_observable_timing
        if (window.location.hash === '#logs' && researchId) {
            SafeLogger.log('Auto-loading logs due to #logs in URL');
            setTimeout(() => {
                logPanelToggle.click();
            }, 500);
        }

        // DEBUG: Force expand the log panel if URL has debug parameter
        if (window.location.search.includes('debug=logs') || window.location.hash.includes('debug')) {
            SafeLogger.log('DEBUG: Force-expanding log panel');
            setTimeout(() => {
                if (logPanelContent.classList.contains('collapsed')) {
                    logPanelToggle.click();
                }
            }, 800);
        }

        // Register global functions to ensure they work across modules
        window.addConsoleLog = addConsoleLog;
        window.filterLogsByType = filterLogsByType;

        // Add a connector to socket.js
        // Track when we last received this exact message to avoid re-adding within 10 seconds
        const processedMessages = new Map();
        window._socketAddLogEntry = function(logEntry) {
            // Simple message deduplication for socket events
            const message = logEntry.message || logEntry.content || '';
            const messageKey = `${message}-${logEntry.type || 'info'}`;
            const now = Date.now();

            // Check if we've seen this message recently (within 10 seconds)
            if (processedMessages.has(messageKey)) {
                const lastProcessed = processedMessages.get(messageKey);
                const timeDiff = now - lastProcessed;

                if (timeDiff < 10000) { // 10 seconds
                    SafeLogger.log(`Skipping duplicate socket message received within ${timeDiff}ms:`, message);
                    return;
                }
            }

            // Update our tracking
            processedMessages.set(messageKey, now);

            // Clean up old entries (keep map from growing indefinitely)
            if (processedMessages.size > 100) {
                // Remove entries older than 60 seconds
                for (const [key, timestamp] of processedMessages.entries()) {
                    if (now - timestamp > 60000) {
                        processedMessages.delete(key);
                    }
                }
            }

            // Process the log entry
            addLogEntryToPanel(logEntry);
        };

        SafeLogger.log('Log panel initialized');
    }

    /**
     * @brief Toggles autoscroll on or off.
     */
    function toggleAutoscroll() {
        window._logPanelState.autoscroll = !window._logPanelState.autoscroll;

        const autoscrollButton = document.querySelector('#log-autoscroll-button');
        const consoleLogContainer = document.getElementById('console-log-container');
        if (!autoscrollButton || !consoleLogContainer) {
            SafeLogger.error("Autoscroll button or console log container not found.");
            return;
        }

        // Highlight the autoscroll button in purple when it's
        // enabled to make that clear.
        if (window._logPanelState.autoscroll) {
            autoscrollButton.classList.add('ldr-selected');
            // Immediately scroll to the top of the panel (newest logs are at top).
            consoleLogContainer.scrollTop = 0;
        } else {
            autoscrollButton.classList.remove('ldr-selected');
        }
    }

    /**
     * @brief Fetches all the logs for a research instance from the API.
     * @param researchId The ID of the research instance.
     * @returns {Promise<any>} The logs.
     */
    async function fetchLogsForResearch(researchId, limit) {
        // Pass an explicit limit to the API so the server doesn't return
        // (and we don't have to parse) more rows than the panel will keep.
        // Live load uses MAX_LOG_ENTRIES; download uses the server-side
        // hard cap (5000) so users still get the full tail.
        const response = await fetch(URLBuilder.researchLogs(researchId, limit));
        return await response.json();
    }

    /**
     * Load logs for a specific research
     * @param {string} researchId - The research ID to load logs for
     */
    async function loadLogsForResearch(researchId) {
        // In-flight guard: if a fetch for this research is already pending
        // (e.g. pre-fetch from initializeLogPanel hasn't resolved yet and the
        // user expanded the panel), don't fire a second request.
        const panelEl = document.getElementById('log-panel-content') || document.getElementById('logPanel');
        if (panelEl && panelEl.dataset.loading === 'true') {
            SafeLogger.log('loadLogsForResearch already in flight, skipping duplicate');
            return;
        }
        if (panelEl) {
            panelEl.dataset.loading = 'true';
        }

        try {
            // Show loading state, but only if the container has no live
            // entries yet — otherwise we'd clobber socket-driven logs that
            // arrived before this fetch completes.
            const logContent = document.getElementById('console-log-container');
            if (logContent && !logContent.querySelector('.ldr-console-log-entry')) {
                logContent.innerHTML = '<div class="ldr-loading-spinner ldr-centered"><div class="ldr-spinner"></div><div style="margin-left: 10px;">Loading logs...</div></div>';
            }

            SafeLogger.log('Loading logs for research ID:', researchId);

            const data = await fetchLogsForResearch(researchId, MAX_LOG_ENTRIES);
            SafeLogger.log('Logs API response:', data);

            // Initialize array to hold all logs from different sources
            const allLogs = [];

            // Track seen messages to avoid duplicate content with different timestamps
            const seenMessages = new Map();

            // Process progress_log if available
            if (data.progress_log && typeof data.progress_log === 'string') {
                try {
                    const progressLogs = JSON.parse(data.progress_log);
                    if (Array.isArray(progressLogs) && progressLogs.length > 0) {
                        SafeLogger.log(`Found ${progressLogs.length} logs in progress_log`);

                        // Process progress logs
                        progressLogs.forEach(logItem => {
                            if (!logItem.time || !logItem.message) return; // Skip invalid logs

                            // Skip if we've seen this exact message before
                            const messageKey = normalizeMessage(logItem.message);
                            if (seenMessages.has(messageKey)) {
                                // Only consider logs within 1 minute of each other as duplicates
                                const previousLog = seenMessages.get(messageKey);
                                const previousTime = new Date(previousLog.time);
                                const currentTime = new Date(logItem.time);
                                const timeDiff = Math.abs(currentTime - previousTime) / 1000; // in seconds

                                if (timeDiff < 60) { // Within 1 minute
                                    // Use the newer timestamp if available
                                    if (currentTime > previousTime) {
                                        previousLog.time = logItem.time;
                                    }
                                    return; // Skip this duplicate
                                }

                                // If we get here, it's the same message but far apart in time (e.g., a repeated step)
                                // We'll include it as a separate entry
                            }

                            // Determine log type based on metadata
                            let logType = 'info';
                            if (logItem.metadata) {
                                if (logItem.metadata.phase === 'iteration_complete' ||
                                    logItem.metadata.phase === 'report_complete' ||
                                    logItem.metadata.phase === 'complete' ||
                                    logItem.metadata.is_milestone === true) {
                                    logType = 'milestone';
                                } else if (logItem.metadata.phase === 'error') {
                                    logType = 'error';
                                }
                            }

                            // Add message keywords for better type detection
                            if (logType !== 'milestone') {
                                const msg = logItem.message.toLowerCase();
                                if (msg.includes('complete') ||
                                    msg.includes('finished') ||
                                    msg.includes('starting phase') ||
                                    msg.includes('generated report')) {
                                    logType = 'milestone';
                                } else if (msg.includes('error') || msg.includes('failed')) {
                                    logType = 'error';
                                }
                            }

                            // Create a log entry object with a unique ID for deduplication
                            const logEntry = {
                                id: `${logItem.time}-${hashString(logItem.message)}`,
                                time: logItem.time,
                                message: logItem.message,
                                type: logType,
                                metadata: logItem.metadata || {},
                                source: 'progress_log'
                            };

                            // Track this message to avoid showing exact duplicates with different timestamps
                            seenMessages.set(messageKey, logEntry);

                            // Add to all logs array
                            allLogs.push(logEntry);
                        });
                    }
                } catch (e) {
                    SafeLogger.error('Error parsing progress_log:', e);
                }
            }

            // Standard logs array processing
            // Check if data is directly an array (new format) or has a logs property (old format)
            const logsArray = Array.isArray(data) ? data : (data && data.logs);

            if (logsArray && Array.isArray(logsArray)) {
                SafeLogger.log(`Processing ${logsArray.length} standard logs`);

                // Process each standard log
                logsArray.forEach(log => {
                    if (!log.timestamp && !log.time) return; // Skip invalid logs

                    // Skip duplicates based on message content
                    const messageKey = normalizeMessage(log.message || log.content || '');
                    if (seenMessages.has(messageKey)) {
                        // Only consider logs within 1 minute of each other as duplicates
                        const previousLog = seenMessages.get(messageKey);
                        const previousTime = new Date(previousLog.time);
                        const currentTime = new Date(log.timestamp || log.time);
                        const timeDiff = Math.abs(currentTime - previousTime) / 1000; // in seconds

                        if (timeDiff < 60) { // Within 1 minute
                            // Use the newer timestamp if available
                            if (currentTime > previousTime) {
                                previousLog.time = log.timestamp || log.time;
                            }
                            return; // Skip this duplicate
                        }
                    }

                    // Create standardized log entry
                    const logEntry = {
                        id: `${log.timestamp || log.time}-${hashString(log.message || log.content || '')}`,
                        time: log.timestamp || log.time,
                        message: log.message || log.content || 'No message',
                        type: log.log_type || log.type || log.level || 'info',
                        metadata: log.metadata || {},
                        source: 'standard_logs'
                    };

                    // Track this message
                    seenMessages.set(messageKey, logEntry);

                    // Add to all logs array
                    allLogs.push(logEntry);
                });
            }

            const panelContent = document.getElementById('log-panel-content') || document.getElementById('logPanel');

            // Clear container
            if (logContent) {
                if (allLogs.length === 0) {
                    // If socket events populated logs while this fetch was
                    // in flight, don't clobber them with the empty placeholder.
                    const hasLiveEntries = logContent.querySelector('.ldr-console-log-entry');
                    if (!hasLiveEntries) {
                        logContent.innerHTML = '<div class="ldr-empty-log-message">No logs available for this research.</div>';
                    }
                    // Leave dataset.loaded unset so a future toggle re-fetches
                    // once the backend has flushed log rows.
                    if (panelContent) {
                        delete panelContent.dataset.loaded;
                    }
                    return;
                }

                normalizeTimestamps(allLogs);

                // Deduplicate logs by ID and sort by timestamp (oldest first)
                const uniqueLogsMap = new Map();
                allLogs.forEach(log => {
                    uniqueLogsMap.set(log.id, log);
                });
                const uniqueLogs = Array.from(uniqueLogsMap.values());
                const sortedLogs = uniqueLogs.sort((a, b) => {
                    return new Date(b.time) - new Date(a.time);
                });

                SafeLogger.log(`Displaying ${sortedLogs.length} logs after deduplication (from original ${allLogs.length})`);

                // If socket events populated entries while this fetch was
                // in flight, append via addLogEntryToPanel (which dedupes by
                // id and message) instead of clobbering with innerHTML = ''.
                const hasLiveEntries = logContent.querySelector('.ldr-console-log-entry');
                if (hasLiveEntries) {
                    sortedLogs.forEach(logEntry => addLogEntryToPanel(logEntry, false));
                    if (panelContent) {
                        panelContent.dataset.loaded = 'true';
                    }
                    return;
                }

                logContent.innerHTML = '';

                // Batch DOM insert using DocumentFragment (O(1) reflow vs O(n))
                // sortedLogs is newest-first, but DOM needs [oldest, ..., newest]
                // for column-reverse CSS to show newest at visual top
                const fragment = document.createDocumentFragment();
                for (let i = sortedLogs.length - 1; i >= 0; i--) {
                    const element = createLogEntryElement(sortedLogs[i]);
                    if (element) {
                        fragment.appendChild(element);
                    }
                }
                logContent.appendChild(fragment);

                // Prune oldest entries (at DOM start, since oldest-first ordering)
                while (logContent.children.length > MAX_LOG_ENTRIES) {
                    logContent.firstElementChild.remove();
                }

                // Update log count indicator
                const logIndicators = document.querySelectorAll('.ldr-log-indicator');
                if (logIndicators.length > 0) {
                    logIndicators.forEach(indicator => {
                        indicator.textContent = logContent.children.length;
                    });
                }

                // Mark loaded only after a successful non-empty fetch so an
                // empty initial response doesn't permanently suppress retries.
                if (panelContent) {
                    panelContent.dataset.loaded = 'true';
                }
            }

        } catch (error) {
            SafeLogger.error('Error loading logs:', error);

            // Show error in log panel
            // SECURITY: error.message can contain arbitrary text — must escape before innerHTML
            const logContent = document.getElementById('console-log-container');
            if (logContent) {
                // bearer:disable javascript_lang_dangerous_insert_html
                logContent.innerHTML = `<div class="ldr-error-message">Error loading logs: ${escapeHtml(error.message)}</div>`;
            }
        } finally {
            if (panelEl) {
                delete panelEl.dataset.loading;
            }
        }
    }

    /**
     * Add a log entry to the console - public API
     * @param {string} message - Log message
     * @param {string} level - Log level (info, milestone, error)
     * @param {Object} metadata - Optional metadata
     */
    function addConsoleLog(message, level = 'info', metadata = null) {
        SafeLogger.log(`[${level.toUpperCase()}] ${message}`);

        const timestamp = new Date().toISOString();
        const logEntry = {
            id: `${timestamp}-${hashString(message)}`,
            time: timestamp,
            message,
            type: level,
            metadata: metadata || { type: level }
        };

        // Queue log entries if panel is not expanded yet
        if (!window._logPanelState.expanded) {
            window._logPanelState.queuedLogs.push(logEntry);
            SafeLogger.log('Queued log entry for later display');

            // Update log count even if not displaying yet
            updateLogCounter(1);

            // Auto-expand log panel on first log
            const logPanelToggle = document.getElementById('log-panel-toggle');
            if (logPanelToggle) {
                SafeLogger.log('Auto-expanding log panel because logs are available');
                logPanelToggle.click();
            }

            return;
        }

        // Add directly to panel if it's expanded
        addLogEntryToPanel(logEntry, true);
    }

    /**
     * Create a DOM element for a log entry without inserting it.
     * Used by both addLogEntryToPanel() for live logs and batch loading via DocumentFragment.
     * @param {Object} logEntry - The log entry data
     * @returns {HTMLElement|null} - The created element, or null on failure
     */
    function createLogEntryElement(logEntry) {
        // Ensure the log entry has an ID
        if (!logEntry.id) {
            const timestamp = logEntry.time || logEntry.timestamp || new Date().toISOString();
            const message = logEntry.message || logEntry.content || 'No message';
            logEntry.id = `${timestamp}-${hashString(message)}`;
        }

        // Get the log template
        const template = document.getElementById('console-log-entry-template');

        // Determine log level - CHECK FOR DIRECT TYPE FIELD FIRST
        let logLevel = 'info';
        if (logEntry.type) {
            logLevel = logEntry.type;
        } else if (logEntry.metadata && logEntry.metadata.type) {
            logLevel = logEntry.metadata.type;
        } else if (logEntry.level) {
            logLevel = logEntry.level;
        }

        // Format timestamp
        const timestamp = new Date(logEntry.time || logEntry.timestamp || new Date());
        const timeStr = timestamp.toLocaleTimeString();

        // Get message
        const message = logEntry.message || logEntry.content || 'No message';

        let element;

        if (template) {
            // Create a new log entry from the template
            const entry = document.importNode(template.content, true);
            element = entry.querySelector('.ldr-console-log-entry');

            // Add the log type as data attribute for filtering
            if (element) {
                element.dataset.logType = logLevel.toLowerCase();
                element.classList.add(`ldr-log-${logLevel.toLowerCase()}`);
                // Initialize counter for duplicate tracking
                element.dataset.counter = '1';
                // Store log ID for deduplication
                if (logEntry.id) {
                    element.dataset.logId = logEntry.id;
                }

                // Add special attribute for engine selection events
                if (logEntry.metadata && logEntry.metadata.phase === 'engine_selected') {
                    element.dataset.engineSelected = 'true';
                    // Store engine name as a data attribute
                    if (logEntry.metadata.engine) {
                        element.dataset.engine = logEntry.metadata.engine;
                    }
                }

                element.dataset.logTimeMs = Number.isNaN(timestamp.getTime())
                    ? String(Date.now())
                    : String(timestamp.getTime());
            }

            // Set content
            entry.querySelector('.ldr-log-timestamp').textContent = timeStr;
            entry.querySelector('.ldr-log-badge').textContent = logLevel.charAt(0).toUpperCase() + logLevel.slice(1);
            entry.querySelector('.ldr-log-message').textContent = message;
        } else {
            // Create a simple log entry without template
            element = document.createElement('div');
            element.className = 'ldr-console-log-entry';
            element.dataset.logType = logLevel.toLowerCase();
            element.classList.add(`ldr-log-${logLevel.toLowerCase()}`);
            element.dataset.counter = '1';
            if (logEntry.id) {
                element.dataset.logId = logEntry.id;
            }

            element.dataset.logTimeMs = Number.isNaN(timestamp.getTime())
                ? String(Date.now())
                : String(timestamp.getTime());

            // Create log content
            // bearer:disable javascript_lang_dangerous_insert_html
            element.innerHTML = `
                <span class="ldr-log-timestamp">${escapeHtml(timeStr)}</span>
                <span class="ldr-log-badge">${escapeHtml(logLevel.charAt(0).toUpperCase() + logLevel.slice(1))}</span>
                <span class="ldr-log-message">${escapeHtml(message)}</span>
            `;
        }

        // Apply visibility based on current filter
        if (element) {
            const currentFilter = window._logPanelState.currentFilter || 'all';
            const shouldShow = checkLogVisibility(logLevel.toLowerCase(), currentFilter);
            element.style.display = shouldShow ? '' : 'none';
        }

        return element;
    }

    /**
     * Add a log entry directly to the panel
     * @param {Object} logEntry - The log entry to add
     * @param {boolean} incrementCounter - Whether to increment the log counter
     */
    function addLogEntryToPanel(logEntry, incrementCounter = true) {
        SafeLogger.log('Adding log entry to panel:', logEntry);

        const consoleLogContainer = document.getElementById('console-log-container');
        if (!consoleLogContainer) {
            SafeLogger.warn('Console log container not found');
            return;
        }

        // Clear empty message if present
        const emptyMessage = consoleLogContainer.querySelector('.ldr-empty-log-message');
        if (emptyMessage) {
            emptyMessage.remove();
        }

        // Clear the "Loading logs..." spinner if it's still showing. The
        // initial /logs fetch may have returned empty (research just
        // started, no rows yet) and left the spinner in place; once
        // socket-driven entries start arriving we want them visible
        // instead of accumulating beneath a stuck spinner.
        const loadingSpinner = consoleLogContainer.querySelector('.ldr-loading-spinner');
        if (loadingSpinner) {
            loadingSpinner.remove();
        }

        // Ensure the log entry has an ID
        if (!logEntry.id) {
            const timestamp = logEntry.time || logEntry.timestamp || new Date().toISOString();
            const message = logEntry.message || logEntry.content || 'No message';
            logEntry.id = `${timestamp}-${hashString(message)}`;
        }

        // More robust deduplication: First check by ID if available
        if (logEntry.id) {
            const existingEntryById = consoleLogContainer.querySelector(`.ldr-console-log-entry[data-log-id="${logEntry.id}"]`);
            if (existingEntryById) {
                SafeLogger.log('Skipping duplicate log entry by ID:', logEntry.id);

                // Increment counter on existing entry
                let counter = parseInt(existingEntryById.dataset.counter || '1', 10);
                counter++;
                existingEntryById.dataset.counter = counter;

                // Update visual counter badge
                if (counter > 1) {
                    let counterBadge = existingEntryById.querySelector('.ldr-duplicate-counter');
                    if (!counterBadge) {
                        counterBadge = document.createElement('span');
                        counterBadge.className = 'ldr-duplicate-counter';
                        existingEntryById.appendChild(counterBadge);
                    }
                    counterBadge.textContent = `(${counter}×)`;
                }

                return;
            }
        }

        // Secondary check for duplicate by message content (for backward compatibility)
        const existingEntries = consoleLogContainer.querySelectorAll('.ldr-console-log-entry');
        if (existingEntries.length > 0) {
            const message = logEntry.message || logEntry.content || '';
            const logType = (logEntry.type || 'info').toLowerCase();

            // Check 10 most recent entries. DOM order is oldest -> newest so
            // column-reverse CSS can render the newest entry at the visual top.
            const start = Math.max(0, existingEntries.length - 10);
            for (let i = existingEntries.length - 1; i >= start; i--) {
                const entry = existingEntries[i];
                const entryMessage = entry.querySelector('.ldr-log-message')?.textContent;
                const entryType = entry.dataset.logType;

                // If message and type match, consider it a duplicate (unless it's a milestone)
                if (entryMessage === message &&
                    entryType === logType &&
                    logType !== 'milestone') {

                    SafeLogger.log('Skipping duplicate log entry by content:', message);

                    // Increment counter on existing entry
                    let counter = parseInt(entry.dataset.counter || '1', 10);
                    counter++;
                    entry.dataset.counter = counter;

                    // Update visual counter badge
                    if (counter > 1) {
                        let counterBadge = entry.querySelector('.ldr-duplicate-counter');
                        if (!counterBadge) {
                            counterBadge = document.createElement('span');
                            counterBadge.className = 'ldr-duplicate-counter';
                            entry.appendChild(counterBadge);
                        }
                        counterBadge.textContent = `(${counter}×)`;
                    }

                    return;
                }
            }
        }

        const element = createLogEntryElement(logEntry);

        if (element) {
            // Keep DOM order oldest -> newest. The container uses
            // flex-direction: column-reverse, so the newest entry renders at
            // the visual top while keyboard/DOM traversal stays chronological.
            const newTime = Number(element.dataset.logTimeMs || Date.now());
            const entries = consoleLogContainer.querySelectorAll('.ldr-console-log-entry');
            const nextNewerEntry = Array.from(entries).find(entry => {
                const entryTime = Number(entry.dataset.logTimeMs || 0);
                return entryTime > newTime;
            });
            consoleLogContainer.insertBefore(element, nextNewerEntry || null);
        }

        // Prune oldest entries if over limit to prevent unbounded DOM growth.
        // DOM order is oldest -> newest, so the oldest entries sit at the
        // head of the NodeList. Mirrors the batch-load prune above.
        const entries = consoleLogContainer.querySelectorAll('.ldr-console-log-entry');
        if (entries.length > MAX_LOG_ENTRIES) {
            const toRemove = entries.length - MAX_LOG_ENTRIES;
            for (let i = 0; i < toRemove; i++) {
                entries[i].remove();
            }
            updateLogCounter(-toRemove);
        }

        // Update log count using helper function if needed
        if (incrementCounter && element) {
            updateLogCounter(1);
        }

        // No need to scroll when loading all logs
        // Scroll will be handled after all logs are loaded
        if (incrementCounter && element && window._logPanelState.autoscroll) {
            // Auto-scroll to newest log (at the top)
            setTimeout(() => {
                consoleLogContainer.scrollTop = 0;
            }, 0);
        }
    }

    /**
     * Helper function to update the log counter
     * @param {number} increment - Amount to increment the counter by
     */
    function updateLogCounter(increment) {
        const logIndicators = document.querySelectorAll('.ldr-log-indicator');
        if (logIndicators.length > 0) {
            const currentCount = parseInt(logIndicators[0].textContent, 10) || 0;
            const newCount = currentCount + increment;

            // Update all indicators
            logIndicators.forEach(indicator => {
                indicator.textContent = newCount;
            });
        }
    }

    /**
     * Filter logs by type
     * @param {string} filterType - The type to filter by (all, info, milestone, error)
     */
    function filterLogsByType(filterType = 'all') {
        SafeLogger.log('Filtering logs by type:', filterType);

        filterType = filterType.toLowerCase();

        // Store current filter in shared state
        window._logPanelState.currentFilter = filterType;

        // Get all log entries from the DOM
        const logEntries = document.querySelectorAll('.ldr-console-log-entry');
        SafeLogger.log(`Found ${logEntries.length} log entries to filter`);

        let visibleCount = 0;

        // Apply filters
        logEntries.forEach(entry => {
            // Use data attribute for log type
            const logType = entry.dataset.logType || 'info';

            // Determine visibility based on filter type
            const shouldShow = checkLogVisibility(logType, filterType);

            // Set display style based on filter result
            entry.style.display = shouldShow ? '' : 'none';

            if (shouldShow) {
                visibleCount++;
            }
        });

        SafeLogger.log(`Filtering complete. Showing ${visibleCount} of ${logEntries.length} logs`);

        // Show 'no logs' message if all logs are filtered out
        const consoleContainer = document.getElementById('console-log-container');
        if (consoleContainer && logEntries.length > 0) {
            // Remove any existing empty message
            const existingEmptyMessage = consoleContainer.querySelector('.ldr-empty-log-message');
            if (existingEmptyMessage) {
                existingEmptyMessage.remove();
            }

            // Add empty message if needed
            if (visibleCount === 0) {
                SafeLogger.log(`Adding 'no logs' message for filter: ${filterType}`);
                const newEmptyMessage = document.createElement('div');
                newEmptyMessage.className = 'ldr-empty-log-message';
                newEmptyMessage.textContent = `No ${filterType} logs to display.`;
                consoleContainer.appendChild(newEmptyMessage);
            }
        }
    }

    /**
     * @brief Handler for the log download button which downloads all the
     * saved logs to the user's computer.
     */
    function downloadLogs() {
        const researchId = window._logPanelState.connectedResearchId;
        if (!researchId) {
            // No active research yet (e.g. on a freshly-loaded /chat/ page).
            // Without this guard, fetchLogsForResearch(null) would request
            // /api/research/null/logs and fail silently.
            SafeLogger.warn('downloadLogs called without researchId; skipping');
            return;
        }
        // Download path requests the shared hard cap (window.LDR_LOG_LIMITS,
        // Python's HISTORY_LOGS_HARD_CAP) so users get the full tail. The
        // route this fetch hits (/api/research/<id>/logs) clamps ?limit to
        // the same ceiling server-side, so the download is bounded to the
        // newest hard-cap rows.
        const hardCap = window.LDR_LOG_LIMITS?.hard_cap ?? 5000;
        fetchLogsForResearch(researchId, hardCap).then((logData) => {
            // Create a blob with the logs data
            const blob = new Blob([JSON.stringify(logData, null, 2)], { type: 'application/json' });

            // Create a link element and trigger download
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            if (typeof URLValidator !== 'undefined' && URLValidator.safeAssign) {
                URLValidator.safeAssign(a, 'href', url);
            } else {
                a.href = url;
            }
            a.download = `research_logs_${researchId}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });
    }

    // Expose public API
    window.logPanel = {
        initialize: initializeLogPanel,
        addLog: addConsoleLog,
        filterLogs: filterLogsByType,
        loadLogs: loadLogsForResearch
    };

    // Self-invoke to initialize when DOM content is loaded
    document.addEventListener('DOMContentLoaded', function() {
        SafeLogger.log('DOM ready - checking if log panel should be initialized');

        // Find research ID from URL if available (supports both integer and UUID)
        let researchId = null;
        const urlMatch = window.location.pathname.match(/\/(progress|results)\/([a-zA-Z0-9-]+)/);
        if (urlMatch && urlMatch[2]) {
            researchId = urlMatch[2];
            SafeLogger.log('Found research ID in URL:', researchId);

            // Store the current research ID in the state
            window._logPanelState.connectedResearchId = researchId;
        }

        // Check for research page elements
        const isResearchPage = window.location.pathname.includes('/progress/') ||
                              window.location.pathname.includes('/results/') ||
                              window.location.pathname.includes('/chat/') ||
                              document.getElementById('research-progress') ||
                              document.getElementById('research-results');

        // Initialize log panel if on a research page
        if (isResearchPage) {
            SafeLogger.log('On a research page, initializing log panel for research ID:', researchId);
            initializeLogPanel(researchId);

            // Extra check: If we have a research ID but panel not initialized properly
            setTimeout(() => {
                if (researchId && !window._logPanelState.initialized) {
                    SafeLogger.log('Log panel not initialized properly, retrying...');
                    initializeLogPanel(researchId);
                }
            }, 1000);
        } else {
            SafeLogger.log('Not on a research page, skipping log panel initialization');
        }
    });
})();
