/**
 * GraphTalker - Web UI JavaScript
 *
 * Handles SSE (Server-Sent Events) connection to stream iteration output
 * and renders markdown answers with syntax highlighting.
 */

// DOM elements
const questionInput = document.getElementById('question');
const sendBtn = document.getElementById('send-btn');
const stopBtn = document.getElementById('stop-btn');
const clearBtn = document.getElementById('clear-btn');
const continueCheckbox = document.getElementById('continue-checkbox');
const chatArea = document.getElementById('chat-area');
const iterationsDiv = document.getElementById('iterations');
const statusSpan = document.getElementById('status');
const contextFill = document.getElementById('context-fill');
const contextPct = document.getElementById('context-pct');

const WELCOME_INTRO = `<p>Ask GraphTalker about your AllegroGraph data. GraphTalker will iteratively query the database to find answers. Press <kbd>Ctrl</kbd>+<kbd>Enter</kbd> or <kbd>Command</kbd>+<kbd>Return</kbd> to send.</p>`;

// Current EventSource connection
let eventSource = null;

// Flag to track if query was stopped by user
let queryStopped = false;

// Flag to track if query completed normally (via 'done' event)
let queryCompleted = false;

// Current session tracking (for save/continue)
let currentSessionId = null;
let currentSessionTitle = null;

// Heartbeat to signal browser is still connected (enables auto-shutdown on tab close)
let heartbeatInterval = null;

function sendHeartbeat() {
    fetch('api/heartbeat', { method: 'POST' }).catch(() => {});
}

/**
 * Send a query to Claude via SSE streaming endpoint
 */
function sendQuery() {
    const question = questionInput.value.trim();
    if (!question) {
        return;
    }

    const continueConversation = continueCheckbox.checked;

    // Add user message to chat
    addUserMessage(question);

    // Clear input
    questionInput.value = '';

    // Clear iterations panel for new query
    iterationsDiv.innerHTML = '';

    // Disable send button, show stop button
    sendBtn.disabled = true;
    sendBtn.classList.add('hidden');
    stopBtn.classList.remove('hidden');
    queryStopped = false;
    queryCompleted = false;
    statusSpan.textContent = 'Connecting...';
    statusSpan.className = 'status running';

    // Close any existing connection
    if (eventSource) {
        eventSource.close();
    }

    // Build URL with query parameters
    const maxIterations = document.getElementById('max-iterations').value || '15';
    const params = new URLSearchParams({
        question: question,
        continue: continueConversation ? 'true' : 'false',
        maxIterations: maxIterations
    });

    // Create SSE connection
    eventSource = new EventSource(`stream?${params.toString()}`);

    eventSource.onopen = function() {
        statusSpan.textContent = 'Running...';
    };

    // Handle different event types
    eventSource.addEventListener('start', function(e) {
        const data = JSON.parse(e.data);
        appendIteration(`=== Query started ===\n`);
    });

    eventSource.addEventListener('iteration', function(e) {
        const data = JSON.parse(e.data);
        appendIteration(data.output);
        scrollIterationsToBottom();
    });

    eventSource.addEventListener('answer', function(e) {
        const data = JSON.parse(e.data);
        addAssistantMessage(data.text);
        appendIteration('\n=== Complete ===\n');
        scrollChatToBottom();
    });

    eventSource.addEventListener('context', function(e) {
        const data = JSON.parse(e.data);
        updateContextBar(data.percentage);
    });

    eventSource.addEventListener('error_msg', function(e) {
        const data = JSON.parse(e.data);
        addErrorMessage(data.message);
    });

    eventSource.addEventListener('done', function(e) {
        const data = JSON.parse(e.data);
        queryCompleted = true;
        resetQueryUI();

        // Check if connection changed (e.g. via connect_to_repository tool)
        const dropdown = document.getElementById('repo-dropdown');
        const currentRepo = dropdown.value;
        const serverRepo = data.repository;
        if (serverRepo && serverRepo !== currentRepo) {
            fetchRepositories();
        }

        // Focus back on input for next question
        questionInput.focus();
    });

    // Handle connection errors
    eventSource.onerror = function(e) {
        if (queryCompleted) {
            // Server closed the connection normally after 'done' — not an error
            if (eventSource) {
                eventSource.close();
                eventSource = null;
            }
            return;
        }

        console.error('SSE error:', e);
        resetQueryUI();
        if (!queryStopped) {
            statusSpan.textContent = 'Connection error';
        }

        if (eventSource) {
            eventSource.close();
            eventSource = null;
        }

        // Show error if we haven't received any data and wasn't stopped by user
        if (!queryStopped && iterationsDiv.textContent === '') {
            addErrorMessage('Failed to connect to server. Is the eval server running?');
        }
    };
}

/**
 * Reset UI after query completes or is stopped
 */
function resetQueryUI() {
    statusSpan.textContent = 'Ready';
    statusSpan.className = 'status';
    sendBtn.disabled = false;
    sendBtn.classList.remove('hidden');
    stopBtn.classList.add('hidden');
}

/**
 * Stop the current query
 */
function stopQuery() {
    queryStopped = true;

    if (eventSource) {
        eventSource.close();
        eventSource = null;
    }

    // Notify server to abort (fire and forget)
    fetch('api/abort', { method: 'POST' }).catch(() => {});

    appendIteration('\n=== Query stopped by user ===\n');
    addErrorMessage('Query stopped by user');

    resetQueryUI();
    questionInput.focus();
}

/**
 * Add a user message to the chat
 */
function addUserMessage(text) {
    // Remove welcome message if present
    const welcome = chatArea.querySelector('.welcome-message');
    if (welcome) {
        welcome.remove();
    }

    const messageDiv = document.createElement('div');
    messageDiv.className = 'chat-message user';
    messageDiv.innerHTML = `
        <div class="message-label">You</div>
        <div class="message-content">${escapeHtml(text)}</div>
    `;
    chatArea.appendChild(messageDiv);
    scrollChatToBottom();
}

/**
 * Sanitize response to prevent inline rendering of visualizations
 * This removes HTML tags that could cause charts/visualizations to render in the chat
 */
function sanitizeResponseForChat(text) {
    return text
        .replace(/<canvas[^>]*>[\s\S]*?<\/canvas>/gi, '[canvas removed]')
        .replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '[svg removed]')
        .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '[script removed]')
        .replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '[iframe removed]')
        .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '[style removed]')
        .replace(/<object[^>]*>[\s\S]*?<\/object>/gi, '[object removed]')
        .replace(/<embed[^>]*>/gi, '[embed removed]')
        .replace(/\s+on\w+\s*=/gi, ' data-removed=');
}

/**
 * Check if code content looks like visualization code
 */
