import * as d3 from 'https://esm.sh/d3@7';

const getDisplayLabel = d => d.label.length > 20 ? d.label.substring(0, 17) + "..." : d.label;
const getPillWidth = d => getDisplayLabel(d).length * 8 + 30;

const getNodeType = d => {
    if (d) {
        if (d.type) {
            const t = d.type.toLowerCase();
            if (t === 'definition') return 'definition';
            if (t === 'table') return 'table';
            if (t === 'rule') return 'rule';
            if (t === 'section') return 'section';
        }
        if (d.group) {
            const g = d.group.toLowerCase();
            if (g === 'definition') return 'definition';
            if (g === 'table') return 'table';
            if (g === 'rule') return 'rule';
            if (g === 'section') return 'section';
        }
    }
    return null;
};

const getNodeGroup = d => {
    if (d && d.group) {
        const g = d.group.toLowerCase();
        if (g === 'bridge') return 'bridge';
        if (g === 'current') return 'current';
        if (g === 'concept') return 'concept';
    }
    return 'concept'; // fallback
};

const getCategoryStyle = d => {
    const type = getNodeType(d);
    const group = getNodeGroup(d);

    // 1. Rule (red/coral)
    if (type === 'rule') {
        return {
            color: '#ff4646',
            fill: 'rgba(255, 70, 70, 0.1)',
            opacity: 0.8,
            glowKey: 'rule',
            textColor: '#ff8b8b'
        };
    }
    // 2. Definition (gold/amber)
    if (type === 'definition') {
        return {
            color: '#ffb03a',
            fill: 'rgba(255, 176, 58, 0.1)',
            opacity: 0.8,
            glowKey: 'definition',
            textColor: '#ffd18c'
        };
    }
    // 3. Table (purple/magenta)
    if (type === 'table') {
        return {
            color: '#d946ef',
            fill: 'rgba(217, 70, 239, 0.1)',
            opacity: 0.8,
            glowKey: 'table',
            textColor: '#f5d0fe'
        };
    }
    // 4. Section (blue/indigo)
    if (type === 'section') {
        return {
            color: '#3b82f6',
            fill: 'rgba(59, 130, 246, 0.1)',
            opacity: 0.8,
            glowKey: 'section',
            textColor: '#93c5fd'
        };
    }
    // 5. Bridge (cyan/comparison)
    if (group === 'bridge') {
        return {
            color: '#06b6d4',
            fill: 'rgba(6, 182, 212, 0.1)',
            opacity: 0.7,
            glowKey: 'bridge',
            textColor: '#67e8f9'
        };
    }
    // 6. Current (active/focus landmark - neon green)
    if (group === 'current') {
        return {
            color: 'var(--nexus-neon)',
            fill: 'rgba(0, 255, 153, 0.15)',
            opacity: 0.9,
            glowKey: 'current',
            textColor: '#ffffff'
        };
    }
    // 7. Concept / Default (subtle cool steel blue/teal)
    return {
        color: '#00d2c4',
        fill: 'rgba(0, 210, 196, 0.05)',
        opacity: 0.4,
        glowKey: 'concept',
        textColor: '#e0e0e0'
    };
};

let simulation;
let zoomBehavior;
let svgElement;

let node, link, rootGroup, badge, width, height, currentDotNetHelper, resizeObserver;

let isMobileMode = false;
let activeNodeId = null;

const getNodeGlyph = d => {
    if (!d) return 'C';
    const type = getNodeType(d);
    const group = getNodeGroup(d);
    if (type === 'rule') return '§';
    if (type === 'definition') return 'D';
    if (type === 'table') return 'T';
    if (type === 'section') return 'S';
    if (group === 'bridge') return 'B';
    if (group === 'current') return '★';
    return 'C';
};

