<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Mnemograph Viewer</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #1a1a2e;
            color: #eee;
            overflow: hidden;
        }

        #container {
            width: 100vw;
            height: 100vh;
            position: relative;
        }

        svg {
            width: 100%;
            height: 100%;
        }

        /* Controls panel */
        #controls {
            position: absolute;
            top: 16px;
            left: 16px;
            background: rgba(30, 30, 50, 0.95);
            padding: 16px;
            border-radius: 8px;
            min-width: 240px;
            z-index: 100;
        }

        #controls h2 {
            font-size: 14px;
            margin-bottom: 12px;
            color: #888;
            text-transform: uppercase;
            letter-spacing: 1px;
        }

        #controls input[type="text"] {
            width: 100%;
            padding: 8px;
            border: 1px solid #444;
            border-radius: 4px;
            background: #2a2a4a;
            color: #eee;
            margin-bottom: 12px;
        }

        #controls label {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 8px;
            cursor: pointer;
        }

        #controls input[type="checkbox"] {
            width: 16px;
            height: 16px;
        }

        .control-section {
            margin: 12px 0;
            padding-top: 12px;
            border-top: 1px solid #333;
        }

        .control-section label {
            display: block;
            margin-bottom: 4px;
            font-size: 11px;
            color: #888;
            text-transform: uppercase;
        }

        .control-section select {
            width: 100%;
            padding: 6px 8px;
            border: 1px solid #444;
            border-radius: 4px;
            background: #2a2a4a;
            color: #eee;
            font-size: 13px;
        }

        .control-section input[type="range"] {
            width: 100%;
            margin-top: 4px;
        }

        .stats {
            margin-top: 12px;
            padding-top: 12px;
            border-top: 1px solid #444;
            font-size: 12px;
            color: #888;
        }

        #refreshBtn {
            width: 100%;
            margin-top: 12px;
            padding: 10px;
            background: #3b82f6;
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            cursor: pointer;
            transition: background 0.2s;
        }

        #refreshBtn:hover {
            background: #2563eb;
        }

        #refreshBtn:disabled {
            background: #6b7280;
            cursor: not-allowed;
        }

        #stopBtn {
            width: 100%;
            margin-top: 8px;
            padding: 10px;
            background: #dc2626;
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            cursor: pointer;
            transition: background 0.2s;
        }

        #stopBtn:hover {
            background: #b91c1c;
        }

        /* Info panel (hover details) */
        #info {
            position: absolute;
            top: 16px;
            right: 16px;
            background: rgba(30, 30, 50, 0.95);
            padding: 16px;
            border-radius: 8px;
            max-width: 320px;
            z-index: 100;
            display: none;
        }

        #info.visible { display: block; }

        #info h3 {
            font-size: 16px;
            margin-bottom: 8px;
        }

        #info .type {
            font-size: 11px;
            text-transform: uppercase;
            color: #888;
            margin-bottom: 12px;
        }

        #info .observations {
            font-size: 13px;
            line-height: 1.5;
            color: #ccc;
        }

        #info .observation {
            margin-bottom: 8px;
            padding-left: 12px;
            border-left: 2px solid #444;
        }

        /* Legend */
        #legend {
            position: absolute;
            bottom: 16px;
            left: 16px;
            background: rgba(30, 30, 50, 0.95);
            padding: 12px 16px;
            border-radius: 8px;
            display: flex;
            gap: 16px;
            font-size: 12px;
        }

        .legend-item {
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .legend-dot {
            width: 12px;
            height: 12px;
            border-radius: 50%;
        }

        /* Graph elements */
        .node {
            cursor: pointer;
        }

        .node circle {
            stroke: #fff;
            stroke-width: 1.5px;
        }

        .node.ghost circle {
            stroke: #666;
        }

        .node text {
            font-size: 11px;
            fill: #fff;
            pointer-events: none;
        }

        .node.ghost text {
            fill: #666;
        }

        .node.dimmed {
            opacity: 0.2;
        }

        .link {
            stroke: #666;
            stroke-opacity: 0.6;
        }

        .link.ghost {
            stroke-opacity: 0.2;
        }

        .link.highlighted {
            stroke: #fff;
            stroke-opacity: 1;
        }

        /* Tooltip */
        #tooltip {
            position: absolute;
            background: rgba(0, 0, 0, 0.8);
            color: #fff;
            padding: 6px 10px;
            border-radius: 4px;
            font-size: 12px;
            pointer-events: none;
            z-index: 200;
            display: none;
        }

        /* Server stopped overlay */
        #stopped-overlay {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: #1a1a2e;
            align-items: center;
            justify-content: center;
            color: #888;
            font-family: sans-serif;
            z-index: 1000;
        }

        #stopped-overlay.visible {
            display: flex;
        }

        #stopped-overlay .content {
            text-align: center;
        }
    </style>