function isVisualizationCode(content) {
    const patterns = [
        /new\s+Chart\s*\(/i,                              // Chart.js instantiation
        /cdn\.jsdelivr\.net\/npm\/chart\.js/i,            // Chart.js CDN
        /<canvas\s+id=/i,                                 // HTML canvas with id
        /"type"\s*:\s*"(bar|pie|line|scatter|doughnut|radar|polarArea)"/i,  // Chart types in JSON
        /"datasets"\s*:\s*\[/i,                           // datasets array (Chart.js structure)
        /<!DOCTYPE\s+html>[\s\S]*<script/i,               // Full HTML with script
        /backgroundColor[\s\S]*?rgba\s*\(/i,              // Chart.js color arrays
        /getContext\s*\(\s*['"]2d['"]\s*\)/i,             // Canvas 2d context
        /"labels"\s*:\s*\[/i,                             // labels array
    ];

    return patterns.some(pattern => pattern.test(content));
}

/**
 * Wrap visualization code blocks in collapsible details elements
 */
function wrapVisualizationCode(container) {
    // Find all code blocks
    const codeBlocks = container.querySelectorAll('pre code');

    codeBlocks.forEach(codeBlock => {
        const content = codeBlock.textContent;

        // Check if this looks like visualization code
        if (isVisualizationCode(content)) {
            const pre = codeBlock.parentElement;

            // Check if already wrapped
            if (pre.parentElement && pre.parentElement.classList.contains('viz-code-collapsed')) {
                return;
            }

            // Create details wrapper
            const details = document.createElement('details');
            details.className = 'viz-code-collapsed';

            const summary = document.createElement('summary');
            summary.textContent = '📊 Visualization Code (click to expand)';

            // Wrap the pre element
            pre.parentNode.insertBefore(details, pre);
            details.appendChild(summary);
            details.appendChild(pre);
        }
    });
}

/**
 * Add an assistant message to the chat with markdown rendering
 */
function addAssistantMessage(text) {
    const messageDiv = document.createElement('div');
    messageDiv.className = 'chat-message assistant';

    const contentDiv = document.createElement('div');
    contentDiv.className = 'message-content';

    // Sanitize to prevent inline visualization rendering
    const sanitizedText = sanitizeResponseForChat(text);

    // Pre-process text to wrap ASCII art in code blocks
    // Detect lines with box-drawing characters that aren't already in code blocks
    const processedText = preprocessAsciiArt(sanitizedText);

    // Parse markdown - use marked.parse with options
    try {
        contentDiv.innerHTML = marked.parse(processedText, {
            breaks: true,
            gfm: true
        });
    } catch (e) {
        console.error('Markdown parse error:', e);
        contentDiv.textContent = text;
    }

    messageDiv.innerHTML = '<div class="message-label">GraphTalker</div>';
    messageDiv.appendChild(contentDiv);

    // Wrap visualization code in collapsible widgets
    wrapVisualizationCode(contentDiv);

    // Apply syntax highlighting to any code blocks that weren't caught
    contentDiv.querySelectorAll('pre code').forEach((block) => {
        if (!block.classList.contains('hljs')) {
            hljs.highlightElement(block);
        }
    });

    chatArea.appendChild(messageDiv);
}

/**
 * Detect ASCII art and wrap it in code blocks for proper rendering
 */
function preprocessAsciiArt(text) {
    // Box-drawing characters pattern
    const boxChars = /[─│┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▀▄█▌▐░▒▓]/;

    const lines = text.split('\n');
    const result = [];
    let inCodeBlock = false;
    let inAsciiArt = false;
    let asciiArtBuffer = [];

    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];

        // Track if we're inside a markdown code block
        if (line.startsWith('```')) {
            if (inAsciiArt) {
                // End ASCII art block before code block starts
                result.push('```');
                result.push(asciiArtBuffer.join('\n'));
                result.push('```');
                asciiArtBuffer = [];
                inAsciiArt = false;
            }
            inCodeBlock = !inCodeBlock;
            result.push(line);
            continue;
        }

        // If inside a code block, pass through
        if (inCodeBlock) {
            result.push(line);
            continue;
        }

        // Check if line contains box-drawing characters
        const hasBoxChars = boxChars.test(line);

        if (hasBoxChars) {
            if (!inAsciiArt) {
                // Start ASCII art block
                inAsciiArt = true;
                asciiArtBuffer = [];
            }
            asciiArtBuffer.push(line);
        } else {
            if (inAsciiArt) {
                // End ASCII art block
                result.push('```');
                result.push(asciiArtBuffer.join('\n'));
                result.push('```');
                asciiArtBuffer = [];
                inAsciiArt = false;
            }
            result.push(line);
        }
    }

    // Handle trailing ASCII art
    if (inAsciiArt && asciiArtBuffer.length > 0) {
        result.push('```');
        result.push(asciiArtBuffer.join('\n'));
        result.push('```');
    }

    return result.join('\n');
}

/**
 * Add an error message to the chat
 */
function addErrorMessage(text) {
    const messageDiv = document.createElement('div');
    messageDiv.className = 'chat-message error';
    messageDiv.innerHTML = `
        <div class="message-label">Error</div>
        <div class="message-content">${escapeHtml(text)}</div>
    `;
    chatArea.appendChild(messageDiv);
    scrollChatToBottom();
}

/**
 * Escape HTML special characters
 */
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

/**
 * Format summary text with basic markdown support
 */
function formatSummaryMarkdown(text) {
    if (!text) return '';

    // Process line by line to handle tables and other markdown
    const lines = text.split('\n');
    const result = [];
    let inTable = false;
    let tableRows = [];

    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const trimmedLine = line.trim();

        // Check if this looks like a table row
        const isTableRow = trimmedLine.startsWith('|') && trimmedLine.endsWith('|');
        // Check if this is a separator row (|---|---|)
        const isSeparator = /^\|[-:\s|]+\|$/.test(trimmedLine);

        if (isTableRow || isSeparator) {
            if (!inTable) {
                inTable = true;
                tableRows = [];
            }
            if (!isSeparator) {
                tableRows.push(trimmedLine);
            }
        } else {
            if (inTable) {
                // End of table, convert to HTML
                result.push(convertTableToHtml(tableRows));
                inTable = false;
                tableRows = [];
            }
            // Process non-table line
            result.push(formatMarkdownLine(line));
        }
    }

    // Handle table at end of text
    if (inTable && tableRows.length > 0) {
        result.push(convertTableToHtml(tableRows));
    }

    return result.join('');
}

/**
 * Format a single line of markdown (non-table)
 */
function formatMarkdownLine(line) {
    // Headers: ### text -> <h4>, ## text -> <h3>
    if (/^### (.*)$/.test(line)) {
        return line.replace(/^### (.*)$/, '<h4>$1</h4>');
    }
    if (/^## (.*)$/.test(line)) {
        return line.replace(/^## (.*)$/, '<h3>$1</h3>');
    }

    // Apply inline formatting
    let formatted = line
        // Bold: **text** or __text__
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        .replace(/__(.*?)__/g, '<strong>$1</strong>')
        // Italic: *text* or _text_
        .replace(/\*([^*]+)\*/g, '<em>$1</em>')
        .replace(/_([^_]+)_/g, '<em>$1</em>');

    // Add line break after non-empty lines
    return formatted + (formatted.trim() ? '<br>' : '');
}

/**
 * Convert table rows to HTML table
 */
function convertTableToHtml(rows) {
    if (rows.length === 0) return '';

    let html = '<table class="viz-table">';

    rows.forEach((row, index) => {
        // Split by | and remove empty first/last elements
        const cells = row.split('|').slice(1, -1).map(c => c.trim());

        if (index === 0) {
            // Header row
            html += '<thead><tr>';
            cells.forEach(cell => {
                html += `<th>${escapeHtml(cell)}</th>`;
            });
            html += '</tr></thead><tbody>';
        } else {
            // Data row
            html += '<tr>';
            cells.forEach(cell => {
                html += `<td>${escapeHtml(cell)}</td>`;
            });
            html += '</tr>';
        }
    });

    html += '</tbody></table>';
    return html;
}

/**
 * Copy text to clipboard and show feedback
 */
function copyToClipboard(text, buttonElement) {
    navigator.clipboard.writeText(text).then(() => {
        // Show success feedback
        const originalTitle = buttonElement.title;
        buttonElement.classList.add('copied');
        buttonElement.title = 'Copied!';

        // Reset after 2 seconds
        setTimeout(() => {
            buttonElement.classList.remove('copied');
            buttonElement.title = originalTitle;
        }, 2000);
    }).catch(err => {
        console.error('Failed to copy:', err);
        // Fallback for older browsers
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        textArea.style.opacity = '0';
        document.body.appendChild(textArea);
        textArea.select();
        try {
            document.execCommand('copy');
            buttonElement.classList.add('copied');
            buttonElement.title = 'Copied!';
            setTimeout(() => {
                buttonElement.classList.remove('copied');
                buttonElement.title = 'Copy URI';
            }, 2000);
        } catch (e) {
            console.error('Fallback copy failed:', e);
        }
        document.body.removeChild(textArea);
    });
}

/**
 * Delete a query from the query library
 */
function deleteQuery(uri, title) {
    if (!confirm(`Delete query "${title}"?\n\nThis will also delete any associated visualizations.`)) {
        return;
    }

    fetch(`api/queries/${encodeURIComponent(uri)}`, {
        method: 'DELETE'
    })
        .then(response => response.json())
        .then(data => {
            if (data.error) {
                alert(`Failed to delete query: ${data.error}`);
                return;
            }
            // Refresh the query list
            fetchQueries();
            // Clear the details panel
            document.getElementById('query-details').innerHTML =
                '<div class="placeholder">Query deleted. Select another query to view details.</div>';
        })
        .catch(err => {
            alert(`Failed to delete query: ${err.message}`);
        });
}

/**
 * Append text to the iterations panel
 */
function appendIteration(text) {
    // Remove placeholder if present
    const placeholder = iterationsDiv.querySelector('.placeholder');
    if (placeholder) {
        placeholder.remove();
    }

    // Create a text node and append (preserves formatting)
    const span = document.createElement('span');
    span.textContent = text;
    iterationsDiv.appendChild(span);
}

/**
 * Scroll chat area to bottom
 */
function scrollChatToBottom() {
    chatArea.scrollTop = chatArea.scrollHeight;
}

/**
 * Scroll iterations panel to bottom
 */
function scrollIterationsToBottom() {
    iterationsDiv.scrollTop = iterationsDiv.scrollHeight;
}

/**
 * Update the context usage bar
 */
function updateContextBar(percentage) {
    contextFill.style.width = `${Math.min(percentage, 100)}%`;
    contextPct.textContent = `${percentage.toFixed(1)}%`;

    // Update color based on usage
    contextFill.classList.remove('warning', 'danger');
    if (percentage >= 90) {
        contextFill.classList.add('danger');
    } else if (percentage >= 70) {
        contextFill.classList.add('warning');
    }
}

function welcomeMessageHTML(bodyHTML) {
    return `<div class="welcome-message">
        ${bodyHTML}
    </div>`;
}

/**
 * Clear the conversation
 */
function clearConversation() {
    if (!confirm('Clear the conversation history?')) {
        return;
    }

    // Call the clear endpoint
    fetch('api/clear', { method: 'POST' })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                // Reset chat area
                chatArea.innerHTML = welcomeMessageHTML(`
                        ${WELCOME_INTRO}
                        <p class="hint">Try: "What classes are in the schema?" or "show me all products".</p>`);

                // Reset iterations
                iterationsDiv.innerHTML = '<div class="placeholder">Iteration output will appear here as GraphTalker works through your query...</div>';

                // Reset context bar
                updateContextBar(0);

                // Reset session tracking
                currentSessionId = null;
                currentSessionTitle = null;

                // Clear and focus input
                questionInput.value = '';
                questionInput.focus();
            } else {
                addErrorMessage(data.error || 'Failed to clear conversation');
            }
        })
        .catch(err => {
            addErrorMessage('Failed to clear conversation: ' + err.message);
        });
}

/**
 * Fetch and display the application version
 */
function fetchVersion() {
    fetch('api/version')
        .then(response => response.json())
        .then(data => {
            if (data.version) {
                const el = document.getElementById('app-version');
                if (el) el.textContent = data.version;
            }
        })
        .catch(err => {
            console.error('Failed to fetch version:', err);
        });
}

/**
 * Fetch initial context stats
 */
function fetchContextStats() {
    fetch('api/context')
        .then(response => response.json())
        .then(data => {
            if (data.percentage !== undefined) {
                updateContextBar(data.percentage);
            }
            if (data.maxIterations !== undefined) {
                document.getElementById('max-iterations').value = data.maxIterations;
            }
        })
        .catch(err => {
            console.error('Failed to fetch context stats:', err);
        });
}

/**
 * Fetch and populate the repository dropdown
 */
function fetchRepositories() {
    const dropdown = document.getElementById('repo-dropdown');
    dropdown.disabled = true;

    fetch('api/repositories')
        .then(response => response.json())
        .then(data => {
            if (data.error) {
                console.error('Failed to fetch repositories:', data.error);
                dropdown.innerHTML = '<option value="">Error loading repos</option>';
                return;
            }

            // Clear and populate dropdown
            dropdown.innerHTML = '';

            const repos = data.repositories || [];
            const currentRepo = data.currentRepository;

            // Build list of repo names first to check if currentRepo is valid
            const repoNames = repos.map(repo =>
                typeof repo === 'string' ? repo : (repo.id || repo.title || repo.name || String(repo))
            );

            // Check if currentRepo is missing or doesn't exist in the list
            const currentRepoValid = currentRepo && repoNames.includes(currentRepo);

            // If no valid repository is currently selected, add a placeholder
            if (!currentRepoValid) {
                const placeholder = document.createElement('option');
                placeholder.value = '';
                placeholder.textContent = '-- Select a repository --';
                placeholder.disabled = true;
                placeholder.selected = true;
                dropdown.appendChild(placeholder);
            }

            repos.forEach(repo => {
                const option = document.createElement('option');
                // Handle both string repos and object repos (with id/title properties)
                const repoName = typeof repo === 'string' ? repo : (repo.id || repo.title || repo.name || String(repo));
                option.value = repoName;
                option.textContent = repoName;
                if (repoName === currentRepo) {
                    option.selected = true;
                }
                dropdown.appendChild(option);
            });

            if (repos.length === 0) {
                dropdown.innerHTML = '<option value="">No repositories found</option>';
            }

            dropdown.disabled = false;
            dropdown.dispatchEvent(new Event('change'));
        })
        .catch(err => {
            console.error('Failed to fetch repositories:', err);
            dropdown.innerHTML = '<option value="">Error loading repos</option>';
        });
}

/**
 * Handle repository selection change
 */
function onRepositoryChange() {
    const dropdown = document.getElementById('repo-dropdown');
    const selectedRepo = dropdown.value;

    if (!selectedRepo) {
        return;
    }

    dropdown.disabled = true;
    statusSpan.textContent = 'Switching repository...';
    statusSpan.className = 'status running';

    fetch('api/repository', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ repository: selectedRepo })
    })
        .then(response => response.json())
        .then(data => {
            dropdown.disabled = false;

            if (data.success) {
                statusSpan.textContent = `Switched to ${selectedRepo}`;
                statusSpan.className = 'status';

                // Clear the chat UI since conversation was cleared on server
                if (data.conversationCleared) {
                    chatArea.innerHTML = welcomeMessageHTML(`
                            ${WELCOME_INTRO}
                            <p class="hint">Current repository: <strong>${selectedRepo}</strong>.</p>`);
                    updateContextBar(0);
                }

                // Show notification about SHACL caching
                iterationsDiv.innerHTML = '';
                appendIteration(`=== Repository switched to: ${selectedRepo} ===\n`);
                if (data.shaclCached) {
                    appendIteration(`SHACL schema cached (will be loaded on first query).\n`);
                }
                appendIteration(`Conversation cleared.\n`);

                // Clear after 2 seconds
                setTimeout(() => {
                    if (statusSpan.textContent === `Switched to ${selectedRepo}`) {
                        statusSpan.textContent = 'Ready';
                    }
                }, 2000);
            } else {
                statusSpan.textContent = 'Ready';
                statusSpan.className = 'status';
                addErrorMessage(`Failed to switch repository: ${data.error}`);
            }
        })
        .catch(err => {
            dropdown.disabled = false;
            statusSpan.textContent = 'Ready';
            statusSpan.className = 'status';
            addErrorMessage(`Failed to switch repository: ${err.message}`);
        });
}