function updateNodeAppearances() {
    if (!node) return;
    
    node.each(function(d) {
        const g = d3.select(this);
        const rect = g.select(".node-pill");
        const text = g.select("text");
        
        const isCurrent = getNodeGroup(d) === 'current';
        const isSelected = activeNodeId && d.id === activeNodeId;
        const showFull = !isMobileMode || isSelected || isCurrent;
        
        if (showFull) {
            rect.transition().duration(250)
                .attr("x", -getPillWidth(d) / 2)
                .attr("width", getPillWidth(d))
                .attr("height", 30)
                .attr("rx", 15)
                .attr("y", -15);
                
            text.text(getDisplayLabel(d))
                .attr("font-size", isCurrent || isSelected ? "0.85rem" : "0.8rem")
                .attr("font-weight", isCurrent || isSelected ? "600" : "normal");
        } else {
            rect.transition().duration(250)
                .attr("x", -15)
                .attr("width", 30)
                .attr("height", 30)
                .attr("rx", 15)
                .attr("y", -15);
                
            text.text(getNodeGlyph(d))
                .attr("font-size", "0.9rem")
                .attr("font-weight", "bold");
        }
    });
}

export function setMobileMode(isMobile) {
    isMobileMode = isMobile;
    if (!simulation) return;
    
    if (isMobile) {
        simulation.force("charge", d3.forceManyBody().strength(-60));
        simulation.force("link").distance(180);
        simulation.force("collide", d3.forceCollide().radius(d => {
            const isCurrent = getNodeGroup(d) === 'current';
            const isSelected = activeNodeId && d.id === activeNodeId;
            if (isCurrent || isSelected) {
                return (getPillWidth(d) / 2) + 15;
            }
            return 20;
        }));
    } else {
        simulation.force("charge", d3.forceManyBody().strength(-400));
        simulation.force("link").distance(120);
        simulation.force("collide", d3.forceCollide().radius(d => (getPillWidth(d) / 2) + 20));
    }
    
    updateNodeAppearances();
    simulation.alpha(0.3).restart();
}

export function mount(containerId, data, dotNetHelper) {
    const container = document.getElementById(containerId);
    if (!container) return;

    currentDotNetHelper = dotNetHelper;
    width = container.clientWidth || 400;
    height = container.clientHeight || 400;

    // Create SVG
    svgElement = d3.select(container).append("svg")
        .attr("viewBox", [0, 0, width, height])
        .attr("width", "100%")
        .attr("height", "100%")
        .style("background", "radial-gradient(circle, #1a1a1a 0%, #121212 100%)");

    // Radial gradients for Nebula effects
    const defs = svgElement.append("defs");
    
    // Fallback radial gradient for legacy nebulaGlow
    const radialGradient = defs.append("radialGradient")
        .attr("id", "nebulaGlow")
        .attr("cx", "50%")
        .attr("cy", "50%")
        .attr("r", "50%");
    radialGradient.append("stop").attr("offset", "0%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 1);
    radialGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 0);

    const colors = {
        'rule': '#ff4646',
        'definition': '#ffb03a',
        'table': '#d946ef',
        'section': '#3b82f6',
        'bridge': '#06b6d4',
        'current': 'var(--nexus-neon)',
        'concept': '#00d2c4'
    };

    Object.entries(colors).forEach(([key, color]) => {
        const radGrad = defs.append("radialGradient")
            .attr("id", `nebulaGlow-${key}`)
            .attr("cx", "50%")
            .attr("cy", "50%")
            .attr("r", "50%");
        radGrad.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 1);
        radGrad.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0);
    });

    // Root Group for Zoom
    rootGroup = svgElement.append("g").attr("class", "zoom-containment");

    // Container groups for links and nodes to keep order (links below nodes)
    rootGroup.append("g").attr("class", "links-layer");
    rootGroup.append("g").attr("class", "nodes-layer");

    // Badge Element (TU JESTEŚ)
    badge = rootGroup.append("g")
        .attr("class", "active-badge")
        .style("display", "none")
        .style("pointer-events", "none");

    badge.append("rect")
        .attr("x", -35)
        .attr("y", -35)
        .attr("width", 70)
        .attr("height", 20)
        .attr("rx", 10)
        .attr("fill", "var(--nexus-neon)");

    badge.append("text")
        .text("TU JESTEŚ")
        .attr("text-anchor", "middle")
        .attr("y", -21)
        .attr("fill", "#000")
        .attr("font-weight", "bold")
        .attr("font-size", "0.6rem");

    // Attach Zoom Behavior
    zoomBehavior = d3.zoom()
        .scaleExtent([0.3, 4])
        .on("zoom", (e) => rootGroup.attr("transform", e.transform));
    
    svgElement.call(zoomBehavior).on("wheel.zoom", null);

    // Use ResizeObserver for more reliable container size tracking
    resizeObserver = new ResizeObserver(entries => {
        for (let entry of entries) {
            handleResize(containerId);
        }
    });
    resizeObserver.observe(container);

    isMobileMode = window.innerWidth < 768;

    simulation = d3.forceSimulation()
        .force("link", d3.forceLink().id(d => d.id).distance(isMobileMode ? 180 : 120))
        .force("charge", d3.forceManyBody().strength(isMobileMode ? -60 : -400))
        .force("center", d3.forceCenter(width / 2, height / 2))
        .force("collide", d3.forceCollide().radius(d => {
            if (isMobileMode) {
                const isCurrent = getNodeGroup(d) === 'current';
                const isSelected = activeNodeId && d.id === activeNodeId;
                if (isCurrent || isSelected) return (getPillWidth(d) / 2) + 15;
                return 20;
            }
            return (getPillWidth(d) / 2) + 20;
        }));

    simulation.on("tick", () => {
        if (link) {
            link.attr("d", d => {
                const dx = d.target.x - d.source.x;
                const dy = d.target.y - d.source.y;
                const dr = Math.sqrt(dx * dx + dy * dy);
                return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
            });
        }

        if (node) {
            node.attr("transform", d => {
                if (d.x === undefined || isNaN(d.x) || !isFinite(d.x)) d.x = width / 2;
                if (d.y === undefined || isNaN(d.y) || !isFinite(d.y)) d.y = height / 2;
                // Keep within bounds with padding
                const pillWidth = getPillWidth(d);
                const halfWidth = pillWidth / 2;
                d.x = Math.max(halfWidth + 20, Math.min(width - halfWidth - 20, d.x));
                d.y = Math.max(35, Math.min(height - 35, d.y));
                return `translate(${d.x},${d.y})`;
            });
        }
        
        if (badge && badge.style("display") !== "none") {
            const activeData = badge.datum();
            if (activeData) {
                badge.attr("transform", `translate(${activeData.x},${activeData.y})`);
            }
        }
    });

    updateData(data);
}

