<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>

    <!-- Include theme CSS - absolute path with baseurl support -->
    <link rel="stylesheet" href="{{ css_path }}">

    <!-- Theme configuration defaults -->
    <script>
        window.BENGAL_THEME_DEFAULTS = {
            appearance: '{{ default_appearance }}',
            palette: '{{ default_palette }}'
        };
    </script>

    <!-- Theme & Palette initialization - INLINED to prevent FOUC (must be synchronous) -->
    <script>
        (function () {
            try {
                var defaults = window.BENGAL_THEME_DEFAULTS || { appearance: 'system', palette: '' };
                var defaultAppearance = defaults.appearance;
                if (defaultAppearance === 'system') {
                    defaultAppearance = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
                        ? 'dark' : 'light';
                }
                var storedTheme = localStorage.getItem('bengal-theme');
                var storedPalette = localStorage.getItem('bengal-palette');
                var theme = storedTheme ? (storedTheme === 'system' ? defaultAppearance : storedTheme) : defaultAppearance;
                var palette = storedPalette ?? defaults.palette;
                document.documentElement.setAttribute('data-theme', theme);
                if (palette) { document.documentElement.setAttribute('data-palette', palette); }
            } catch (e) { document.documentElement.setAttribute('data-theme', 'light'); }
        })();
    </script>

    <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body class="graph-body">
    <div id="container" class="graph-container">
        <div class="graph-controls">
            <h2>Knowledge Graph</h2>
            <div class="graph-tag-filter-badge hidden" id="tag-filter-badge">
                <span>Filtering by tag: <strong id="tag-filter-value"></strong></span>
                <a href="?" id="tag-filter-clear" class="graph-tag-filter-clear">Show all</a>
            </div>
            <input
                type="text"
                id="search"
                placeholder="Search pages or tags..."
                autocomplete="off"
            />
            <div class="graph-filter-group">
                <label>Filter by Type:</label>
                <select id="filter-type">
                    <option value="all">All Pages</option>
                    <option value="hub">Hubs Only</option>
                    <option value="orphan">Orphans Only</option>
                    <option value="regular">Regular Pages</option>
                </select>
            </div>
            <div class="graph-stats">
                <p><strong>Pages:</strong> {{ stats.total_pages }}</p>
                <p><strong>Links:</strong> {{ stats.total_links }}</p>
                <p><strong>Hubs:</strong> {{ stats.hubs }}</p>
                <p><strong>Orphans:</strong> {{ stats.orphans }}</p>
            </div>
        </div>

        <div class="graph-legend">
            <h3>Legend</h3>
            <div class="graph-legend-item">
                <div class="graph-legend-color" style="background: var(--graph-node-hub);"></div>
                <span>Hub (highly connected)</span>
            </div>
            <div class="graph-legend-item">
                <div class="graph-legend-color" style="background: var(--graph-node-regular);"></div>
                <span>Regular page</span>
            </div>
            <div class="graph-legend-item">
                <div class="graph-legend-color" style="background: var(--graph-node-orphan);"></div>
                <span>Orphan (no incoming links)</span>
            </div>
            <div class="graph-legend-item">
                <div class="graph-legend-color" style="background: var(--graph-node-generated);"></div>
                <span>Generated page</span>
            </div>
        </div>

        <div id="graph" class="graph-svg">
            <div class="graph-loading" id="graph-loading">
                <p>Loading graph...</p>
            </div>
        </div>
        <div class="graph-tooltip" id="tooltip"></div>
    </div>

    <script>
        const graphJsonUrl = "{{ graph_json_url }}";

        async function loadAndRenderGraph() {
            const loadingEl = document.getElementById('graph-loading');
            let graphData;
            try {
                const res = await fetch(graphJsonUrl);
                if (!res.ok) throw new Error(res.statusText);
                graphData = await res.json();
            } catch (err) {
                loadingEl.innerHTML = '<p>Failed to load graph. ' + (err.message || 'Unknown error') + '</p>';
                return;
            }

        // Dimensions
        const width = window.innerWidth;
        const height = window.innerHeight;

        // Create SVG
        const svg = d3.select("#graph")
            .append("svg")
            .attr("width", width)
            .attr("height", height);

        // Add zoom behavior (stored for programmatic fit-to-view)
        const g = svg.append("g");
        const zoomBehavior = d3.zoom()
            .scaleExtent([0.1, 4])
            .on("zoom", (event) => {
                g.attr("transform", event.transform);
            });
        svg.call(zoomBehavior);

        // Helper function to resolve CSS variables to actual colors
        function resolveCSSVariable(varName) {
            const cleanVar = varName.replace(/var\(|\s|\)/g, '');
            const root = document.documentElement;
            const value = getComputedStyle(root).getPropertyValue(cleanVar).trim();
            return value || '#9e9e9e';
        }

        // Map node types to graph-specific CSS variables
        const nodeTypeColorMap = {
            'hub': '--graph-node-hub',
            'regular': '--graph-node-regular',
            'orphan': '--graph-node-orphan',
            'generated': '--graph-node-generated'
        };

        // Resolve colors before rendering
        graphData.nodes.forEach(node => {
            const cssVar = nodeTypeColorMap[node.type] || '--graph-node-regular';
            node.color = resolveCSSVariable(cssVar);
        });

        // Resolve link color
        const linkColor = resolveCSSVariable('--graph-link-color') || 'rgba(100, 100, 100, 0.2)';
        const linkHighlightColor = resolveCSSVariable('--graph-link-highlight') || 'rgba(255, 180, 100, 0.7)';

        // Create force simulation and PRE-COMPUTE positions before rendering
        //
        // Philosophy: Let LINKS define structure, use collision only to prevent overlap
        // - No charge/repulsion - that tears connected nodes apart
        // - Link strength scaled by connectivity - hubs pull harder, become cluster centers
        // - Collision prevents overlap without pushing clusters apart

        // Build node lookup for link strength calculation
        const nodeById = new Map(graphData.nodes.map(n => [n.id, n]));

        const simulation = d3.forceSimulation(graphData.nodes)
            .force("link", d3.forceLink(graphData.edges)
                .id(d => d.id)
                .distance(d => {
                    // Bidirectional links (weight=2) are closer
                    return d.weight === 2 ? 60 : 100;
                })
                .strength(d => {
                    // Bidirectional links pull harder
                    return d.weight === 2 ? 1.5 : 0.8;
                }))
            .force("charge", d3.forceManyBody()
                .strength(-200)       // More repulsion for spacing
                .distanceMax(400))    // Affects further nodes
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("collision", d3.forceCollide()
                .radius(d => d.size + 12)
                .strength(0.8))

        // Pre-compute 85% of layout in chunks (keeps UI responsive)
        const totalIterations = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
        const precomputeIterations = Math.floor(totalIterations * 0.85);
        const ticksPerFrame = 30;

        await new Promise((resolve) => {
            let done = 0;
            function tickChunk() {
                const batch = Math.min(ticksPerFrame, precomputeIterations - done);
                for (let i = 0; i < batch; i++) simulation.tick();
                done += batch;
                if (done < precomputeIterations) requestAnimationFrame(tickChunk);
                else resolve();
            }
            requestAnimationFrame(tickChunk);
        });

        loadingEl.classList.add('hidden');

        // Render with mostly-computed positions - links first (behind nodes)
        const link = g.append("g")
            .attr("class", "graph-links")
            .selectAll("line")
            .data(graphData.edges)
            .enter().append("line")
            .attr("class", "graph-link")
            .attr("stroke", linkColor)
            .attr("stroke-width", 1)
            .attr("stroke-linecap", "round")
            .style("opacity", 0)
            .attr("x1", d => d.source.x)
            .attr("y1", d => d.source.y)
            .attr("x2", d => d.target.x)
            .attr("y2", d => d.target.y);

        // Phase-in links with stagger (quicker - many links)
        link.transition()
            .delay((d, i) => Math.min(i * 8, 400))
            .duration(350)
            .ease(d3.easeCubicOut)
            .style("opacity", 0.6);

        // Render nodes (on top of links) - start small and invisible for phase-in
        const node = g.append("g")
            .attr("class", "graph-nodes")
            .selectAll("circle")
            .data(graphData.nodes)
            .enter().append("circle")
            .attr("class", d => `graph-node graph-node-${d.type}`)
            .attr("r", 0)
            .attr("fill", d => d.color)
            .attr("cx", d => d.x)
            .attr("cy", d => d.y)
            .style("opacity", 0)
            .call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended))
            .on("click", (event, d) => {
                window.location.href = d.url;
            })
            .on("mouseover", (event, d) => {
                showTooltip(event, d);
                highlightConnections(d);
            })
            .on("mouseout", () => {
                hideTooltip();
                clearHighlights();
            });

        // Phase-in nodes with stagger (bubbles grow in)
        const staggerMs = 18;
        const phaseDuration = 450;
        const maxStagger = 600;
        node.transition()
            .delay((d, i) => Math.min(i * staggerMs, maxStagger))
            .duration(phaseDuration)
            .ease(d3.easeElasticOut.amplitude(0.8).period(0.4))
            .attr("r", d => d.size)
            .style("opacity", 1);

        // After phase-in completes, zoom out to fit the graph
        const phaseInEndMs = maxStagger + phaseDuration + 50;
        setTimeout(() => {
            const nodes = graphData.nodes;
            if (nodes.length === 0) return;
            let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
            nodes.forEach(n => {
                const r = n.size + 12;
                minX = Math.min(minX, n.x - r);
                maxX = Math.max(maxX, n.x + r);
                minY = Math.min(minY, n.y - r);
                maxY = Math.max(maxY, n.y + r);
            });
            const dx = maxX - minX, dy = maxY - minY;
            const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
            const padding = 100;
            const scale = Math.min(0.95, 0.9 * Math.min(
                (width - 2 * padding) / Math.max(dx, 1),
                (height - 2 * padding) / Math.max(dy, 1)
            ));
            const tx = width / 2 - scale * cx;
            const ty = height / 2 - scale * cy;
            const transform = d3.zoomIdentity.translate(tx, ty).scale(scale);
            svg.transition().duration(700).ease(d3.easeCubicOut)
                .call(zoomBehavior.transform, transform);
        }, phaseInEndMs);

        // Setup tick handler for animations and dragging
        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("cx", d => d.x)
                .attr("cy", d => d.y);
        });

        // Very quick settling - barely noticeable snap
        simulation.alphaDecay(0.2).alpha(0.05).restart();

        // Stop simulation after settle to save CPU (restarts on drag)
        setTimeout(() => {
            if (simulation.alpha() < 0.01) simulation.stop();
        }, 2500);

        // Drag functions - only restart simulation when actually dragging (not on click)
        function dragstarted(event, d) {
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = event.x;
            d.fy = event.y;
        }

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

        // Tooltip functions
        const tooltip = d3.select("#tooltip");

        function showTooltip(event, d) {
            const tags = d.tags.length > 0
                ? `<div class="tags">${d.tags.map(t => `<span class="tag">${t}</span>`).join('')}</div>`
                : '';

            tooltip
                .style("display", "block")
                .style("left", (event.pageX + 10) + "px")
                .style("top", (event.pageY + 10) + "px")
                .html(`
                    <h4>${d.label}</h4>
                    <p>${d.reading_time} min read · ${d.incoming_refs} in / ${d.outgoing_refs} out</p>
                    ${tags}
                `);
        }

        function hideTooltip() {
            tooltip.style("display", "none");
        }

        // Highlight state management
        let currentHighlight = null;
        let clearTimer = null;

        function highlightConnections(d) {
            // Cancel any pending clear
            if (clearTimer) {
                clearTimeout(clearTimer);
                clearTimer = null;
            }

            // Skip if already highlighting this node
            if (currentHighlight === d.id) return;
            currentHighlight = d.id;

            // Find connected node IDs
            const connectedIds = new Set([d.id]);
            graphData.edges.forEach(e => {
                const sourceId = typeof e.source === 'object' ? e.source.id : e.source;
                const targetId = typeof e.target === 'object' ? e.target.id : e.target;
                if (sourceId === d.id) connectedIds.add(targetId);
                if (targetId === d.id) connectedIds.add(sourceId);
            });

            const query = searchInput.value.toLowerCase();
            const filter = filterType.value;
            const visibleIds = new Set(
                graphData.nodes.filter(n => isNodeVisible(n, query, filter)).map(n => n.id)
            );

            // Respect filter: connected=1, visible-but-not-connected=0.12, filtered-out=0 or 0.2
            node.transition().duration(150)
                .style("opacity", n => {
                    if (connectedIds.has(n.id)) return 1;
                    if (isNodeVisible(n, query, filter)) return 0.12;
                    return tagFilterMode ? 0 : 0.2;
                });

            // Highlight connected links; hide links between filtered-out nodes
            link.transition().duration(150)
                .style("opacity", e => {
                    const sourceId = typeof e.source === 'object' ? e.source.id : e.source;
                    const targetId = typeof e.target === 'object' ? e.target.id : e.target;
                    if (!visibleIds.has(sourceId) || !visibleIds.has(targetId)) return 0;
                    return (sourceId === d.id || targetId === d.id) ? 1 : 0.06;
                })
                .style("stroke-width", e => {
                    const sourceId = typeof e.source === 'object' ? e.source.id : e.source;
                    const targetId = typeof e.target === 'object' ? e.target.id : e.target;
                    return (sourceId === d.id || targetId === d.id) ? 2 : 1;
                });

            link.classed("highlighted", e => {
                const sourceId = typeof e.source === 'object' ? e.source.id : e.source;
                const targetId = typeof e.target === 'object' ? e.target.id : e.target;
                return sourceId === d.id || targetId === d.id;
            });
        }

        function scheduleClearHighlights() {
            // Delay clearing - gives time to move to another node
            // Restore filter state (not full graph) so search/type filter persists
            if (clearTimer) clearTimeout(clearTimer);
            clearTimer = setTimeout(() => {
                currentHighlight = null;
                updateVisibility();
                link.transition().duration(300).style("stroke-width", 1);
                link.classed("highlighted", false);
            }, 800);
        }

        function clearHighlights() {
            scheduleClearHighlights();
        }

        // Search and filter functionality
        const searchInput = document.getElementById('search');
        const filterType = document.getElementById('filter-type');

        // Tag-only mode: when ?tag=X in URL, show only nodes with that tag (hide others)
        const urlParams = new URLSearchParams(window.location.search);
        const tagParam = urlParams.get('tag');
        const tagFilterMode = !!tagParam;
        if (tagParam) {
            searchInput.value = tagParam;
            const badge = document.getElementById('tag-filter-badge');
            const valueEl = document.getElementById('tag-filter-value');
            if (badge && valueEl) {
                valueEl.textContent = tagParam;
                badge.classList.remove('hidden');
            }
        }

        function nodeMatchesTag(d) {
            if (!tagParam) return true;
            return d.tags.some(t => t.toLowerCase() === tagParam.toLowerCase());
        }

        function isNodeVisible(d, query, filter) {
            if (tagFilterMode && !nodeMatchesTag(d)) return false;
            const matchesSearch = !query ||
                d.label.toLowerCase().includes(query) ||
                d.tags.some(t => t.toLowerCase().includes(query));
            const matchesType = filter === 'all' || d.type === filter;
            return matchesSearch && matchesType;
        }

        function updateVisibility() {
            const query = searchInput.value.toLowerCase();
            const filter = filterType.value;
            const visibleIds = new Set(
                graphData.nodes.filter(n => isNodeVisible(n, query, filter)).map(n => n.id)
            );

            const nodeOpacity = (d) => {
                const visible = isNodeVisible(d, query, filter);
                return visible ? 1 : (tagFilterMode ? 0 : 0.2);
            };
            node.style("opacity", nodeOpacity);

            link.style("opacity", d => {
                const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
                const targetId = typeof d.target === 'object' ? d.target.id : d.target;
                return visibleIds.has(sourceId) && visibleIds.has(targetId) ? 0.6 : 0;
            });
        }

        function debounce(fn, ms) {
            let t = null;
            return function (...args) {
                if (t) clearTimeout(t);
                t = setTimeout(() => { t = null; fn.apply(this, args); }, ms);
            };
        }

        searchInput.addEventListener('input', debounce(updateVisibility, 150));
        filterType.addEventListener('change', updateVisibility);

        // Initial filter when arriving with ?tag=
        if (tagParam) {
            updateVisibility();
        }

        // Keyboard shortcuts
        document.addEventListener('keydown', (e) => {
            if (e.key === '/' || (e.metaKey && e.key === 'k')) {
                e.preventDefault();
                searchInput.focus();
            }
            if (e.key === 'Escape') {
                searchInput.value = '';
                filterType.value = 'all';
                searchInput.blur();
                updateVisibility();
            }
        });
        }

        loadAndRenderGraph();
    </script>
</body>
</html>