// Keyboard shortcut: Ctrl+Enter or Cmd+Enter to send
questionInput.addEventListener('keydown', function(e) {
    if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
        sendQuery();
    }
});

// Auto-resize textarea
questionInput.addEventListener('input', function() {
    this.style.height = 'auto';
    this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});

/**
 * Switch between tabs
 */
function switchTab(tabName) {
    // Update tab buttons
    document.querySelectorAll('.tab-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.tab === tabName);
    });

    // Update tab content
    document.querySelectorAll('.tab-content').forEach(content => {
        content.classList.toggle('active', content.id === `tab-${tabName}`);
    });

    // Focus appropriate element based on tab
    if (tabName === 'chat') {
        questionInput.focus();
    } else if (tabName === 'query-library') {
        // Load queries when switching to query library tab
        fetchQueries();
    } else if (tabName === 'history') {
        // Load sessions when switching to history tab
        fetchSessions();
    }
}

/**
 * Fetch queries from the query library
 */
function fetchQueries() {
    const queryList = document.getElementById('query-list');
    queryList.innerHTML = '<div class="placeholder">Loading queries...</div>';

    fetch('api/queries')
        .then(response => response.json())
        .then(data => {
            if (data.error) {
                queryList.innerHTML = `<div class="placeholder">Error: ${data.error}</div>`;
                return;
            }

            const queries = data.queries || [];
            if (queries.length === 0) {
                queryList.innerHTML = '<div class="placeholder">No queries found in the query library.</div>';
                return;
            }

            queryList.innerHTML = '';
            queries.forEach(query => {
                const card = document.createElement('div');
                card.className = 'query-card';
                card.dataset.uri = query.uri;
                card.innerHTML = `
                    <div class="query-card-title">${escapeHtml(query.title || 'Untitled Query')}</div>
                    <div class="query-card-meta">
                        ${query.repository ? `Repository: ${escapeHtml(query.repository)}` : ''}
                        ${query.created ? ` | ${new Date(query.created).toLocaleDateString()}` : ''}
                    </div>
                `;
                card.onclick = () => selectQuery(query.uri);
                queryList.appendChild(card);
            });
        })
        .catch(err => {
            queryList.innerHTML = `<div class="placeholder">Failed to load queries: ${err.message}</div>`;
        });
}

