// D3 Force Graph Implementation
window.ForceGraph = {
    instances: {},

    create: function (containerId, nodesData, linksData, options) {
        // Always clean up previous graph
        this.destroy(containerId);

        const container = document.getElementById(containerId);
        if (!container) {
            console.error('Container not found:', containerId);
            return;
        }

        // Clear existing content
        container.innerHTML = '';
        container.style.position = 'relative';

        const width = container.clientWidth || 400;
        const height = container.clientHeight || 350;
        const nodeCount = nodesData.length;

        // Adaptive forces based on node count
        const chargeStrength = nodeCount <= 5 ? -150 : nodeCount <= 12 ? -200 : nodeCount <= 30 ? -300 : -400;
        const linkDistance = nodeCount <= 5 ? 70 : nodeCount <= 12 ? 90 : nodeCount <= 30 ? 100 : 120;
        const collideRadius = nodeCount <= 8 ? 25 : nodeCount <= 20 ? 30 : 35;

        // Detect center node
        const centerId = (options && options.centerId) || (nodesData.length > 0 ? nodesData[0].id : null);
        const onNodeClick = options && options.onNodeClick;

        // Store instance state
        const instance = { simulation: null, svg: null, tooltip: null };
        this.instances[containerId] = instance;

        // Create SVG
        instance.svg = d3.select(`#${containerId}`)
            .append('svg')
            .attr('width', width)
            .attr('height', height)
            .attr('viewBox', [0, 0, width, height]);

        // Create arrow markers for each link type
        const defs = instance.svg.append('defs');
        const markerColors = { hard: '#ef4444', strong: '#f59e0b', soft: '#999', default: '#999' };

        Object.entries(markerColors).forEach(([type, color]) => {
            defs.append('marker')
                .attr('id', `arrowhead-${containerId}-${type}`)
                .attr('viewBox', '0 -5 10 10')
                .attr('refX', 20)
                .attr('refY', 0)
                .attr('markerWidth', 6)
                .attr('markerHeight', 6)
                .attr('orient', 'auto')
                .append('path')
                .attr('d', 'M0,-5L10,0L0,5')
                .attr('fill', color);
        });

        // Create zoomable group
        const zoomGroup = instance.svg.append('g').attr('class', 'zoom-group');

        // Create force simulation
        instance.simulation = d3.forceSimulation(nodesData)
            .force('link', d3.forceLink(linksData).id(d => d.id).distance(linkDistance))
            .force('charge', d3.forceManyBody().strength(chargeStrength))
            .force('center', d3.forceCenter(width / 2, height / 2))
            .force('collide', d3.forceCollide(collideRadius));

        // Link color by type
        function getLinkColor(d) {
            const lt = (d.linkType || '').toLowerCase();
            return markerColors[lt] || markerColors.default;
        }

        // Link opacity scales with max depth of endpoints
        function getLinkOpacity(d) {
            const srcDepth = (typeof d.source === 'object' ? d.source.depth : 0) || 0;
            const tgtDepth = (typeof d.target === 'object' ? d.target.depth : 0) || 0;
            const maxDepth = Math.max(srcDepth, tgtDepth);
            return Math.max(0.25, 0.7 - maxDepth * 0.15);
        }

        // Create links
        const self = this;
        const link = zoomGroup.append('g')
            .attr('class', 'links')
            .selectAll('line')
            .data(linksData)
            .join('line')
            .attr('stroke', getLinkColor)
            .attr('stroke-opacity', getLinkOpacity)
            .attr('stroke-width', d => Math.max(1, Math.sqrt(d.weight || 1) * 1.5))
            .attr('marker-end', d => {
                const lt = (d.linkType || '').toLowerCase();
                const type = markerColors[lt] ? lt : 'default';
                return `url(#arrowhead-${containerId}-${type})`;
            })
            .style('cursor', 'help')
            .on('mouseenter', function (event, d) {
                self.showEdgeTooltip(containerId, d, event);
            })
            .on('mouseleave', function () {
                self.hideEdgeTooltip(containerId);
            });

        // Node sizing by depth
        function getNodeRadius(d) {
            if (d.id === centerId) return 14;
            const depth = d.depth || 0;
            if (depth <= 1) return 10;
            return 7;
        }

        // Node opacity by depth
        function getNodeOpacity(d) {
            const depth = d.depth || 0;
            return Math.max(0.45, 1.0 - depth * 0.15);
        }

        // Node stroke by depth
        function getNodeStroke(d) {
            if (d.id === centerId) return '#f59e0b';
            return '#fff';
        }

        function getNodeStrokeWidth(d) {
            if (d.id === centerId) return 3;
            const depth = d.depth || 0;
            if (depth <= 1) return 1.5;
            return 1;
        }

        // Create nodes
        const node = zoomGroup.append('g')
            .attr('class', 'nodes')
            .selectAll('g')
            .data(nodesData)
            .join('g')
            .call(this.drag(instance.simulation));

        // Add circles with depth-based styling
        node.append('circle')
            .attr('r', getNodeRadius)
            .attr('fill', d => this.getNodeColor(d.category))
            .attr('fill-opacity', getNodeOpacity)
            .attr('stroke', getNodeStroke)
            .attr('stroke-width', getNodeStrokeWidth)
            .style('cursor', 'pointer');

        // Add labels
        node.append('text')
            .text(d => this.truncateLabel(d.label || d.id, 20))
            .attr('x', d => (d.id === centerId ? 16 : 13))
            .attr('y', 4)
            .attr('font-size', d => d.id === centerId ? '12px' : '10px')
            .attr('font-weight', d => d.id === centerId ? 'bold' : 'normal')
            .attr('fill', '#555')
            .attr('fill-opacity', getNodeOpacity);

        // Click handler for rich tooltip (reuses the `self` capture above).
        node.on('click', function (event, d) {
            event.stopPropagation();
            self.showTooltip(containerId, d, event, onNodeClick);
        });

        // Clicking SVG background dismisses tooltip
        instance.svg.on('click', function () {
            self.hideTooltip(containerId);
        });

        // Update positions on simulation tick
        instance.simulation.on('tick', () => {
            nodesData.forEach(d => {
                d.x = Math.max(15, Math.min(width - 15, d.x));
                d.y = Math.max(15, Math.min(height - 15, d.y));
            });

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

        // Add zoom behavior
        const zoom = d3.zoom()
            .scaleExtent([0.3, 5])
            .on('zoom', (event) => {
                zoomGroup.attr('transform', event.transform);
            });

        instance.svg.call(zoom);

        return { nodeCount: nodesData.length, linkCount: linksData.length };
    },

    showTooltip: function (containerId, d, event, onNodeClick) {
        this.hideTooltip(containerId);

        const container = document.getElementById(containerId);
        if (!container) return;

        const tooltip = document.createElement('div');
        tooltip.className = 'graph-tooltip';

        // Title
        const title = document.createElement('div');
        title.style.fontWeight = 'bold';
        title.style.marginBottom = '6px';
        title.textContent = d.label || d.id;
        tooltip.appendChild(title);

        // Category chip
        if (d.category) {
            const cat = document.createElement('span');
            cat.style.display = 'inline-block';
            cat.style.padding = '2px 8px';
            cat.style.borderRadius = '10px';
            cat.style.fontSize = '11px';
            cat.style.marginBottom = '6px';
            cat.style.color = '#fff';
            cat.style.backgroundColor = this.getNodeColor(d.category);
            cat.textContent = d.category;
            tooltip.appendChild(cat);
        }

        // Tags
        if (d.tags && d.tags.length > 0) {
            const tagsDiv = document.createElement('div');
            tagsDiv.className = 'graph-tooltip-meta';
            tagsDiv.style.marginTop = '4px';
            tagsDiv.textContent = 'Tags: ' + d.tags.slice(0, 5).join(', ');
            tooltip.appendChild(tagsDiv);
        }

        // Connections
        if (d.connections != null) {
            const conn = document.createElement('div');
            conn.className = 'graph-tooltip-meta';
            conn.style.marginTop = '2px';
            conn.textContent = 'Connections: ' + d.connections;
            tooltip.appendChild(conn);
        }

        // Drill link — always offered when onNodeClick is wired, including
        // for the center node so users have a stable affordance to "open
        // this node's drill view" instead of guessing via a separate UI.
        if (onNodeClick) {
            const drillLink = document.createElement('div');
            drillLink.style.marginTop = '8px';
            drillLink.style.borderTop = '1px solid var(--mud-palette-lines-default)';
            drillLink.style.paddingTop = '6px';
            const a = document.createElement('a');
            a.href = '#';
            a.className = 'graph-tooltip-drill';
            a.textContent = 'Drill into this node →';
            a.onclick = function (e) {
                e.preventDefault();
                onNodeClick.invokeMethodAsync('InvokeCallback', d.id);
            };
            drillLink.appendChild(a);
            tooltip.appendChild(drillLink);
        }

        // Position relative to container
        const rect = container.getBoundingClientRect();
        const x = event.clientX - rect.left + 10;
        const y = event.clientY - rect.top - 10;
        tooltip.style.left = Math.min(x, rect.width - 310) + 'px';
        tooltip.style.top = Math.min(y, rect.height - 100) + 'px';

        container.appendChild(tooltip);
        if (this.instances[containerId]) {
            this.instances[containerId].tooltip = tooltip;
        }
    },

    hideTooltip: function (containerId) {
        const inst = this.instances[containerId];
        if (inst && inst.tooltip) {
            inst.tooltip.remove();
            inst.tooltip = null;
        }
    },

    /**
     * Lightweight hover tooltip for edges — exposes LinkType, Confidence,
     * Relationship, SharedTags, Distance. Distinct from `showTooltip` (the
     * rich node click panel) so node interactions remain unaffected.
     * Builds nodes via createElement+textContent so server-supplied strings
     * never reach innerHTML.
     */
    showEdgeTooltip: function (containerId, d, event) {
        const container = document.getElementById(containerId);
        if (!container) return;
        // Drop any prior edge tooltip; node tooltip lives on a separate slot.
        this.hideEdgeTooltip(containerId);

        const tt = document.createElement('div');
        tt.className = 'graph-edge-tooltip';

        const addLine = (label, value) => {
            if (value === undefined || value === null || value === '') return;
            const row = document.createElement('div');
            if (label) {
                const k = document.createElement('strong');
                k.textContent = label + ': ';
                row.appendChild(k);
            }
            row.appendChild(document.createTextNode(String(value)));
            tt.appendChild(row);
        };

        const linkType = (d.linkType || '').toLowerCase();
        const heading = document.createElement('div');
        const headStrong = document.createElement('strong');
        headStrong.textContent = linkType ? (linkType[0].toUpperCase() + linkType.slice(1)) : 'Link';
        heading.appendChild(headStrong);
        const confidence = typeof d.weight === 'number' ? ' · conf ' + d.weight.toFixed(2) : '';
        heading.appendChild(document.createTextNode(confidence));
        tt.appendChild(heading);

        addLine('Rel', d.relationship);
        addLine('Tags', d.sharedTags);
        if (typeof d.distance === 'number') addLine('Dist', d.distance.toFixed(3));

        const rect = container.getBoundingClientRect();
        const x = event.clientX - rect.left + 10;
        const y = event.clientY - rect.top + 10;
        tt.style.left = Math.min(x, rect.width - 200) + 'px';
        tt.style.top = Math.min(y, rect.height - 80) + 'px';

        container.appendChild(tt);
        if (this.instances[containerId]) {
            this.instances[containerId].edgeTooltip = tt;
        }
    },

    hideEdgeTooltip: function (containerId) {
        const inst = this.instances[containerId];
        if (inst && inst.edgeTooltip) {
            inst.edgeTooltip.remove();
            inst.edgeTooltip = null;
        }
    },

    drag: function (simulation) {
        function dragstarted(event) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            event.subject.fx = event.subject.x;
            event.subject.fy = event.subject.y;
        }

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

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

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

    getNodeColor: function (category) {
        const colors = {
            'experience': '#5b21b6',
            'focus': '#ec4899',
            'continuum': '#10b981',
            'plugin': '#f59e0b',
            'default': '#6366f1'
        };
        return colors[category] || colors.default;
    },

    truncateLabel: function (text, maxLen) {
        if (!text) return '';
        return text.length > maxLen ? text.substring(0, maxLen - 1) + '\u2026' : text;
    },

    destroy: function (containerId) {
        const inst = this.instances[containerId];
        if (inst) {
            if (inst.simulation) {
                inst.simulation.stop();
            }
            if (inst.tooltip) {
                inst.tooltip.remove();
            }
            if (inst.edgeTooltip) {
                inst.edgeTooltip.remove();
            }
            delete this.instances[containerId];
        }
        const container = document.getElementById(containerId);
        if (container) {
            container.innerHTML = '';
        }
    },

    resize: function (containerId) {
        const container = document.getElementById(containerId);
        const inst = this.instances[containerId];
        if (container && inst && inst.svg) {
            const width = container.clientWidth;
            const height = container.clientHeight || 350;
            inst.svg.attr('width', width).attr('height', height);
            inst.svg.attr('viewBox', [0, 0, width, height]);
        }
    }
};

// Include D3.js library if not already loaded
if (typeof d3 === 'undefined') {
    const script = document.createElement('script');
    script.src = 'https://d3js.org/d3.v7.min.js';
    document.head.appendChild(script);
}