export function updateData(data) {
    if (!simulation || !rootGroup) return;
    if (!data || !data.nodes) {
        clear();
        return;
    }

    // Keep existing node positions if they match by ID
    const oldNodes = new Map(simulation.nodes().map(d => [d.id, d]));
    data.nodes.forEach(d => {
        if (d.x !== undefined && (!isFinite(d.x) || isNaN(d.x))) d.x = undefined;
        if (d.y !== undefined && (!isFinite(d.y) || isNaN(d.y))) d.y = undefined;
        if (oldNodes.has(d.id)) {
            const old = oldNodes.get(d.id);
            if (old.x !== undefined && isFinite(old.x) && !isNaN(old.x)) d.x = old.x;
            if (old.y !== undefined && isFinite(old.y) && !isNaN(old.y)) d.y = old.y;
            d.vx = old.vx;
            d.vy = old.vy;
        }
    });

    // Sanitize links to filter out any references to non-existent nodes
    const nodeIds = new Set(data.nodes.map(n => n.id));
    const validLinks = (data.links || []).filter(l => {
        const srcId = typeof l.source === 'object' ? l.source.id : l.source;
        const tgtId = typeof l.target === 'object' ? l.target.id : l.target;
        return nodeIds.has(srcId) && nodeIds.has(tgtId);
    });

    // Update Links
    link = rootGroup.select(".links-layer")
        .selectAll("path")
        .data(validLinks, d => {
            const srcId = typeof d.source === 'object' ? d.source.id : d.source;
            const tgtId = typeof d.target === 'object' ? d.target.id : d.target;
            return srcId + "-" + tgtId + "-" + d.type;
        })
        .join(
            enter => enter.append("path")
                .attr("stroke", d => {
                    if (d.type === 'Defines' || d.type === 'maps_to') return 'var(--nexus-accent, #00ffaa)';
                    if (d.type === 'Next' || d.type === 'relates_to') return 'rgba(255,255,255,0.2)';
                    if (d.type === 'Contains' || d.type === 'contains') return 'var(--nexus-neon)';
                    return 'rgba(255,255,255,0.1)';
                })
                .attr("fill", "none")
                .attr("stroke-width", d => (d.type === 'Defines' || d.type === 'maps_to') ? 2 : 1)
                .attr("stroke-dasharray", d => d.type === 'References' ? "5,5" : "0")
                .style("opacity", 0)
                .call(enter => enter.transition().duration(500).style("opacity", 1)),
            update => update,
            exit => exit.transition().duration(500).style("opacity", 0).remove()
        );

    // Update Nodes
    node = rootGroup.select(".nodes-layer")
        .selectAll("g.node-group")
        .data(data.nodes, d => d.id)
        .join(
            enter => {
                const g = enter.append("g")
                    .attr("class", "node-group neon-flash-node")
                    .style("cursor", "pointer")
                    .style("opacity", 0)
                    .on("click", (e, d) => {
                        currentDotNetHelper.invokeMethodAsync('OnNodeClicked', d.id);
                        setActiveNode(d.id);
                    })
                    .call(drag(simulation));

                g.append("circle")
                    .attr("r", 30)
                    .attr("fill", d => `url(#nebulaGlow-${getCategoryStyle(d).glowKey})`)
                    .attr("opacity", d => getCategoryStyle(d).opacity);

                g.append("rect")
                    .attr("class", "node-pill")
                    .attr("fill", "rgba(20, 20, 20, 0.95)")
                    .attr("stroke", d => getCategoryStyle(d).color)
                    .attr("stroke-width", d => getNodeGroup(d) === 'current' ? 2 : 1.2);

                g.append("text")
                    .attr("text-anchor", "middle")
                    .attr("y", 5)
                    .attr("fill", d => getCategoryStyle(d).textColor);

                g.append("title")
                    .text(d => d.description ? `${d.label}\n\n${d.description}` : d.label);
                
                g.transition().duration(500).style("opacity", 1);
                
                return g;
            },
            update => update.classed("neon-flash-node", false),
            exit => exit.transition().duration(500).style("opacity", 0).remove()
        );

    updateNodeAppearances();

    simulation.nodes(data.nodes);
    simulation.force("link").links(validLinks);
    simulation.alpha(0.5).restart();

    // Trigger zoom to fit after a short delay to allow simulation to settle
    setTimeout(zoomToFit, 100);
}