/**
 * Select a query and show its details
 */
function selectQuery(uri) {
    // Update selected state
    document.querySelectorAll('.query-card').forEach(card => {
        card.classList.toggle('selected', card.dataset.uri === uri);
    });

    const detailsDiv = document.getElementById('query-details');
    detailsDiv.innerHTML = '<div class="placeholder">Loading query details...</div>';

    // Fetch query details and visualizations in parallel
    Promise.all([
        fetch(`api/queries/${encodeURIComponent(uri)}`).then(r => r.json()),
        fetch(`api/queries/${encodeURIComponent(uri)}/visualizations`).then(r => r.json())
    ])
        .then(([queryData, vizData]) => {
            if (queryData.error) {
                detailsDiv.innerHTML = `<div class="placeholder">Error: ${queryData.error}</div>`;
                return;
            }

            const visualizations = vizData.visualizations || [];

            detailsDiv.innerHTML = `
                <div class="query-detail-section">
                    <h3>Title</h3>
                    <p>${escapeHtml(queryData.title || 'Untitled')}</p>
                </div>
                <div class="query-detail-section">
                    <h3>URI</h3>
                    <div class="uri-row">
                        <code class="uri-text">${escapeHtml(uri)}</code>
                        <button class="copy-btn" onclick="copyToClipboard('${escapeHtml(uri).replace(/'/g, "\\'")}', this)" title="Copy URI">
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                                <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                            </svg>
                        </button>
                        <button class="delete-btn" onclick="deleteQuery('${escapeHtml(uri).replace(/'/g, "\\'")}', '${escapeHtml(queryData.title || '').replace(/'/g, "\\'")}')" title="Delete Query">
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <polyline points="3 6 5 6 21 6"></polyline>
                                <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
                                <line x1="10" y1="11" x2="10" y2="17"></line>
                                <line x1="14" y1="11" x2="14" y2="17"></line>
                            </svg>
                        </button>
                    </div>
                </div>
                <div class="query-detail-section">
                    <h3>SPARQL Query</h3>
                    <pre><code class="language-sparql">${escapeHtml(queryData.sparql || 'No query available')}</code></pre>
                </div>
                ${queryData.repository ? `
                <div class="query-detail-section">
                    <h3>Target Repository</h3>
                    <p>${escapeHtml(queryData.repository)}</p>
                </div>
                ` : ''}
                ${visualizations.length > 0 ? `
                <div class="query-detail-section">
                    <h3>Visualizations (${visualizations.length})</h3>
                    <div class="viz-grid" id="viz-grid">
                        ${visualizations.map((viz, index) => `
                            <div class="viz-card" data-viz-index="${index}">
                                <div class="viz-card-type">${escapeHtml(viz.type || 'chart')}</div>
                                <div class="viz-card-description">${escapeHtml(viz.description || '')}</div>
                                ${viz.summary ? `<div class="viz-card-summary">${escapeHtml(viz.summary)}</div>` : ''}
                            </div>
                        `).join('')}
                    </div>
                </div>
                ` : ''}
                ${queryData.description ? `
                <div class="query-detail-section query-description-section">
                    <h3>Description</h3>
                    <div class="query-description-content markdown-content">${marked.parse(queryData.description)}</div>
                </div>
                ` : ''}
            `;

            // Apply syntax highlighting
            detailsDiv.querySelectorAll('pre code').forEach(block => {
                hljs.highlightElement(block);
            });

            // Attach click handlers for visualization cards
            if (visualizations.length > 0) {
                document.querySelectorAll('.viz-card').forEach((card, index) => {
                    card.addEventListener('click', () => {
                        const viz = visualizations[index];
                        showVisualization(viz.uri, viz.type, viz.config, viz.description, viz.summary);
                    });
                });
            }
        })
        .catch(err => {
            detailsDiv.innerHTML = `<div class="placeholder">Failed to load query: ${err.message}</div>`;
        });
}

// Store current chart instance for cleanup
let currentChart = null;

/**
 * Sanitize Chart.js config by removing JavaScript function definitions
 * Functions aren't valid JSON and cause parse errors
 */
function sanitizeChartConfig(configStr) {
    // Remove function(...) {...} patterns
    // This regex matches: "key": function(...) { ... }
    // and replaces with "key": null
    let sanitized = configStr;

    // Pattern to match function definitions as property values
    // Handles: "label": function(context) { ... }
    // The function body can contain nested braces, so we need to match them properly
    const functionPattern = /:\s*function\s*\([^)]*\)\s*\{/g;

    let match;
    while ((match = functionPattern.exec(sanitized)) !== null) {
        // Find the matching closing brace
        const startIdx = match.index + match[0].length;
        let braceCount = 1;
        let endIdx = startIdx;

        while (braceCount > 0 && endIdx < sanitized.length) {
            if (sanitized[endIdx] === '{') braceCount++;
            else if (sanitized[endIdx] === '}') braceCount--;
            endIdx++;
        }

        // Replace the entire function with null
        const beforeColon = sanitized.lastIndexOf(':', match.index);
        sanitized = sanitized.substring(0, match.index) + ': null' + sanitized.substring(endIdx);

        // Reset regex lastIndex since string changed
        functionPattern.lastIndex = 0;
    }

    return sanitized;
}

/**
 * Color palette for network graph node types
 */
const networkGraphColors = {
    default: '#6366f1',  // indigo
    palette: [
        '#6366f1', // indigo
        '#ec4899', // pink
        '#14b8a6', // teal
        '#f59e0b', // amber
        '#8b5cf6', // violet
        '#10b981', // emerald
        '#f97316', // orange
        '#06b6d4', // cyan
        '#ef4444', // red
        '#84cc16', // lime
    ]
};

/**
 * Get color for a node type
 */
function getNodeColor(type, typeIndex) {
    if (!type) return networkGraphColors.default;
    return networkGraphColors.palette[typeIndex % networkGraphColors.palette.length];
}

/**
 * Render a network graph using D3.js force-directed layout
 */