</head>
<body>
    <div id="container">
        <svg></svg>

        <div id="controls">
            <h2>Mnemograph</h2>
            <input type="text" id="search" placeholder="Search entities...">
            <label>
                <input type="checkbox" id="showGhosts" checked>
                Show off-branch entities
            </label>
            <label>
                <input type="checkbox" id="showLabels" checked>
                Show labels
            </label>

            <div class="control-section">
                <label for="layout">Layout</label>
                <select id="layout">
                    <option value="force">Force-directed</option>
                    <option value="radial">Radial</option>
                    <option value="cluster">Clustered</option>
                </select>
            </div>

            <div class="control-section">
                <label for="colorMode">Color by</label>
                <select id="colorMode">
                    <option value="type">Entity type</option>
                    <option value="component">Component</option>
                    <option value="degree">Degree (centrality)</option>
                </select>
            </div>

            <div class="control-section">
                <label for="weightFilter">Min edge weight: <span id="weightValue">0.0</span></label>
                <input type="range" id="weightFilter" min="0" max="1" step="0.1" value="0">
            </div>

            <div class="stats">
                <div id="branchName">Branch: main</div>
                <div id="entityCount">Entities: 0</div>
                <div id="relationCount">Relations: 0</div>
            </div>

            <button id="refreshBtn" style="display: none;">Refresh</button>
            <button id="stopBtn" style="display: none;">Stop Server</button>
        </div>

        <div id="info">
            <h3 id="infoName"></h3>
            <div class="type" id="infoType"></div>
            <div class="observations" id="infoObservations"></div>
        </div>

        <div id="legend"></div>

        <div id="tooltip"></div>
    </div>

    <div id="stopped-overlay">
        <div class="content">
            <h2>Server stopped</h2>
            <p>You can close this tab.</p>
        </div>
    </div>

    <script>
        // Entity type colors
        const TYPE_COLORS = {
            concept: '#6366f1',    // Indigo
            decision: '#f59e0b',   // Amber
            project: '#10b981',    // Emerald
            pattern: '#8b5cf6',    // Violet
            question: '#ef4444',   // Red
            learning: '#06b6d4',   // Cyan
            entity: '#6b7280',     // Gray
            default: '#6b7280'     // Gray
        };

        // Component colors for clustering
        const COMPONENT_COLORS = [
            '#e11d48', '#db2777', '#c026d3', '#9333ea', '#7c3aed',
            '#6366f1', '#3b82f6', '#0ea5e9', '#06b6d4', '#14b8a6',
            '#10b981', '#22c55e', '#84cc16', '#eab308', '#f59e0b'
        ];

        // State
        let graphData = null;
        let simulation = null;
        let selectedNode = null;
        let currentLayout = 'force';
        let currentColorMode = 'type';
        let minWeight = 0;

        // Find connected components using Union-Find
        function findComponents(nodes, links) {
            const parent = new Map();
            const rank = new Map();

            function find(x) {
                if (!parent.has(x)) {
                    parent.set(x, x);
                    rank.set(x, 0);
                }
                if (parent.get(x) !== x) {
                    parent.set(x, find(parent.get(x)));
                }
                return parent.get(x);
            }

            function union(x, y) {
                const px = find(x), py = find(y);
                if (px === py) return;
                if (rank.get(px) < rank.get(py)) {
                    parent.set(px, py);
                } else if (rank.get(px) > rank.get(py)) {
                    parent.set(py, px);
                } else {
                    parent.set(py, px);
                    rank.set(px, rank.get(px) + 1);
                }
            }

            nodes.forEach(n => find(n.id));
            links.forEach(l => union(l.source.id || l.source, l.target.id || l.target));

            // Map each root to a component index
            const roots = [...new Set(nodes.map(n => find(n.id)))];
            const componentMap = new Map(roots.map((r, i) => [r, i]));

            return new Map(nodes.map(n => [n.id, componentMap.get(find(n.id))]));
        }

        // Get color based on current mode
        function getNodeColor(node, degrees, components) {
            if (currentColorMode === 'type') {
                return TYPE_COLORS[node.type] || TYPE_COLORS.default;
            } else if (currentColorMode === 'component') {
                const comp = components.get(node.id) || 0;
                return COMPONENT_COLORS[comp % COMPONENT_COLORS.length];
            } else if (currentColorMode === 'degree') {
                const degree = degrees.get(node.id) || 0;
                const maxDegree = Math.max(...degrees.values(), 1);
                const t = degree / maxDegree;
                // Interpolate from blue (low) to red (high)
                return d3.interpolateViridis(t);
            }
            return TYPE_COLORS.default;
        }

        // Escape HTML to prevent XSS
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.textContent;
        }

        // Load data from file or embedded
        async function loadData() {
            // Check for data in URL hash
            const hash = window.location.hash.slice(1);
            if (hash) {
                try {
                    const response = await fetch(hash);
                    return await response.json();
                } catch (e) {
                    console.error('Failed to load:', hash, e);
                }
            }

            // Check for embedded data
            const embedded = document.getElementById('graph-data');
            if (embedded) {
                return JSON.parse(embedded.textContent);
            }

            // Prompt for file
            return new Promise((resolve) => {
                const input = document.createElement('input');
                input.type = 'file';
                input.accept = '.json';
                input.onchange = async (e) => {
                    const file = e.target.files[0];
                    const text = await file.text();
                    resolve(JSON.parse(text));
                };
                input.click();
            });
        }

        // Initialize visualization
        async function init() {
            graphData = await loadData();

            // Update stats using textContent (safe)
            document.getElementById('branchName').textContent = 'Branch: ' + graphData.meta.branch;
            document.getElementById('entityCount').textContent = 'Entities: ' + graphData.meta.entity_count;
            document.getElementById('relationCount').textContent = 'Relations: ' + graphData.meta.relation_count;

            // Build legend
            const types = [...new Set(graphData.entities.map(e => e.type))];
            const legend = document.getElementById('legend');
            legend.replaceChildren();
            types.forEach(type => {
                const item = document.createElement('div');
                item.className = 'legend-item';
                const dot = document.createElement('div');
                dot.className = 'legend-dot';
                dot.style.background = TYPE_COLORS[type] || TYPE_COLORS.default;
                const label = document.createElement('span');
                label.textContent = type;
                item.appendChild(dot);
                item.appendChild(label);
                legend.appendChild(item);
            });

            render();
            setupControls();
        }

        function render() {
            const svg = d3.select('svg');
            const width = window.innerWidth;
            const height = window.innerHeight;

            svg.selectAll('*').remove();

            // Create container for zoom
            const g = svg.append('g');

            // Setup zoom
            const zoom = d3.zoom()
                .scaleExtent([0.1, 4])
                .on('zoom', (event) => g.attr('transform', event.transform));

            svg.call(zoom);

            // Filter based on ghost visibility and weight
            const showGhosts = document.getElementById('showGhosts').checked;
            const entities = showGhosts
                ? graphData.entities
                : graphData.entities.filter(e => e.on_branch);
            const entityIds = new Set(entities.map(e => e.id));
            const relations = graphData.relations.filter(
                r => entityIds.has(r.from) && entityIds.has(r.to) && r.weight >= minWeight
            );

            // Create node map
            const nodeMap = new Map(entities.map(e => [e.id, {...e}]));

            // Calculate node degrees
            const degrees = new Map();
            relations.forEach(r => {
                degrees.set(r.from, (degrees.get(r.from) || 0) + 1);
                degrees.set(r.to, (degrees.get(r.to) || 0) + 1);
            });

            // Create links with source/target objects
            const links = relations.map(r => ({
                ...r,
                source: nodeMap.get(r.from),
                target: nodeMap.get(r.to)
            }));

            const nodes = Array.from(nodeMap.values());

            // Find connected components for coloring
            const components = findComponents(nodes, links);

            // Create simulation based on layout type
            simulation = d3.forceSimulation(nodes);

            if (currentLayout === 'force') {
                simulation
                    .force('link', d3.forceLink(links).id(d => d.id).distance(100))
                    .force('charge', d3.forceManyBody().strength(-300))
                    .force('center', d3.forceCenter(width / 2, height / 2))
                    .force('collision', d3.forceCollide().radius(30));
            } else if (currentLayout === 'radial') {
                // Radial layout - arrange by degree (hubs in center)
                const maxDegree = Math.max(...degrees.values(), 1);
                simulation
                    .force('link', d3.forceLink(links).id(d => d.id).distance(80))
                    .force('charge', d3.forceManyBody().strength(-100))
                    .force('r', d3.forceRadial(d => {
                        const degree = degrees.get(d.id) || 0;
                        return 50 + (1 - degree / maxDegree) * Math.min(width, height) * 0.35;
                    }, width / 2, height / 2).strength(0.8))
                    .force('collision', d3.forceCollide().radius(25));
            } else if (currentLayout === 'cluster') {
                // Cluster by component
                const numComponents = new Set(components.values()).size;
                const angleStep = (2 * Math.PI) / Math.max(numComponents, 1);
                const clusterRadius = Math.min(width, height) * 0.3;

                simulation
                    .force('link', d3.forceLink(links).id(d => d.id).distance(60))
                    .force('charge', d3.forceManyBody().strength(-150))
                    .force('x', d3.forceX(d => {
                        const comp = components.get(d.id) || 0;
                        return width / 2 + Math.cos(comp * angleStep) * clusterRadius;
                    }).strength(0.3))
                    .force('y', d3.forceY(d => {
                        const comp = components.get(d.id) || 0;
                        return height / 2 + Math.sin(comp * angleStep) * clusterRadius;
                    }).strength(0.3))
                    .force('collision', d3.forceCollide().radius(20));
            }

            // Draw links
            const link = g.append('g')
                .selectAll('line')
                .data(links)
                .join('line')
                .attr('class', d => 'link ' + (d.on_branch ? '' : 'ghost'))
                .attr('stroke-width', d => 1 + d.weight * 3);

            // Draw nodes
            const node = g.append('g')
                .selectAll('g')
                .data(nodes)
                .join('g')
                .attr('class', d => 'node ' + (d.on_branch ? '' : 'ghost'))
                .call(d3.drag()
                    .on('start', dragstarted)
                    .on('drag', dragged)
                    .on('end', dragended));

            // Node circles
            node.append('circle')
                .attr('r', d => 8 + Math.sqrt(degrees.get(d.id) || 1) * 3)
                .attr('fill', d => getNodeColor(d, degrees, components))
                .attr('fill-opacity', d => d.on_branch ? 1 : 0.2);

            // Node labels
            node.append('text')
                .attr('dx', 12)
                .attr('dy', 4)
                .text(d => d.name)
                .attr('class', 'label');

            // Hover handlers
            node.on('mouseover', (event, d) => showInfo(d))
                .on('mouseout', () => hideInfo())
                .on('click', (event, d) => selectNode(d, link, node));

            link.on('mouseover', (event, d) => showTooltip(event, d.type))
                .on('mouseout', hideTooltip);

            // Simulation tick
            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);

                node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
            });

            // Initial zoom to fit
            setTimeout(() => {
                const bounds = g.node().getBBox();
                if (bounds.width > 0 && bounds.height > 0) {
                    const scale = 0.8 * Math.min(width / bounds.width, height / bounds.height);
                    const tx = width / 2 - scale * (bounds.x + bounds.width / 2);
                    const ty = height / 2 - scale * (bounds.y + bounds.height / 2);
                    svg.call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
                }
            }, 500);
        }

        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;
        }

        function showInfo(entity) {
            const info = document.getElementById('info');
            document.getElementById('infoName').textContent = entity.name;
            document.getElementById('infoType').textContent = entity.type;

            // Build observations safely using DOM methods
            const obsContainer = document.getElementById('infoObservations');
            obsContainer.replaceChildren();
            entity.observations.forEach(obs => {
                const div = document.createElement('div');
                div.className = 'observation';
                div.textContent = obs;
                obsContainer.appendChild(div);
            });

            info.classList.add('visible');
        }

        function hideInfo() {
            if (!selectedNode) {
                document.getElementById('info').classList.remove('visible');
            }
        }

        function selectNode(d, links, nodes) {
            if (selectedNode === d) {
                // Deselect
                selectedNode = null;
                links.classed('highlighted', false);
                nodes.classed('selected', false);
                hideInfo();
            } else {
                // Select
                selectedNode = d;
                showInfo(d);

                // Highlight connected links
                links.classed('highlighted', l =>
                    l.source.id === d.id || l.target.id === d.id
                );
            }
        }

        function showTooltip(event, text) {
            const tooltip = document.getElementById('tooltip');
            tooltip.textContent = text;
            tooltip.style.left = event.pageX + 10 + 'px';
            tooltip.style.top = event.pageY + 10 + 'px';
            tooltip.style.display = 'block';
        }

        function hideTooltip() {
            document.getElementById('tooltip').style.display = 'none';
        }

        function setupControls() {
            // Search
            document.getElementById('search').addEventListener('input', (e) => {
                const query = e.target.value.toLowerCase();
                d3.selectAll('.node').classed('dimmed', d =>
                    query && !d.name.toLowerCase().includes(query)
                );
            });

            // Show ghosts toggle
            document.getElementById('showGhosts').addEventListener('change', render);

            // Show labels toggle
            document.getElementById('showLabels').addEventListener('change', (e) => {
                d3.selectAll('.label').style('display', e.target.checked ? 'block' : 'none');
            });

            // Layout selector
            document.getElementById('layout').addEventListener('change', (e) => {
                currentLayout = e.target.value;
                render();
            });

            // Color mode selector
            document.getElementById('colorMode').addEventListener('change', (e) => {
                currentColorMode = e.target.value;
                render();
            });

            // Weight filter slider
            const weightSlider = document.getElementById('weightFilter');
            const weightValue = document.getElementById('weightValue');
            weightSlider.addEventListener('input', (e) => {
                minWeight = parseFloat(e.target.value);
                weightValue.textContent = minWeight.toFixed(1);
                render();
            });

            // Refresh and Stop buttons (only shown in watch mode)
            const refreshBtn = document.getElementById('refreshBtn');
            const stopBtn = document.getElementById('stopBtn');
            if (window.API_ENABLED) {
                refreshBtn.style.display = 'block';
                stopBtn.style.display = 'block';

                refreshBtn.addEventListener('click', async () => {
                    refreshBtn.disabled = true;
                    refreshBtn.textContent = 'Loading...';
                    try {
                        const response = await fetch('/api/graph');
                        if (response.ok) {
                            graphData = await response.json();
                            render();
                            refreshBtn.textContent = 'Refreshed!';
                            setTimeout(() => { refreshBtn.textContent = 'Refresh'; }, 1000);
                        } else {
                            refreshBtn.textContent = 'Error';
                            setTimeout(() => { refreshBtn.textContent = 'Refresh'; }, 2000);
                        }
                    } catch (err) {
                        console.error('Refresh failed:', err);
                        refreshBtn.textContent = 'Error';
                        setTimeout(() => { refreshBtn.textContent = 'Refresh'; }, 2000);
                    }
                    refreshBtn.disabled = false;
                });

                stopBtn.addEventListener('click', async () => {
                    if (!confirm('Stop the visualization server?')) return;
                    stopBtn.disabled = true;
                    stopBtn.textContent = 'Stopping...';
                    try {
                        await fetch('/api/shutdown', { method: 'POST' });
                        stopBtn.textContent = 'Server stopped';
                        refreshBtn.disabled = true;
                        // Show stopped overlay (safe, no innerHTML)
                        document.getElementById('stopped-overlay').classList.add('visible');
                    } catch (err) {
                        console.error('Shutdown failed:', err);
                        stopBtn.textContent = 'Error';
                        setTimeout(() => { stopBtn.textContent = 'Stop Server'; stopBtn.disabled = false; }, 2000);
                    }
                });
            }
        }

        // Handle window resize
        window.addEventListener('resize', () => {
            if (simulation) {
                simulation.force('center', d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2));
                simulation.alpha(0.3).restart();
            }
        });

        // Start when DOM is ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            // DOM already loaded, but wait a tick for any script tags to be parsed
            setTimeout(init, 0);
        }
    </script>
</body>
</html>