function drag(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);
}

export function setActiveNode(nodeId) {
    if (!svgElement || !node) return;
    
    activeNodeId = nodeId;
    // Safety check: ensure we only target the first occurrence if IDs are duplicated
    const targetNode = node.filter(d => d.id === nodeId);
    if (targetNode.empty()) {
        dimNodes(null);
        badge.style("display", "none");
        return;
    }

    const firstMatch = targetNode.filter((d, i) => i === 0);
    const d = firstMatch.datum();
    if (!d || d.x === undefined || d.y === undefined || isNaN(d.x) || !isFinite(d.x) || isNaN(d.y) || !isFinite(d.y)) return;
    
    // Reset all active classes
    rootGroup.selectAll(".node-pill").classed("nexus-node-active", false);
    firstMatch.select(".node-pill").classed("nexus-node-active", true);

    // Position badge
    badge.style("display", "block").datum(d);
    badge.attr("transform", `translate(${d.x},${d.y})`);

    // Dim others (only exact matches for nodeId will be fully opaque)
    dimNodes(nodeId);

    // Dynamic collision update if in mobile mode to expand active node
    if (isMobileMode && simulation) {
        simulation.force("collide", d3.forceCollide().radius(d => {
            const isCurrent = getNodeGroup(d) === 'current';
            const isSelected = activeNodeId && d.id === activeNodeId;
            if (isCurrent || isSelected) {
                return (getPillWidth(d) / 2) + 15;
            }
            return 20;
        }));
    }
    
    updateNodeAppearances();

    // Smooth transition to the first matching node
    svgElement.transition().duration(1000).call(
        zoomBehavior.transform,
        d3.zoomIdentity.translate(width / 2, height / 2).scale(1.2).translate(-d.x, -d.y)
    );
}