function renderNetworkGraph(containerId, nodes, edges, options = {}) {
    const container = document.getElementById(containerId);
    if (!container) return;

    // Clear any existing content
    container.innerHTML = '';

    // Get container dimensions
    const width = container.clientWidth || 800;
    const height = options.height || 500;

    // Create SVG
    const svg = d3.select(container)
        .append('svg')
        .attr('width', width)
        .attr('height', height)
        .attr('viewBox', [0, 0, width, height]);

    // Add zoom behavior
    const g = svg.append('g');
    const zoom = d3.zoom()
        .scaleExtent([0.1, 4])
        .on('zoom', (event) => {
            g.attr('transform', event.transform);
        });
    svg.call(zoom);

    // Build type-to-index map for coloring
    const typeSet = new Set(nodes.map(n => n.type || 'default'));
    const typeArray = Array.from(typeSet);
    const typeToIndex = {};
    typeArray.forEach((t, i) => typeToIndex[t] = i);

    // Create node id map for edge resolution
    const nodeById = new Map(nodes.map(n => [n.id, n]));

    // Process edges - ensure source/target are ids
    const processedEdges = edges.map(e => ({
        source: typeof e.source === 'object' ? e.source.id : e.source,
        target: typeof e.target === 'object' ? e.target.id : e.target,
        predicate: e.predicate || e.label || '',
        weight: e.weight || 1
    }));

    // Create force simulation
    const simulation = d3.forceSimulation(nodes)
        .force('link', d3.forceLink(processedEdges)
            .id(d => d.id)
            .distance(options.linkDistance || 100))
        .force('charge', d3.forceManyBody()
            .strength(options.chargeStrength || -300))
        .force('center', d3.forceCenter(width / 2, height / 2))
        .force('collision', d3.forceCollide().radius(30));

    // Draw edges (lines)
    const link = g.append('g')
        .attr('class', 'links')
        .selectAll('line')
        .data(processedEdges)
        .join('line')
        .attr('stroke', '#999')
        .attr('stroke-opacity', 0.6)
        .attr('stroke-width', d => Math.sqrt(d.weight || 1) * 1.5);

    // Draw edge labels if enabled
    const showEdgeLabels = options.showEdgeLabels !== false;
    let edgeLabels = null;
    if (showEdgeLabels) {
        edgeLabels = g.append('g')
            .attr('class', 'edge-labels')
            .selectAll('text')
            .data(processedEdges)
            .join('text')
            .attr('font-size', 13)
            .attr('fill', '#555')
            .attr('text-anchor', 'middle')
            .attr('font-weight', '500')
            .text(d => d.predicate);
    }

    // Draw nodes (circles)
    const node = g.append('g')
        .attr('class', 'nodes')
        .selectAll('circle')
        .data(nodes)
        .join('circle')
        .attr('r', d => d.size || 15)
        .attr('fill', d => getNodeColor(d.type, typeToIndex[d.type || 'default']))
        .attr('stroke', '#fff')
        .attr('stroke-width', 2)
        .style('cursor', 'pointer')
        .call(drag(simulation));

    // Add node labels
    const labels = g.append('g')
        .attr('class', 'labels')
        .selectAll('text')
        .data(nodes)
        .join('text')
        .attr('font-size', 14)
        .attr('font-weight', '500')
        .attr('dx', 20)
        .attr('dy', 5)
        .text(d => d.label || d.id);

    // Tooltip on hover
    node.append('title')
        .text(d => {
            let text = d.label || d.id;
            if (d.type) text += ` (${d.type})`;
            if (d.properties) {
                Object.entries(d.properties).forEach(([k, v]) => {
                    text += `\n${k}: ${v}`;
                });
            }
            return text;
        });

    // Tick function - update positions
    simulation.on('tick', () => {
        link
            .attr('x1', d => d.source.x)
            .attr('y1', d => d.source.y)
            .attr('x2', d => d.target.x)
            .attr('y2', d => d.target.y);

        if (edgeLabels) {
            edgeLabels
                .attr('x', d => (d.source.x + d.target.x) / 2)
                .attr('y', d => (d.source.y + d.target.y) / 2);
        }

        node
            .attr('cx', d => d.x)
            .attr('cy', d => d.y);

        labels
            .attr('x', d => d.x)
            .attr('y', d => d.y);
    });

    // Add legend if we have multiple types
    if (typeArray.length > 1 && typeArray[0] !== 'default') {
        const legend = svg.append('g')
            .attr('class', 'legend')
            .attr('transform', `translate(10, 10)`);

        typeArray.forEach((type, i) => {
            const legendRow = legend.append('g')
                .attr('transform', `translate(0, ${i * 22})`);

            legendRow.append('circle')
                .attr('r', 7)
                .attr('fill', getNodeColor(type, i));

            legendRow.append('text')
                .attr('x', 14)
                .attr('y', 5)
                .attr('font-size', 13)
                .attr('font-weight', '500')
                .text(type);
        });
    }

    // Drag behavior
    function drag(simulation) {
        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }

        return d3.drag()
            .on('start', dragstarted)
            .on('drag', dragged)
            .on('end', dragended);
    }
}

/**
 * Show a visualization in a modal
 */
function showVisualization(uri, type, config, description, summary) {
    // Create modal if it doesn't exist
    let modal = document.getElementById('viz-modal');
    if (!modal) {
        modal = document.createElement('div');
        modal.id = 'viz-modal';
        modal.className = 'modal-overlay';
        modal.innerHTML = `
            <div class="modal-content viz-modal-content">
                <div class="modal-header">
                    <h3 id="viz-modal-title">Visualization</h3>
                    <button class="modal-close" onclick="closeVisualizationModal()">&times;</button>
                </div>
                <div class="modal-body" id="viz-modal-body">
                </div>
            </div>
        `;
        // Close on background click
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeVisualizationModal();
            }
        });
        document.body.appendChild(modal);
    }

    // Update modal title with description
    const modalTitle = document.getElementById('viz-modal-title');
    modalTitle.textContent = description || 'Visualization';

    // Destroy previous chart(s) if exists
    if (currentChart) {
        if (Array.isArray(currentChart)) {
            currentChart.forEach(c => c.destroy());
        } else {
            currentChart.destroy();
        }
        currentChart = null;
    }

    const modalBody = document.getElementById('viz-modal-body');

    // Build summary section HTML if summary exists
    const summaryHtml = summary ? `
        <div class="viz-summary-section">
            <div class="viz-summary-content">${formatSummaryMarkdown(summary)}</div>
        </div>
    ` : '';

    try {
        // Sanitize config: remove JavaScript function definitions which aren't valid JSON
        const sanitizedConfig = sanitizeChartConfig(config);
        const configObj = JSON.parse(sanitizedConfig);

        // Check if this is a multi-chart dashboard
        const isMultiChart = Array.isArray(configObj.charts) && configObj.charts.length > 0;
        // Check if this is a single Chart.js config (has type and data inside config)
        const isChartJs = configObj.type && configObj.data;
        // Check if this is a table visualization - check both the type parameter AND configObj.type
        const isTable = (type?.toLowerCase() === 'table' || configObj.type?.toLowerCase() === 'table') &&
                        (configObj.columns || configObj.rows || configObj.data);
        // Check if this is a network graph visualization
        const isNetworkGraph = (type?.toLowerCase() === 'network_graph' || configObj.type?.toLowerCase() === 'network_graph') &&
                               (configObj.nodes || configObj.data?.nodes);

        // Check if this is a map visualization
        const isMap = (type?.toLowerCase() === 'map' || type?.toLowerCase() === 'leaflet_map' ||
                       configObj.type?.toLowerCase() === 'map') &&
                      (configObj.data || configObj.geojson || configObj.features);

        if (isMap) {
            // Leaflet map visualization
            const mapId = 'viz-leaflet-map';

            modalBody.innerHTML = `
                <div class="viz-render-container map-container">
                    <div id="${mapId}" class="leaflet-map"></div>
                </div>
                ${summaryHtml}
            `;

            // Render map after modal is visible
            setTimeout(() => {
                const mapOptions = configObj.options || {};
                const center = configObj.center || [0, 0];
                const zoom = configObj.zoom || 2;

                const map = L.map(mapId).setView(center, zoom);

                // Add tile layer (OpenStreetMap)
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                    attribution: '&copy; OpenStreetMap contributors',
                    maxZoom: 19
                }).addTo(map);

                // Get GeoJSON data from config
                const geojsonData = configObj.data || configObj.geojson || {
                    type: 'FeatureCollection',
                    features: configObj.features || []
                };

                // Default style function
                const defaultStyle = {
                    color: mapOptions.color || '#3388ff',
                    weight: mapOptions.weight || 2,
                    opacity: mapOptions.opacity || 1,
                    fillColor: mapOptions.fillColor || '#3388ff',
                    fillOpacity: mapOptions.fillOpacity || 0.3
                };

                // Add GeoJSON layer
                const geojsonLayer = L.geoJSON(geojsonData, {
                    style: function(feature) {
                        const props = feature.properties || {};
                        return {
                            color: props.color || props.stroke || defaultStyle.color,
                            weight: props.weight || props['stroke-width'] || defaultStyle.weight,
                            opacity: props.opacity || props['stroke-opacity'] || defaultStyle.opacity,
                            fillColor: props.fillColor || props.fill || props.color || defaultStyle.fillColor,
                            fillOpacity: props.fillOpacity || props['fill-opacity'] || defaultStyle.fillOpacity
                        };
                    },
                    pointToLayer: function(feature, latlng) {
                        const props = feature.properties || {};
                        return L.circleMarker(latlng, {
                            radius: props.radius || mapOptions.markerRadius || 8,
                            color: props.color || defaultStyle.color,
                            fillColor: props.fillColor || props.color || defaultStyle.fillColor,
                            fillOpacity: props.fillOpacity || defaultStyle.fillOpacity,
                            weight: props.weight || defaultStyle.weight
                        });
                    },
                    onEachFeature: function(feature, layer) {
                        const props = feature.properties || {};
                        // Build popup from properties
                        const popupParts = [];
                        if (props.name || props.label || props.title) {
                            popupParts.push(`<strong>${escapeHtml(props.name || props.label || props.title)}</strong>`);
                        }
                        // Add other properties (skip styling ones)
                        const skipKeys = new Set(['color', 'fillColor', 'fillOpacity', 'weight', 'opacity',
                                                   'radius', 'stroke', 'stroke-width', 'stroke-opacity',
                                                   'fill', 'fill-opacity', 'marker-size']);
                        for (const [key, value] of Object.entries(props)) {
                            if (!skipKeys.has(key) && key !== 'name' && key !== 'label' && key !== 'title') {
                                popupParts.push(`${escapeHtml(key)}: ${escapeHtml(String(value))}`);
                            }
                        }
                        if (popupParts.length > 0) {
                            layer.bindPopup(popupParts.join('<br>'));
                        }
                        // Tooltip with name
                        if (props.name || props.label || props.title) {
                            layer.bindTooltip(props.name || props.label || props.title);
                        }
                    }
                }).addTo(map);

                // Auto-fit bounds to data if we have features
                const featureCount = geojsonData.features ? geojsonData.features.length :
                                     (geojsonData.type === 'FeatureCollection' ? 0 : 1);
                if (featureCount > 0) {
                    try {
                        map.fitBounds(geojsonLayer.getBounds(), { padding: [20, 20] });
                    } catch (e) {
                        // fitBounds can fail on empty/invalid geometries
                    }
                }

                // Store map instance for cleanup
                currentChart = { destroy: () => map.remove() };
            }, 100);
        } else if (isNetworkGraph) {
            // Network graph visualization using D3.js
            const graphData = configObj.data || configObj;
            const nodes = graphData.nodes || [];
            const edges = graphData.edges || graphData.links || [];
            const options = configObj.options || {};

            modalBody.innerHTML = `
                <div class="viz-render-container network-graph-container">
                    <div id="network-graph-svg" class="network-graph-svg"></div>
                </div>
                ${summaryHtml}
            `;

            // Render the network graph after modal is shown
            setTimeout(() => {
                renderNetworkGraph('network-graph-svg', nodes, edges, options);
            }, 100);
        } else if (isMultiChart) {
            // Multi-chart dashboard
            const charts = configObj.charts;
            const canvasIds = charts.map((_, i) => `viz-canvas-${i}`);

            modalBody.innerHTML = `
                <div class="viz-render-container multi-chart">
                    ${charts.map((chart, i) => `
                        <div class="chart-wrapper">
                            ${chart.title ? `<h4 class="chart-title">${escapeHtml(chart.title)}</h4>` : ''}
                            <canvas id="${canvasIds[i]}"></canvas>
                        </div>
                    `).join('')}
                </div>
                ${summaryHtml}
            `;

            // Store chart instances for cleanup
            currentChart = [];

            // Render each chart
            charts.forEach((chart, i) => {
                const canvas = document.getElementById(canvasIds[i]);
                const ctx = canvas.getContext('2d');

                // Clone config
                const chartConfig = JSON.parse(JSON.stringify(chart));

                // Remove callback strings
                if (chartConfig.options?.plugins?.tooltip?.callbacks) {
                    delete chartConfig.options.plugins.tooltip.callbacks;
                }

                // Ensure responsive
                if (!chartConfig.options) chartConfig.options = {};
                chartConfig.options.responsive = true;
                chartConfig.options.maintainAspectRatio = false;

                const instance = new Chart(ctx, chartConfig);
                currentChart.push(instance);
            });
        } else if (isTable) {
            // Table visualization (check before isChartJs since tables may have 'data' field)
            const columns = configObj.columns || [];
            const rows = configObj.rows || configObj.data || [];

            // Build table HTML
            let tableHtml = '<table class="viz-table"><thead><tr>';

            // Handle columns - could be array of strings or array of objects with label/key/field
            const columnHeaders = columns.map(col =>
                typeof col === 'string' ? col : (col.header || col.label || col.name || col.key || '')
            );
            const columnKeys = columns.map(col =>
                typeof col === 'string' ? col : (col.field || col.key || col.name || col.label || col.header || '')
            );

            // Render header
            columnHeaders.forEach(header => {
                tableHtml += `<th>${escapeHtml(String(header))}</th>`;
            });
            tableHtml += '</tr></thead><tbody>';

            // Render rows
            rows.forEach(row => {
                tableHtml += '<tr>';
                if (Array.isArray(row)) {
                    // Row is an array - use positional indexing
                    row.forEach(cell => {
                        tableHtml += `<td>${escapeHtml(String(cell ?? ''))}</td>`;
                    });
                } else if (typeof row === 'object' && row !== null) {
                    // Row is an object - use column keys
                    columnKeys.forEach(key => {
                        const value = row[key] ?? '';
                        tableHtml += `<td>${escapeHtml(String(value))}</td>`;
                    });
                }
                tableHtml += '</tr>';
            });
            tableHtml += '</tbody></table>';

            modalBody.innerHTML = `
                <div class="viz-render-container table-container">
                    ${tableHtml}
                </div>
                ${summaryHtml}
            `;
        } else if (isChartJs) {
            // Single Chart.js chart
            modalBody.innerHTML = `
                <div class="viz-render-container">
                    <canvas id="viz-canvas"></canvas>
                </div>
                ${summaryHtml}
            `;

            // Render the chart
            const canvas = document.getElementById('viz-canvas');
            const ctx = canvas.getContext('2d');

            // Clone config to avoid mutation issues
            const chartConfig = JSON.parse(JSON.stringify(configObj));

            // Remove any callback strings (they won't work as actual functions)
            if (chartConfig.options?.plugins?.tooltip?.callbacks) {
                delete chartConfig.options.plugins.tooltip.callbacks;
            }

            currentChart = new Chart(ctx, chartConfig);
        } else {
            // Fallback: show config as JSON
            modalBody.innerHTML = `
                <div class="viz-info">
                    <strong>Type:</strong> ${escapeHtml(type)}<br>
                    <strong>URI:</strong> ${escapeHtml(uri)}
                </div>
                <div class="viz-config">
                    <h4>Configuration</h4>
                    <pre>${JSON.stringify(configObj, null, 2)}</pre>
                </div>
                <div class="viz-render" id="viz-render-area">
                    <p><em>This visualization type is not yet supported for rendering.</em></p>
                </div>
                ${summaryHtml}
            `;
        }
    } catch (e) {
        modalBody.innerHTML = `<div class="placeholder">Error parsing visualization config: ${e.message}</div>`;
    }

    modal.classList.add('active');
}

