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

    <script>{{D3_MIN}}</script>
    <script>{{D3_SANKY}}</script>

    <style>{{MCP_APP_BASE_CSS}}</style>
    <style>
        body { padding: 16px; }

        .node rect {
            cursor: pointer;
            stroke-width: 0;
        }

        .node text {
            pointer-events: none;
            font-size: var(--font-text-xs-size);
            font-weight: var(--font-weight-medium);
            fill: var(--color-text-primary);
        }

        .link {
            fill: none;
            stroke-opacity: 0.35;
            cursor: pointer;
            transition: stroke-opacity 0.15s ease;
        }

        .link:hover {
            stroke-opacity: 0.6;
        }
    </style>
</head>
<body>
    <div class="av-loading" id="loadingIndicator">Waiting for data…</div>
    <svg id="sankey" style="width:100%;"></svg>
    <div class="av-tooltip" id="tooltip"></div>

    <script>{{MCP_APP_BRIDGE}}</script>
    <script>
    (function () {
        "use strict";

        var svg, sankey, tooltip;
        var margin = { top: 16, right: 16, bottom: 16, left: 16 };

        function getWidth() {
            return document.body.clientWidth - 32 || 500;
        }

        function getHeight() {
            if (McpAppBridge.displayMode === "fullscreen" || McpAppBridge.displayMode === "pip") {
                return window.innerHeight - 64;
            }
            return 420;
        }

        var palette = [
            "#5c98f9", "#f97066", "#47c9a2", "#f0a03c",
            "#9b8afb", "#e87fa0", "#3cc0e0", "#8bba5a",
        ];

        function nodeColor(d) {
            return palette[Math.abs(hashCode(d.name)) % palette.length];
        }

        function hashCode(str) {
            var h = 0;
            for (var i = 0; i < str.length; i++) {
                h = ((h << 5) - h + str.charCodeAt(i)) | 0;
            }
            return h;
        }

        function initializeSVG() {
            var width = getWidth();
            var height = getHeight();
            svg = d3.select("#sankey").attr("width", width).attr("height", height);
            svg.selectAll("*").remove();
            sankey = d3.sankey().nodeWidth(18).nodePadding(12)
                .extent([[margin.left, margin.top], [width - margin.right, height - margin.bottom]]);
            tooltip = d3.select("#tooltip");
        }

        function prepareGraph(data) {
            var nodes = data.nodes.map(function (d) { return Object.assign({}, d); });
            var links = [];
            var nameToIdx = new Map();
            nodes.forEach(function (n, i) { nameToIdx.set(n.name, i); });

            // Filter out links with unknown nodes or invalid values
            data.links.forEach(function (link) {
                if (nameToIdx.has(link.source) && nameToIdx.has(link.target) && link.value > 0) {
                    links.push({ source: link.source, target: link.target, value: link.value });
                }
            });

            // Detect and break cycles by creating synthetic return nodes.
            // d3-sankey doesn't support circular links so we redirect back-edges
            // to a new "Target → Source" node to preserve the data visually.
            var adj = new Map();
            links.forEach(function (l) {
                if (!adj.has(l.source)) adj.set(l.source, []);
                adj.get(l.source).push(l.target);
            });

            var WHITE = 0, GRAY = 1, BLACK = 2;
            var color = new Map();
            nodes.forEach(function (n) { color.set(n.name, WHITE); });
            var backEdges = new Set();

            function dfs(u) {
                color.set(u, GRAY);
                (adj.get(u) || []).forEach(function (v) {
                    if (color.get(v) === GRAY) {
                        backEdges.add(u + "\0" + v);
                    } else if (color.get(v) === WHITE) {
                        dfs(v);
                    }
                });
                color.set(u, BLACK);
            }
            nodes.forEach(function (n) {
                if (color.get(n.name) === WHITE) dfs(n.name);
            });

            if (backEdges.size > 0) {
                var fixedLinks = [];
                links.forEach(function (l) {
                    var key = l.source + "\0" + l.target;
                    if (backEdges.has(key)) {
                        var syntheticName = l.target + " ↩";
                        if (!nameToIdx.has(syntheticName)) {
                            nameToIdx.set(syntheticName, nodes.length);
                            nodes.push({ name: syntheticName, category: "cycle" });
                        }
                        fixedLinks.push({ source: l.source, target: syntheticName, value: l.value });
                    } else {
                        fixedLinks.push(l);
                    }
                });
                links = fixedLinks;
            }

            // Rebuild index map after possible new nodes
            var idxMap = new Map();
            nodes.forEach(function (n, i) { idxMap.set(n.name, i); });

            var processedLinks = links.map(function (l) {
                return { source: idxMap.get(l.source), target: idxMap.get(l.target), value: l.value };
            });

            return { nodes: nodes, links: processedLinks };
        }

        function drawSankey(data) {
            var prepared = prepareGraph(data);

            var graph = sankey({
                nodes: prepared.nodes,
                links: prepared.links,
            });

            svg.append("g").selectAll("path").data(graph.links).enter().append("path")
                .attr("class", "link")
                .attr("d", d3.sankeyLinkHorizontal())
                .attr("stroke", function (d) { return nodeColor(d.source); })
                .attr("stroke-width", function (d) { return Math.max(1, d.width); })
                .on("mouseover", function (event, d) {
                    tooltip.style("opacity", 1)
                        .html("<strong>" + d.source.name + " → " + d.target.name + "</strong><br/>" + d.value.toLocaleString());
                    McpAppBridge.positionTooltip(tooltip, event);
                })
                .on("mouseout", function () { tooltip.style("opacity", 0); });

            var node = svg.append("g").selectAll("g").data(graph.nodes).enter().append("g").attr("class", "node");

            node.append("rect")
                .attr("x", function (d) { return d.x0; }).attr("y", function (d) { return d.y0; })
                .attr("height", function (d) { return d.y1 - d.y0; }).attr("width", function (d) { return d.x1 - d.x0; })
                .attr("fill", function (d) { return nodeColor(d); })
                .attr("rx", 2)
                .on("mouseover", function (event, d) {
                    var totalIn = d.targetLinks.reduce(function (s, l) { return s + l.value; }, 0);
                    var totalOut = d.sourceLinks.reduce(function (s, l) { return s + l.value; }, 0);
                    tooltip.style("opacity", 1)
                        .html("<strong>" + d.name + "</strong>" +
                            (totalIn ? "<br/>In: " + totalIn.toLocaleString() : "") +
                            (totalOut ? "<br/>Out: " + totalOut.toLocaleString() : ""));
                    McpAppBridge.positionTooltip(tooltip, event);
                })
                .on("mouseout", function () { tooltip.style("opacity", 0); });

            var w = getWidth();
            node.append("text")
                .attr("x", function (d) { return d.x0 < w / 2 ? d.x1 + 8 : d.x0 - 8; })
                .attr("y", function (d) { return (d.y1 + d.y0) / 2; })
                .attr("dy", "0.35em")
                .attr("text-anchor", function (d) { return d.x0 < w / 2 ? "start" : "end"; })
                .text(function (d) { return d.name; });
        }

        function renderData(data) {
            document.getElementById("loadingIndicator").classList.add("hidden");
            initializeSVG();
            try {
                if (!data.nodes || !data.nodes.length || !data.links || !data.links.length) {
                    throw new Error("Sankey data must include at least one node and one link.");
                }
                drawSankey(data);
            } catch (e) {
                svg.append("text")
                    .attr("x", getWidth() / 2).attr("y", getHeight() / 2)
                    .attr("text-anchor", "middle")
                    .attr("fill", McpAppBridge.cssVar("--color-text-secondary") || "#878787")
                    .attr("font-size", "14px")
                    .text("Unable to render diagram: " + (e.message || e));
            }
            setTimeout(McpAppBridge.reportSize, 80);
        }

        McpAppBridge.init({
            appName: "autovisualiser-sankey",
            onData: renderData,
            onTheme: function () {
                if (McpAppBridge.currentData) renderData(McpAppBridge.currentData);
            },
        });
    })();
    </script>
</body>
</html>