export function dimNodes(activeNodeId) {
    if (!node) return;
    
    node.transition().duration(500)
        .style("opacity", d => (activeNodeId === null || d.id === activeNodeId) ? 1 : 0.4);
    
    if (link) {
        link.transition().duration(500)
            .style("opacity", d => {
                if (activeNodeId === null) return 1;
                // Check if this link is connected to the active node
                const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
                const targetId = typeof d.target === 'object' ? d.target.id : d.target;
                return (sourceId === activeNodeId || targetId === activeNodeId) ? 1 : 0.1;
            });
    }
}

export function unmount(containerId) {
    if (simulation) {
        simulation.stop();
    }
    if (resizeObserver) {
        resizeObserver.disconnect();
    }
    const container = document.getElementById(containerId);
    if (container) {
        container.innerHTML = ''; // clear svg
    }
}

export function handleResize(containerId) {
    const container = document.getElementById(containerId);
    if (!container || !svgElement || !simulation) return;

    const newWidth = container.clientWidth;
    const newHeight = container.clientHeight;

    // If container is hidden (size is 0), skip resize to avoid collapsing coordinates to (0,0) or NaN
    if (newWidth <= 0 || newHeight <= 0) return;

    width = newWidth;
    height = newHeight;

    svgElement.attr("viewBox", [0, 0, width, height]);
    simulation.force("center", d3.forceCenter(width / 2, height / 2));
    
    const prevMobileMode = isMobileMode;
    isMobileMode = window.innerWidth < 768;
    if (isMobileMode !== prevMobileMode) {
        setMobileMode(isMobileMode);
    } else {
        simulation.alpha(0.3).restart();
    }
}

export function scrollToNode(id) {
    const el = document.getElementById(id);
    if (el) {
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
}

export function pause() {
    if (simulation) {
        simulation.stop();
    }
}

export function zoomIn() {
    if (svgElement && zoomBehavior) {
        svgElement.transition().duration(300).call(zoomBehavior.scaleBy, 1.3);
    }
}

export function zoomOut() {
    if (svgElement && zoomBehavior) {
        svgElement.transition().duration(300).call(zoomBehavior.scaleBy, 0.7);
    }
}

export function zoomReset() {
    zoomToFit();
}

export function zoomToFit() {
    if (!node || node.empty() || !svgElement || !zoomBehavior) return;
    if (width <= 0 || height <= 0 || isNaN(width) || isNaN(height)) return;

    // Get the actual bounding box of the nodes
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    node.each(d => {
        if (d && d.x !== undefined && d.y !== undefined && isFinite(d.x) && isFinite(d.y)) {
            const pw = getPillWidth(d) / 2;
            minX = Math.min(minX, d.x - pw);
            maxX = Math.max(maxX, d.x + pw);
            minY = Math.min(minY, d.y - 15);
            maxY = Math.max(maxY, d.y + 15);
        }
    });

    if (minX === Infinity || maxX === minX || maxY === minY) return;

    const graphWidth = maxX - minX;
    const graphHeight = maxY - minY;
    if (graphWidth <= 0 || graphHeight <= 0 || isNaN(graphWidth) || isNaN(graphHeight)) return;

    const midX = (minX + maxX) / 2;
    const midY = (minY + maxY) / 2;

    const padding = 60;
    const scale = Math.min(
        (width - padding) / graphWidth,
        (height - padding) / graphHeight,
        1.2 // Max scale
    );

    if (isNaN(scale) || !isFinite(scale) || scale <= 0) return;

    svgElement.transition().duration(750).call(
        zoomBehavior.transform,
        d3.zoomIdentity
            .translate(width / 2, height / 2)
            .scale(scale)
            .translate(-midX, -midY)
    );
}

export function clear() {
    if (!rootGroup) return;
    try {
        rootGroup.select(".links-layer").selectAll("path").remove();
        rootGroup.select(".nodes-layer").selectAll("g.node-group").remove();
        if (badge) badge.style("display", "none");
        if (simulation) {
            simulation.stop();
            const linkForce = simulation.force("link");
            if (linkForce) {
                linkForce.links([]);
            }
            simulation.nodes([]);
        }

        // Reset selections
        link = null;
        node = null;

        // Reset D3 zoom transform to clean identity state
        if (svgElement && zoomBehavior) {
            svgElement.call(zoomBehavior.transform, d3.zoomIdentity);
        }
    } catch (e) {
        console.warn("Failed to clear force simulation safely:", e);
    }
}