/**
 * Close the visualization modal
 */
function closeVisualizationModal() {
    const modal = document.getElementById('viz-modal');
    if (modal) {
        modal.classList.remove('active');
    }
}

// ==================== PDF Export Functions ====================

/**
 * Export current chat conversation to PDF
 * Smart export: if on History tab with a session selected, exports that instead
 */
async function exportChatToPdf() {
    // Check which tab is active
    const historyTabActive = document.querySelector('#tab-history.active');

    if (historyTabActive) {
        // On History tab - export the session preview if available
        const sessionPreview = document.querySelector('.session-preview');
        if (sessionPreview && sessionPreview.children.length > 0) {
            // Get session title from the details panel
            const titleElement = document.querySelector('#session-details .query-detail-section p');
            const title = titleElement ? titleElement.textContent : 'session';
            await exportSessionToPdf(title);
            return;
        } else {
            alert('No session selected to export. Please select a session first.');
            return;
        }
    }

    // On Chat tab - export chat area
    const messages = chatArea.querySelectorAll('.chat-message');
    if (messages.length === 0) {
        alert('No conversation to export.');
        return;
    }

    statusSpan.textContent = 'Generating PDF...';
    statusSpan.className = 'status running';

    try {
        await generatePdfFromElement(chatArea, 'chat-export.pdf');
        statusSpan.textContent = 'PDF exported!';
        statusSpan.className = 'status';
        setTimeout(() => {
            if (statusSpan.textContent === 'PDF exported!') {
                statusSpan.textContent = 'Ready';
            }
        }, 2000);
    } catch (err) {
        console.error('PDF export failed:', err);
        addErrorMessage('Failed to export PDF: ' + err.message);
        statusSpan.textContent = 'Ready';
        statusSpan.className = 'status';
    }
}

/**
 * Export session preview to PDF (from History tab)
 */
async function exportSessionToPdf(title) {
    const sessionPreview = document.querySelector('.session-preview');
    if (!sessionPreview) {
        alert('No session preview to export.');
        return;
    }

    statusSpan.textContent = 'Generating PDF...';
    statusSpan.className = 'status running';

    // Create filename from title
    const filename = (title || 'session').replace(/[^a-z0-9]/gi, '-').toLowerCase() + '.pdf';

    try {
        await generatePdfFromElement(sessionPreview, filename);
        statusSpan.textContent = 'PDF exported!';
        statusSpan.className = 'status';
        setTimeout(() => {
            if (statusSpan.textContent === 'PDF exported!') {
                statusSpan.textContent = 'Ready';
            }
        }, 2000);
    } catch (err) {
        console.error('PDF export failed:', err);
        addErrorMessage('Failed to export PDF: ' + err.message);
        statusSpan.textContent = 'Ready';
        statusSpan.className = 'status';
    }
}

/**
 * Generate PDF from an HTML element using html2canvas and jsPDF
 */
async function generatePdfFromElement(element, filename) {
    const { jsPDF } = window.jspdf;

    // Store original styles to restore later
    const originalMaxHeight = element.style.maxHeight;
    const originalOverflow = element.style.overflow;

    // Temporarily remove height constraints for full capture
    element.style.maxHeight = 'none';
    element.style.overflow = 'visible';

    // Wait a moment for reflow
    await new Promise(resolve => setTimeout(resolve, 100));

    // html2canvas 1.4.1 cannot parse the modern CSS color() function that
    // Chrome now emits from getComputedStyle for system colors. Fix: inline
    // all computed color values on the cloned DOM, converting color() to rgb().
    const COLOR_PROPS = [
        'color', 'background-color',
        'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
        'outline-color', 'fill', 'stroke'
    ];
    function colorFnToRgb(value) {
        if (!value || !value.includes('color(')) return value;
        return value.replace(
            /color\(\s*[\w-]+\s+([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+%?)(?:\s*\/\s*([\d.]+%?))?\s*\)/g,
            (_, r, g, b, a) => {
                function ch(v) {
                    return Math.round(v.endsWith('%') ? parseFloat(v) * 2.55 : parseFloat(v) * 255);
                }
                const ri = ch(r), gi = ch(g), bi = ch(b);
                if (a !== undefined) {
                    const ai = a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a);
                    return `rgba(${ri},${gi},${bi},${ai.toFixed(3)})`;
                }
                return `rgb(${ri},${gi},${bi})`;
            }
        );
    }
    const canvas = await html2canvas(element, {
        scale: 2, // Higher quality
        useCORS: true,
        allowTaint: true,
        backgroundColor: '#ffffff',
        logging: false,
        onclone: (_clonedDoc, clonedElement) => {
            const originals = [element, ...element.querySelectorAll('*')];
            const clones = [clonedElement, ...clonedElement.querySelectorAll('*')];
            for (let i = 0; i < originals.length && i < clones.length; i++) {
                const computed = window.getComputedStyle(originals[i]);
                for (const prop of COLOR_PROPS) {
                    const value = colorFnToRgb(computed.getPropertyValue(prop));
                    if (value) {
                        clones[i].style.setProperty(prop, value);
                    }
                }
            }
        }
    });

    // Restore original styles
    element.style.maxHeight = originalMaxHeight;
    element.style.overflow = originalOverflow;

    // Calculate PDF dimensions (A4 paper)
    const imgWidth = 210; // A4 width in mm
    const pageHeight = 297; // A4 height in mm
    const imgHeight = (canvas.height * imgWidth) / canvas.width;

    // Create PDF
    const pdf = new jsPDF('p', 'mm', 'a4');
    let heightLeft = imgHeight;
    let position = 0;

    // Add image to PDF, handling multiple pages if needed
    const imgData = canvas.toDataURL('image/png');

    // First page
    pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
    heightLeft -= pageHeight;

    // Additional pages if content is longer than one page
    while (heightLeft > 0) {
        position = heightLeft - imgHeight;
        pdf.addPage();
        pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
        heightLeft -= pageHeight;
    }

    // Download the PDF
    pdf.save(filename);
}

// ==================== Session Storage Functions ====================

/**
 * Save current conversation as a session
 */
function saveSession() {
    // Check if there's a conversation to save
    const messages = chatArea.querySelectorAll('.chat-message');
    if (messages.length === 0) {
        alert('No conversation to save.');
        return;
    }

    // Prompt for title (default to existing if continuing)
    const defaultTitle = currentSessionTitle || `Session ${new Date().toLocaleString()}`;
    const title = prompt('Enter a title for this session:', defaultTitle);
    if (!title) {
        return; // User cancelled
    }

    statusSpan.textContent = 'Saving session...';
    statusSpan.className = 'status running';

    fetch('api/sessions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            sessionId: currentSessionId,  // null for new, or existing ID for update
            title: title
        })
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            currentSessionId = data.sessionId;
            currentSessionTitle = title;
            statusSpan.textContent = 'Session saved!';
            statusSpan.className = 'status';
            setTimeout(() => {
                if (statusSpan.textContent === 'Session saved!') {
                    statusSpan.textContent = 'Ready';
                }
            }, 2000);
        } else {
            addErrorMessage('Failed to save session: ' + (data.error || 'Unknown error'));
            statusSpan.textContent = 'Ready';
            statusSpan.className = 'status';
        }
    })
    .catch(err => {
        addErrorMessage('Failed to save session: ' + err.message);
        statusSpan.textContent = 'Ready';
        statusSpan.className = 'status';
    });
}

/**
 * Fetch sessions from the history library
 */
function fetchSessions() {
    const sessionList = document.getElementById('session-list');
    sessionList.innerHTML = '<div class="placeholder">Loading sessions...</div>';

    fetch('api/sessions')
        .then(response => response.json())
        .then(data => {
            if (data.error) {
                sessionList.innerHTML = `<div class="placeholder">Error: ${escapeHtml(data.error)}</div>`;
                return;
            }

            const sessions = data.sessions || [];
            if (sessions.length === 0) {
                sessionList.innerHTML = '<div class="placeholder">No saved sessions found.</div>';
                return;
            }

            sessionList.innerHTML = '';
            sessions.forEach(session => {
                const card = document.createElement('div');
                card.className = 'query-card';  // Reuse query card styling
                card.dataset.uri = session.uri;
                card.innerHTML = `
                    <div class="query-card-title">${escapeHtml(session.title || 'Untitled Session')}</div>
                    <div class="query-card-description">${session.messageCount || 0} messages</div>
                    <div class="query-card-meta">
                        Repository: ${escapeHtml(session.repository || 'Unknown')}
                        ${session.modified ? ` | ${new Date(session.modified).toLocaleDateString()}` : ''}
                    </div>
                `;
                card.onclick = () => selectSession(session.uri);
                sessionList.appendChild(card);
            });
        })
        .catch(err => {
            sessionList.innerHTML = `<div class="placeholder">Failed to load sessions: ${escapeHtml(err.message)}</div>`;
        });
}

/**
 * Select a session and show its details
 */
function selectSession(uri) {
    // Update selected state
    document.querySelectorAll('#session-list .query-card').forEach(card => {
        card.classList.toggle('selected', card.dataset.uri === uri);
    });

    const detailsDiv = document.getElementById('session-details');
    detailsDiv.innerHTML = '<div class="placeholder">Loading session details...</div>';

    fetch(`api/sessions/${encodeURIComponent(uri)}`)
        .then(response => response.json())
        .then(data => {
            if (data.error) {
                detailsDiv.innerHTML = `<div class="placeholder">Error: ${escapeHtml(data.error)}</div>`;
                return;
            }

            // Parse conversation history for display preview
            let conversationPreview = '';
            try {
                const history = JSON.parse(data.conversationHistory);
                // Filter to user/assistant text only for preview
                const displayMessages = history
                    .filter(msg => msg.role === 'user' || msg.role === 'assistant')
                    .map(msg => {
                        const role = msg.role === 'user' ? 'You' : 'GraphTalker';
                        let content = '';
                        if (typeof msg.content === 'string') {
                            content = msg.content;
                        } else if (Array.isArray(msg.content)) {
                            // Extract text blocks
                            content = msg.content
                                .filter(block => block.type === 'text' && block.text)
                                .map(block => block.text)
                                .join('\n');
                        }
                        if (!content) return null;

                        // Render content based on role
                        let renderedContent;
                        if (msg.role === 'user') {
                            // User messages: plain text
                            renderedContent = escapeHtml(content);
                        } else {
                            // Assistant messages: render markdown like in chat
                            const sanitized = sanitizeResponseForChat(content);
                            const processed = preprocessAsciiArt(sanitized);
                            try {
                                renderedContent = marked.parse(processed, { breaks: true, gfm: true });
                            } catch (e) {
                                renderedContent = escapeHtml(content);
                            }
                        }

                        return `<div class="session-message ${msg.role}">
                            <div class="message-label">${role}</div>
                            <div class="message-content">${renderedContent}</div>
                        </div>`;
                    })
                    .filter(Boolean)
                    .join('');
                conversationPreview = displayMessages || '<div class="placeholder">No messages to display</div>';
            } catch (e) {
                conversationPreview = '<div class="placeholder">Unable to parse conversation</div>';
            }

            const safeUri = uri.replace(/'/g, "\\'");
            const safeTitle = (data.title || 'Untitled').replace(/'/g, "\\'");

            detailsDiv.innerHTML = `
                <div class="query-detail-section">
                    <h3>Title</h3>
                    <p>${escapeHtml(data.title || 'Untitled')}</p>
                </div>
                <div class="query-detail-section">
                    <h3>Repository</h3>
                    <p>${escapeHtml(data.repository || 'Unknown')}</p>
                </div>
                <div class="query-detail-section">
                    <h3>Statistics</h3>
                    <p>${data.messageCount || 0} messages</p>
                    ${data.created ? `<p>Created: ${new Date(data.created).toLocaleString()}</p>` : ''}
                    ${data.modified ? `<p>Modified: ${new Date(data.modified).toLocaleString()}</p>` : ''}
                </div>
                <div class="query-detail-section">
                    <h3>Conversation Preview</h3>
                    <div class="session-preview">${conversationPreview}</div>
                </div>
                <div class="query-detail-section session-actions">
                    <button onclick="restoreSession('${safeUri}', '${safeTitle}')" class="primary">
                        Continue This Session
                    </button>
                    <button onclick="exportSessionToPdf('${safeTitle}')" class="secondary">
                        Export PDF
                    </button>
                    <button onclick="deleteSession('${safeUri}')" class="secondary danger">
                        Delete Session
                    </button>
                </div>
            `;

            // Wrap visualization code in collapsible widgets
            const sessionPreview = detailsDiv.querySelector('.session-preview');
            if (sessionPreview) {
                wrapVisualizationCode(sessionPreview);
            }

            // Apply syntax highlighting to code blocks in the preview
            detailsDiv.querySelectorAll('.session-preview pre code').forEach(block => {
                hljs.highlightElement(block);
            });
        })
        .catch(err => {
            detailsDiv.innerHTML = `<div class="placeholder">Failed to load session: ${escapeHtml(err.message)}</div>`;
        });
}

/**
 * Restore a session and switch to chat tab
 */
function restoreSession(uri, title) {
    if (!confirm(`Restore session "${title}"? This will replace your current conversation.`)) {
        return;
    }

    statusSpan.textContent = 'Restoring session...';
    statusSpan.className = 'status running';

    fetch(`api/sessions/${encodeURIComponent(uri)}/restore`, { method: 'POST' })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                // Update current session tracking
                currentSessionId = data.sessionId;
                currentSessionTitle = title;

                // Rebuild chat UI from returned conversation display
                rebuildChatFromSession(data.conversationDisplay || []);

                // Update context bar
                if (data.contextPercentage !== undefined) {
                    updateContextBar(data.contextPercentage);
                }

                // Reset iterations panel
                iterationsDiv.innerHTML = '<div class="placeholder">Session restored. Continue asking questions...</div>';

                // Switch to chat tab
                switchTab('chat');

                statusSpan.textContent = `Restored: ${title}`;
                statusSpan.className = 'status';
                setTimeout(() => {
                    if (statusSpan.textContent === `Restored: ${title}`) {
                        statusSpan.textContent = 'Ready';
                    }
                }, 2000);
            } else {
                addErrorMessage('Failed to restore session: ' + (data.error || 'Unknown error'));
                statusSpan.textContent = 'Ready';
                statusSpan.className = 'status';
            }
        })
        .catch(err => {
            addErrorMessage('Failed to restore session: ' + err.message);
            statusSpan.textContent = 'Ready';
            statusSpan.className = 'status';
        });
}

/**
 * Rebuild chat UI from session conversation
 */
function rebuildChatFromSession(conversationDisplay) {
    // Clear existing chat
    chatArea.innerHTML = '';

    // Add each message
    conversationDisplay.forEach(msg => {
        if (msg.role === 'user') {
            addUserMessage(msg.content);
        } else if (msg.role === 'assistant') {
            addAssistantMessage(msg.content);
        }
    });

    // If no messages, show welcome
    if (conversationDisplay.length === 0) {
        chatArea.innerHTML = welcomeMessageHTML(`<p>Session restored but no displayable messages found.</p>`);
    }

    scrollChatToBottom();
}

/**
 * Delete a session
 */
function deleteSession(uri) {
    if (!confirm('Are you sure you want to delete this session?')) {
        return;
    }

    fetch(`api/sessions/${encodeURIComponent(uri)}`, { method: 'DELETE' })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                // Refresh session list
                fetchSessions();
                // Clear details panel
                document.getElementById('session-details').innerHTML =
                    '<div class="placeholder">Session deleted. Select another session...</div>';
            } else {
                addErrorMessage('Failed to delete session: ' + (data.error || 'Unknown error'));
            }
        })
        .catch(err => {
            addErrorMessage('Failed to delete session: ' + err.message);
        });
}

// ==================== API Key Warning ====================

async function checkApiKeyStatus() {
    try {
        const resp = await fetch('api/settings/anthropicApiKey');
        const data = await resp.json();
        if (!data.keySet) {
            showApiKeyWarning();
        }
    } catch (e) { /* silently ignore — server may not support endpoint */ }
}

function showApiKeyWarning() {
    const modal = document.createElement('div');
    modal.id = 'api-key-warning-modal';
    modal.className = 'modal-overlay active';
    modal.innerHTML = `
        <div class="modal-content">
            <div class="modal-header">
                <h3>Anthropic API Key Not Set</h3>
                <button class="modal-close" onclick="document.getElementById('api-key-warning-modal').remove()">&times;</button>
            </div>
            <div class="modal-body">
                <p>Anthropic API key is not set. GraphTalker will not be able to answer questions until a key is provided.</p>
                <p>Enter your key below and choose how to save it:</p>
                <input id="api-key-input" type="password" placeholder="sk-ant-..."
                       style="width:100%;box-sizing:border-box;margin-top:12px;padding:8px 10px;border:1px solid var(--color-primary-main);border-radius:6px;background:var(--color-grey-100);color:var(--color-text-primary);font-size:14px;outline:none;">
                <div class="repo-selector" style="margin-top:8px;width:100%;">
                    <span class="repo-label">Save for:</span>
                    <select id="api-key-save-type" style="flex:1;max-width:100%;min-width:0;">
                        <option value="session">Session (kept in memory for this GraphTalker instance only)</option>
                        <option value="user">User (persisted for the current user and repository)</option>
                        <option value="repository">Repository (persisted for all users of the current repository)</option>
                    </select>
                </div>
                <button id="api-key-submit" class="secondary" onclick="submitApiKeyFromWarning()"
                        style="width:100%;margin-top:12px;background-color:var(--color-primary-main);color:#fff;">
                    Save API Key
                </button>
                <p id="api-key-error" style="color:var(--error,#e05);margin-top:8px;display:none;"></p>
            </div>
        </div>`;
    document.body.appendChild(modal);

    function syncSaveTypeOptions() {
        const saveType = document.getElementById('api-key-save-type');
        if (!saveType) return;
        const repoSelected = !!document.getElementById('repo-dropdown').value;
        saveType.querySelector('option[value="user"]').disabled = !repoSelected;
        saveType.querySelector('option[value="repository"]').disabled = !repoSelected;
        if (!repoSelected && (saveType.value === 'user' || saveType.value === 'repository'))
            saveType.value = 'session';
    }
    syncSaveTypeOptions();
    document.getElementById('repo-dropdown').addEventListener('change', syncSaveTypeOptions);

    document.getElementById('api-key-input').focus();
}

async function submitApiKeyFromWarning() {
    const input = document.getElementById('api-key-input');
    const errorEl = document.getElementById('api-key-error');
    const key = input.value.trim();
    if (!key) {
        errorEl.textContent = 'Please enter an API key.';
        errorEl.style.display = 'block';
        return;
    }
    try {
        const saveType = document.getElementById('api-key-save-type').value;
        const resp = await fetch('api/settings/anthropicApiKey', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ apiKey: key, saveType: saveType })
        });
        const data = await resp.json();
        if (data.success) {
            document.getElementById('api-key-warning-modal').remove();
        } else {
            errorEl.textContent = data.error || 'Failed to save key.';
            errorEl.style.display = 'block';
        }
    } catch (e) {
        errorEl.textContent = 'Request failed: ' + e.message;
        errorEl.style.display = 'block';
    }
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
    chatArea.innerHTML = welcomeMessageHTML(`
        ${WELCOME_INTRO}
        <p class="hint">Try: "What classes are in the schema?" or "show me all products".</p>`);
    fetchVersion();
    fetchContextStats();
    fetchRepositories();
    checkApiKeyStatus();
    questionInput.focus();
    sendHeartbeat();
    heartbeatInterval = setInterval(sendHeartbeat, 30000);
});
