package com.cameleer.server.app.diagram;

import com.cameleer.common.graph.NodeType;
import com.cameleer.common.graph.RouteEdge;
import com.cameleer.common.graph.RouteGraph;
import com.cameleer.common.graph.RouteNode;
import com.cameleer.server.core.diagram.DiagramLayout;
import com.cameleer.server.core.diagram.DiagramRenderer;
import com.cameleer.server.core.diagram.PositionedEdge;
import com.cameleer.server.core.diagram.PositionedNode;
import org.eclipse.elk.alg.layered.options.LayeredMetaDataProvider;
import org.eclipse.elk.alg.layered.options.NodePlacementStrategy;
import org.eclipse.elk.core.RecursiveGraphLayoutEngine;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.core.options.Direction;
import org.eclipse.elk.core.options.EdgeRouting;
import org.eclipse.elk.core.options.HierarchyHandling;
import org.eclipse.elk.core.util.BasicProgressMonitor;
import org.eclipse.elk.graph.ElkBendPoint;
import org.eclipse.elk.graph.ElkEdge;
import org.eclipse.elk.graph.ElkEdgeSection;
import org.eclipse.elk.graph.ElkGraphFactory;
import org.eclipse.elk.graph.ElkNode;
import org.jfree.svg.SVGGraphics2D;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.geom.GeneralPath;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * ELK + JFreeSVG implementation of {@link DiagramRenderer}.
 * <p>
 * Uses Eclipse ELK layered algorithm for layout computation
 * and JFreeSVG for SVG document generation with color-coded nodes.
 */
public class ElkDiagramRenderer implements DiagramRenderer {

    // Register ELK provider once (not per-instance)
    static {
        org.eclipse.elk.core.data.LayoutMetaDataService.getInstance()
                .registerLayoutMetaDataProviders(new LayeredMetaDataProvider());
    }

    private static final int PADDING = 20;
    private static final int NODE_HEIGHT = 56;
    private static final int NODE_WIDTH = 220;
    private static final int COMPOUND_TOP_PADDING = 30;
    private static final int COMPOUND_SIDE_PADDING = 10;
    private static final int CORNER_RADIUS = 8;
    private static final double NODE_SPACING = 120.0;
    private static final double EDGE_SPACING = 20.0;

    // Blue: endpoints
    private static final Color BLUE = Color.decode("#3B82F6");
    // Green: processors
    private static final Color GREEN = Color.decode("#22C55E");
    // Red: error handling
    private static final Color RED = Color.decode("#EF4444");
    // Purple: EIP patterns
    private static final Color PURPLE = Color.decode("#A855F7");
    // Cyan: cross-route
    private static final Color CYAN = Color.decode("#06B6D4");
    // Gray: edges
    private static final Color EDGE_GRAY = Color.decode("#9CA3AF");

    private static final Set<NodeType> ENDPOINT_TYPES = EnumSet.of(
            NodeType.ENDPOINT, NodeType.TO, NodeType.TO_DYNAMIC, NodeType.DIRECT, NodeType.SEDA
    );

    private static final Set<NodeType> PROCESSOR_TYPES = EnumSet.of(
            NodeType.PROCESSOR, NodeType.BEAN, NodeType.LOG,
            NodeType.SET_HEADER, NodeType.SET_BODY, NodeType.TRANSFORM,
            NodeType.MARSHAL, NodeType.UNMARSHAL
    );

    private static final Set<NodeType> ERROR_TYPES = EnumSet.of(
            NodeType.ERROR_HANDLER, NodeType.ON_EXCEPTION, NodeType.TRY_CATCH,
            NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY
    );

    private static final Set<NodeType> EIP_TYPES = EnumSet.of(
            NodeType.EIP_CHOICE, NodeType.EIP_WHEN, NodeType.EIP_OTHERWISE,
            NodeType.EIP_SPLIT, NodeType.EIP_AGGREGATE, NodeType.EIP_MULTICAST,
            NodeType.EIP_FILTER, NodeType.EIP_RECIPIENT_LIST, NodeType.EIP_ROUTING_SLIP,
            NodeType.EIP_DYNAMIC_ROUTER, NodeType.EIP_LOAD_BALANCE, NodeType.EIP_THROTTLE,
            NodeType.EIP_DELAY, NodeType.EIP_LOOP, NodeType.EIP_IDEMPOTENT_CONSUMER,
            NodeType.EIP_CIRCUIT_BREAKER, NodeType.EIP_PIPELINE
    );

    private static final Set<NodeType> CROSS_ROUTE_TYPES = EnumSet.of(
            NodeType.EIP_WIRE_TAP, NodeType.EIP_ENRICH, NodeType.EIP_POLL_ENRICH
    );

    /** NodeTypes that act as compound containers with children. */
    private static final Set<NodeType> COMPOUND_TYPES = EnumSet.of(
            NodeType.EIP_CHOICE, NodeType.EIP_WHEN, NodeType.EIP_OTHERWISE,
            NodeType.EIP_SPLIT, NodeType.TRY_CATCH,
            NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY,
            NodeType.EIP_LOOP, NodeType.EIP_MULTICAST,
            NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER,
            NodeType.ON_COMPLETION, NodeType.EIP_CIRCUIT_BREAKER,
            NodeType.EIP_FILTER, NodeType.EIP_IDEMPOTENT_CONSUMER, NodeType.EIP_RECIPIENT_LIST
    );

    /** Top-level handler types laid out in their own separate ELK graph. */
    private static final Set<NodeType> HANDLER_SECTION_TYPES = EnumSet.of(
            NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, NodeType.ON_COMPLETION
    );

    @Override
    public String renderSvg(RouteGraph graph) {
        LayoutResult result = computeLayout(graph, Direction.DOWN);
        DiagramLayout layout = result.layout;

        int svgWidth = (int) Math.ceil(layout.width()) + 2 * PADDING;
        int svgHeight = (int) Math.ceil(layout.height()) + 2 * PADDING;

        SVGGraphics2D g2 = new SVGGraphics2D(svgWidth, svgHeight);
        g2.translate(PADDING, PADDING);

        // Draw edges first (behind nodes)
        g2.setStroke(new BasicStroke(2.0f));
        g2.setColor(EDGE_GRAY);
        for (PositionedEdge edge : layout.edges()) {
            drawEdge(g2, edge);
        }

        // Draw nodes
        Font labelFont = new Font("SansSerif", Font.PLAIN, 12);
        g2.setFont(labelFont);

        // Collect IDs of nodes drawn inside compounds (to avoid double-drawing)
        Set<String> compoundChildIds = new HashSet<>();
        for (PositionedNode node : allNodes(layout.nodes())) {
            if (result.compoundInfos.containsKey(node.id()) && node.children() != null) {
                for (PositionedNode child : node.children()) {
                    collectAllIds(child, compoundChildIds);
                }
            }
        }

        // Draw compound containers first (background + children inside)
        for (Map.Entry<String, CompoundInfo> entry : result.compoundInfos.entrySet()) {
            CompoundInfo ci = entry.getValue();
            PositionedNode pn = findNode(layout.nodes(), ci.nodeId);
            if (pn != null) {
                drawCompoundContainer(g2, pn, ci.color);
            }
        }

        // Draw leaf nodes (skip compounds and their children — already drawn above)
        for (PositionedNode node : allNodes(layout.nodes())) {
            if (compoundChildIds.contains(node.id())) continue;
            if (!result.compoundInfos.containsKey(node.id()) || node.children().isEmpty()) {
                drawNode(g2, node, result.nodeColors.getOrDefault(node.id(), PURPLE));
            }
        }

        return g2.getSVGDocument();
    }

    @Override
    public DiagramLayout layoutJson(RouteGraph graph) {
        return computeLayout(graph, Direction.RIGHT).layout;
    }

    @Override
    public DiagramLayout layoutJson(RouteGraph graph, String direction) {
        Direction dir = "TB".equalsIgnoreCase(direction) ? Direction.DOWN : Direction.RIGHT;
        return computeLayout(graph, dir).layout;
    }

    // ----------------------------------------------------------------
    // Layout computation
    // ----------------------------------------------------------------

    private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
        // 1. Build node index from root tree (preserves children) with flat-list fallback
        Map<String, RouteNode> nodeById = buildNodeIndex(graph);
        LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE, nodeById);

        // 2. Partition graph nodes into main flow vs handler sections
        List<RouteNode> mainNodes = new ArrayList<>();
        List<RouteNode> handlerNodes = new ArrayList<>();
        partitionNodes(graph, nodeById, mainNodes, handlerNodes);

        // 3. Build ELK graphs — main flow + separate handler roots
        ElkNode rootNode = createElkRoot("root", rootDirection, 1.0, ctx);
        for (RouteNode rn : mainNodes) {
            if (!ctx.elkNodeMap.containsKey(rn.getId())) {
                createElkNodeRecursive(rn, rootNode, ctx);
            }
        }

        List<ElkNode> handlerRoots = new ArrayList<>();
        for (RouteNode rn : handlerNodes) {
            ElkNode hr = createElkRoot("handler-root-" + rn.getId(), rootDirection, 0.5, ctx);
            createElkNodeRecursive(rn, hr, ctx);
            handlerRoots.add(hr);
        }

        // 4. Create ELK edges (filtering DO_TRY internals and cross-root edges)
        createElkEdges(graph, ctx);

        // 5. Run layout engine
        RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
        engine.layout(rootNode, new BasicProgressMonitor());
        for (ElkNode hr : handlerRoots) {
            engine.layout(hr, new BasicProgressMonitor());
        }

        // 6. Post-process: fix DO_TRY section ordering and widths
        postProcessDoTrySections(ctx);

        // 7. Extract positioned result
        return extractLayout(graph, rootNode, handlerRoots, ctx);
    }

    /**
     * Build a node lookup by walking the root tree (which preserves children for
     * compound nodes). Falls back to the flat nodes list for any nodes not in the
     * tree (backward compatibility when root is not set).
     */
    private Map<String, RouteNode> buildNodeIndex(RouteGraph graph) {
        Map<String, RouteNode> nodeById = new HashMap<>();
        if (graph.getRoot() != null) {
            collectNodesRecursive(graph.getRoot(), nodeById);
        }
        if (graph.getNodes() != null) {
            for (RouteNode rn : graph.getNodes()) {
                nodeById.putIfAbsent(rn.getId(), rn);
            }
        }
        return nodeById;
    }

    /** Recursively collect RouteNodes from the tree into a map (preserves children). */
    private void collectNodesRecursive(RouteNode node, Map<String, RouteNode> map) {
        if (node.getId() != null) {
            map.put(node.getId(), node);
        }
        if (node.getChildren() != null) {
            for (RouteNode child : node.getChildren()) {
                collectNodesRecursive(child, map);
            }
        }
    }

    /**
     * Separate main flow nodes from handler sections by walking FLOW edges
     * from the graph root. Handler sections (onException, onCompletion, etc.)
     * are placed in their own list for independent layout.
     */
    private void partitionNodes(RouteGraph graph, Map<String, RouteNode> nodeById,
                                List<RouteNode> mainNodes, List<RouteNode> handlerNodes) {
        Set<String> mainNodeIds = new HashSet<>();

        if (graph.getRoot() != null && graph.getEdges() != null) {
            mainNodeIds.add(graph.getRoot().getId());
            boolean changed = true;
            while (changed) {
                changed = false;
                for (RouteEdge re : graph.getEdges()) {
                    if (re.getEdgeType() == RouteEdge.EdgeType.ERROR) continue;
                    if (mainNodeIds.contains(re.getSource()) && !mainNodeIds.contains(re.getTarget())) {
                        RouteNode target = nodeById.get(re.getTarget());
                        if (target != null && target.getType() != null
                                && HANDLER_SECTION_TYPES.contains(target.getType())) {
                            continue;
                        }
                        mainNodeIds.add(re.getTarget());
                        changed = true;
                    }
                }
            }
        }

        Set<String> seen = new HashSet<>();
        for (RouteNode rn : nodeById.values()) {
            if (seen.contains(rn.getId())) continue;
            seen.add(rn.getId());
            if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType())
                    && rn.getChildren() != null && !rn.getChildren().isEmpty()) {
                handlerNodes.add(rn);
            } else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
                mainNodes.add(rn);
            }
        }
    }

    /** Create a configured ELK root node for the layered algorithm. */
    private ElkNode createElkRoot(String identifier, Direction direction,
                                  double spacingScale, LayoutContext ctx) {
        ElkNode root = ctx.factory.createElkNode();
        root.setIdentifier(identifier);
        root.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
        root.setProperty(CoreOptions.DIRECTION, direction);
        root.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * spacingScale);
        root.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * spacingScale);
        root.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
        root.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
        root.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
                NodePlacementStrategy.NETWORK_SIMPLEX);
        return root;
    }

    /** Create ELK edges, skipping structural compound-to-child edges and cross-root edges. */
    private void createElkEdges(RouteGraph graph, LayoutContext ctx) {
        if (graph.getEdges() == null) return;
        for (RouteEdge re : graph.getEdges()) {
            ElkNode sourceElk = ctx.elkNodeMap.get(re.getSource());
            ElkNode targetElk = ctx.elkNodeMap.get(re.getTarget());
            if (sourceElk == null || targetElk == null) continue;

            // Skip edges from any compound node to its own descendants
            // (these are structural edges, not flow edges)
            if (ctx.compoundNodeIds.contains(re.getSource()) && isDescendantOf(targetElk, sourceElk)) {
                continue;
            }
            // Skip edges that cross ELK root boundaries — but allow continuation
            // edges that exit a compound (source inside compound, target outside)
            if (getElkRoot(sourceElk) != getElkRoot(targetElk)) {
                // Allow if source and target share the same grandparent (main root)
                // i.e., one is inside a compound and the other is a sibling
                ElkNode sourceRoot = getElkRoot(sourceElk);
                ElkNode targetRoot = getElkRoot(targetElk);
                boolean sameGrandparent = sourceRoot.getParent() != null
                        && targetRoot.getParent() != null
                        && sourceRoot.getParent() == targetRoot.getParent();
                boolean sourceExitsCompound = sourceRoot.getParent() != null
                        && targetRoot == sourceRoot.getParent();
                boolean targetExitsCompound = targetRoot.getParent() != null
                        && sourceRoot == targetRoot.getParent();
                if (!sameGrandparent && !sourceExitsCompound && !targetExitsCompound) continue;
            }

            ElkEdge elkEdge = ctx.factory.createElkEdge();
            elkEdge.setContainingNode(findCommonParent(sourceElk, targetElk));
            elkEdge.getSources().add(sourceElk);
            elkEdge.getTargets().add(targetElk);
        }
    }

    /**
     * Post-process DO_TRY compounds after layout: re-stack sections in correct
     * vertical order (try_body → doFinally → doCatch) and stretch to uniform width.
     * ELK doesn't reliably order disconnected children within a compound.
     */
    private void postProcessDoTrySections(LayoutContext ctx) {
        for (Map.Entry<String, List<String>> entry : ctx.doTrySectionOrder.entrySet()) {
            String doTryId = entry.getKey();
            List<String> orderedIds = entry.getValue();
            List<ElkNode> sections = new ArrayList<>();
            for (String id : orderedIds) {
                ElkNode section = ctx.elkNodeMap.get(id);
                if (section != null) sections.add(section);
            }
            if (sections.size() < 2) continue;

            // Left-align all sections and stack vertically
            double startY = sections.stream().mapToDouble(ElkNode::getY).min().orElse(0);
            double spacing = 20;
            double currentY = startY;
            for (ElkNode section : sections) {
                section.setX(COMPOUND_SIDE_PADDING);
                section.setY(currentY);
                currentY += section.getHeight() + spacing;
            }

            // Uniform width
            double maxWidth = sections.stream().mapToDouble(ElkNode::getWidth).max().orElse(0);
            for (ElkNode section : sections) {
                section.setWidth(maxWidth);
            }

            // Shrink DO_TRY parent height to fit its content
            ElkNode doTryNode = ctx.elkNodeMap.get(doTryId);
            if (doTryNode != null) {
                double contentBottom = currentY - spacing + COMPOUND_SIDE_PADDING;
                doTryNode.setHeight(contentBottom);
                doTryNode.setWidth(maxWidth + 2 * COMPOUND_SIDE_PADDING);
            }
        }
    }

    /** Extract positioned nodes, edges, and bounding box from the ELK layout result. */
    private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode,
                                       List<ElkNode> handlerRoots, LayoutContext ctx) {
        // Extract positioned nodes (uses tree-aware nodeById for compound children)
        List<PositionedNode> positionedNodes = new ArrayList<>();
        for (RouteNode rn : ctx.nodeById.values()) {
            if (ctx.childNodeIds.contains(rn.getId())) continue;
            ElkNode elkNode = ctx.elkNodeMap.get(rn.getId());
            if (elkNode == null) continue;
            positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
        }

        // Extract edges from main root + handler roots
        List<PositionedEdge> positionedEdges = new ArrayList<>();
        List<ElkEdge> allEdges = collectAllEdges(rootNode);
        for (ElkNode hr : handlerRoots) {
            allEdges.addAll(collectAllEdges(hr));
        }
        for (ElkEdge elkEdge : allEdges) {
            String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
            String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
            ElkNode containingNode = elkEdge.getContainingNode();
            ElkNode edgeRoot = containingNode != null ? getElkRoot(containingNode) : null;

            List<double[]> points = new ArrayList<>();
            for (ElkEdgeSection section : elkEdge.getSections()) {
                double cx = containingNode != null ? getAbsoluteX(containingNode, edgeRoot) : 0;
                double cy = containingNode != null ? getAbsoluteY(containingNode, edgeRoot) : 0;
                points.add(new double[]{section.getStartX() + cx, section.getStartY() + cy});
                for (ElkBendPoint bp : section.getBendPoints()) {
                    points.add(new double[]{bp.getX() + cx, bp.getY() + cy});
                }
                points.add(new double[]{section.getEndX() + cx, section.getEndY() + cy});
            }

            String label = "";
            if (graph.getEdges() != null) {
                for (RouteEdge re : graph.getEdges()) {
                    if (re.getSource().equals(sourceId) && re.getTarget().equals(targetId)) {
                        label = re.getLabel() != null ? re.getLabel() : "";
                        break;
                    }
                }
            }
            positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
        }

        // Compute bounding box
        double totalWidth = 0, totalHeight = 0;
        for (PositionedNode pn : allNodes(positionedNodes)) {
            totalWidth = Math.max(totalWidth, pn.x() + pn.width());
            totalHeight = Math.max(totalHeight, pn.y() + pn.height());
        }
        for (PositionedEdge pe : positionedEdges) {
            for (double[] pt : pe.points()) {
                totalWidth = Math.max(totalWidth, pt[0]);
                totalHeight = Math.max(totalHeight, pt[1]);
            }
        }

        DiagramLayout layout = new DiagramLayout(totalWidth, totalHeight, positionedNodes, positionedEdges);
        return new LayoutResult(layout, ctx.nodeColors, ctx.compoundInfos);
    }

    // ----------------------------------------------------------------
    // SVG drawing helpers
    // ----------------------------------------------------------------

    private void drawNode(SVGGraphics2D g2, PositionedNode node, Color color) {
        g2.setColor(color);
        g2.fill(new RoundRectangle2D.Double(
                node.x(), node.y(), node.width(), node.height(),
                CORNER_RADIUS, CORNER_RADIUS));

        // White label
        g2.setColor(Color.WHITE);
        FontMetrics fm = g2.getFontMetrics();
        String label = node.label();
        int labelWidth = fm.stringWidth(label);
        float labelX = (float) (node.x() + (node.width() - labelWidth) / 2.0);
        float labelY = (float) (node.y() + 24);
        g2.drawString(label, labelX, labelY);
    }

    private void drawCompoundContainer(SVGGraphics2D g2, PositionedNode node, Color color) {
        // Semi-transparent background
        Color bg = new Color(color.getRed(), color.getGreen(), color.getBlue(), 38);
        g2.setColor(bg);
        g2.fill(new RoundRectangle2D.Double(
                node.x(), node.y(), node.width(), node.height(),
                CORNER_RADIUS, CORNER_RADIUS));

        // Border
        g2.setColor(color);
        g2.setStroke(new BasicStroke(1.5f));
        g2.draw(new RoundRectangle2D.Double(
                node.x(), node.y(), node.width(), node.height(),
                CORNER_RADIUS, CORNER_RADIUS));

        // Label at top
        g2.setColor(color);
        float labelX = (float) (node.x() + COMPOUND_SIDE_PADDING);
        float labelY = (float) (node.y() + 18);
        g2.drawString(node.label(), labelX, labelY);

        // Draw children inside
        for (PositionedNode child : node.children()) {
            Color childColor = colorForTypeName(child.type());
            drawNode(g2, child, childColor);
        }
    }

    private void drawEdge(SVGGraphics2D g2, PositionedEdge edge) {
        List<double[]> points = edge.points();
        if (points.size() < 2) return;

        GeneralPath path = new GeneralPath();
        path.moveTo(points.get(0)[0], points.get(0)[1]);
        for (int i = 1; i < points.size(); i++) {
            path.lineTo(points.get(i)[0], points.get(i)[1]);
        }
        g2.draw(path);

        // Arrowhead at the last point
        if (points.size() >= 2) {
            double[] end = points.get(points.size() - 1);
            double[] prev = points.get(points.size() - 2);
            drawArrowhead(g2, prev[0], prev[1], end[0], end[1]);
        }
    }

    private void drawArrowhead(SVGGraphics2D g2, double fromX, double fromY, double toX, double toY) {
        double angle = Math.atan2(toY - fromY, toX - fromX);
        int arrowSize = 8;

        GeneralPath arrow = new GeneralPath();
        arrow.moveTo(toX, toY);
        arrow.lineTo(toX - arrowSize * Math.cos(angle - Math.PI / 6),
                toY - arrowSize * Math.sin(angle - Math.PI / 6));
        arrow.lineTo(toX - arrowSize * Math.cos(angle + Math.PI / 6),
                toY - arrowSize * Math.sin(angle + Math.PI / 6));
        arrow.closePath();
        g2.fill(arrow);
    }

    // ----------------------------------------------------------------
    // Color mapping
    // ----------------------------------------------------------------

    private Color colorForType(NodeType type) {
        if (type == null) return PURPLE;
        if (ENDPOINT_TYPES.contains(type)) return BLUE;
        if (PROCESSOR_TYPES.contains(type)) return GREEN;
        if (ERROR_TYPES.contains(type)) return RED;
        if (EIP_TYPES.contains(type)) return PURPLE;
        if (CROSS_ROUTE_TYPES.contains(type)) return CYAN;
        return PURPLE;
    }

    private Color colorForTypeName(String typeName) {
        try {
            NodeType type = NodeType.valueOf(typeName);
            return colorForType(type);
        } catch (IllegalArgumentException e) {
            return PURPLE;
        }
    }

    // ----------------------------------------------------------------
    // Recursive node building
    // ----------------------------------------------------------------

    /**
     * Recursively create ELK nodes. Compound nodes become ELK containers
     * with their children nested inside. Non-compound nodes become leaf nodes.
     */
    private void createElkNodeRecursive(
            RouteNode rn, ElkNode parentElk, LayoutContext ctx) {

        boolean isCompound = rn.getType() != null && COMPOUND_TYPES.contains(rn.getType())
                && rn.getChildren() != null && !rn.getChildren().isEmpty();

        ElkNode elkNode = ctx.factory.createElkNode();
        elkNode.setIdentifier(rn.getId());
        elkNode.setParent(parentElk);

        if (isCompound && rn.getType() == NodeType.DO_TRY) {
            // DO_TRY: vertical container with a virtual _TRY_BODY wrapper for the try body
            // and DO_CATCH/DO_FINALLY as separate children below
            ctx.doTryNodeIds.add(rn.getId());
            ctx.compoundNodeIds.add(rn.getId());
            elkNode.setWidth(200);
            elkNode.setHeight(100);
            elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
            elkNode.setProperty(CoreOptions.DIRECTION, Direction.DOWN);
            elkNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.4);
            elkNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.3);
            elkNode.setProperty(CoreOptions.PADDING,
                    new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
                            COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
            // Separate try body children from handler children
            List<RouteNode> tryBodyChildren = new ArrayList<>();
            List<RouteNode> handlerChildren = new ArrayList<>();
            for (RouteNode child : rn.getChildren()) {
                if (child.getType() == NodeType.DO_CATCH || child.getType() == NodeType.DO_FINALLY) {
                    handlerChildren.add(child);
                } else {
                    tryBodyChildren.add(child);
                }
            }

            // Track desired section order: try_body → doFinally → doCatch
            List<String> sectionOrder = new ArrayList<>();

            // Virtual _TRY_BODY wrapper
            if (!tryBodyChildren.isEmpty()) {
                String wrapperId = rn.getId() + "._try_body";
                ElkNode wrapper = ctx.factory.createElkNode();
                wrapper.setIdentifier(wrapperId);
                wrapper.setParent(elkNode);
                wrapper.setWidth(200);
                wrapper.setHeight(40);
                wrapper.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
                wrapper.setProperty(CoreOptions.DIRECTION, Direction.RIGHT);
                wrapper.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
                wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
                wrapper.setProperty(CoreOptions.PADDING,
                        new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8));
                ctx.compoundNodeIds.add(wrapperId);
                ctx.elkNodeMap.put(wrapperId, wrapper);
                sectionOrder.add(wrapperId);

                for (RouteNode child : tryBodyChildren) {
                    ctx.childNodeIds.add(child.getId());
                    createElkNodeRecursive(child, wrapper, ctx);
                }
            }

            // Handler sections in order: DO_FINALLY (middle), then DO_CATCH (bottom)
            for (RouteNode handler : orderedHandlerChildren(handlerChildren)) {
                ctx.childNodeIds.add(handler.getId());
                createElkNodeRecursive(handler, elkNode, ctx);
                sectionOrder.add(handler.getId());
            }

            ctx.doTrySectionOrder.put(rn.getId(), sectionOrder);
        } else if (isCompound && rn.getType() == NodeType.EIP_CIRCUIT_BREAKER) {
            // CIRCUIT_BREAKER: vertical container with _CB_MAIN for main path
            // and onFallback as a compound section below (like DO_TRY pattern)
            ctx.compoundNodeIds.add(rn.getId());
            elkNode.setWidth(200);
            elkNode.setHeight(100);
            elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
            elkNode.setProperty(CoreOptions.DIRECTION, Direction.DOWN);
            elkNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.4);
            elkNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.3);
            elkNode.setProperty(CoreOptions.PADDING,
                    new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
                            COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));

            // Separate main path children from onFallback child
            List<RouteNode> mainChildren = new ArrayList<>();
            RouteNode fallbackNode = null;
            for (RouteNode child : rn.getChildren()) {
                if ("onFallback".equals(child.getLabel())) {
                    fallbackNode = child;
                } else {
                    mainChildren.add(child);
                }
            }

            List<String> sectionOrder = new ArrayList<>();

            // Virtual _CB_MAIN wrapper for main path (horizontal flow)
            if (!mainChildren.isEmpty()) {
                String wrapperId = rn.getId() + "._cb_main";
                ElkNode wrapper = ctx.factory.createElkNode();
                wrapper.setIdentifier(wrapperId);
                wrapper.setParent(elkNode);
                wrapper.setWidth(200);
                wrapper.setHeight(40);
                wrapper.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
                wrapper.setProperty(CoreOptions.DIRECTION, Direction.RIGHT);
                wrapper.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
                wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
                wrapper.setProperty(CoreOptions.PADDING,
                        new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8));
                ctx.compoundNodeIds.add(wrapperId);
                ctx.elkNodeMap.put(wrapperId, wrapper);
                sectionOrder.add(wrapperId);

                for (RouteNode child : mainChildren) {
                    ctx.childNodeIds.add(child.getId());
                    createElkNodeRecursive(child, wrapper, ctx);
                }
            }

            // onFallback as compound section containing its children
            if (fallbackNode != null) {
                ctx.childNodeIds.add(fallbackNode.getId());
                ElkNode fallbackElk = ctx.factory.createElkNode();
                fallbackElk.setIdentifier(fallbackNode.getId());
                fallbackElk.setParent(elkNode);
                fallbackElk.setWidth(200);
                fallbackElk.setHeight(40);
                fallbackElk.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
                fallbackElk.setProperty(CoreOptions.DIRECTION, Direction.RIGHT);
                fallbackElk.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
                fallbackElk.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
                fallbackElk.setProperty(CoreOptions.PADDING,
                        new org.eclipse.elk.core.math.ElkPadding(18, 8, 8, 8));
                ctx.compoundNodeIds.add(fallbackNode.getId());
                ctx.elkNodeMap.put(fallbackNode.getId(), fallbackElk);
                sectionOrder.add(fallbackNode.getId());

                if (fallbackNode.getChildren() != null) {
                    for (RouteNode child : fallbackNode.getChildren()) {
                        ctx.childNodeIds.add(child.getId());
                        createElkNodeRecursive(child, fallbackElk, ctx);
                    }
                }
            }

            ctx.doTrySectionOrder.put(rn.getId(), sectionOrder);
        } else if (isCompound) {
            ctx.compoundNodeIds.add(rn.getId());
            elkNode.setWidth(200);
            elkNode.setHeight(100);
            elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
            elkNode.setProperty(CoreOptions.DIRECTION, Direction.DOWN);
            elkNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
            elkNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
            elkNode.setProperty(CoreOptions.PADDING,
                    new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
                            COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));

            for (RouteNode child : rn.getChildren()) {
                ctx.childNodeIds.add(child.getId());
                createElkNodeRecursive(child, elkNode, ctx);
            }
        } else {
            elkNode.setWidth(NODE_WIDTH);
            elkNode.setHeight(NODE_HEIGHT);
        }

        ctx.elkNodeMap.put(rn.getId(), elkNode);
        ctx.nodeColors.put(rn.getId(), colorForType(rn.getType()));
    }

    /**
     * Recursively extract a PositionedNode from the ELK layout result.
     * All coordinates are absolute (relative to the ELK root).
     */
    private PositionedNode extractPositionedNode(
            RouteNode rn, ElkNode elkNode, ElkNode rootNode, LayoutContext ctx) {

        double absX = getAbsoluteX(elkNode, rootNode);
        double absY = getAbsoluteY(elkNode, rootNode);

        List<PositionedNode> children = List.of();
        if (ctx.compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) {
            children = new ArrayList<>();

            if (rn.getType() == NodeType.DO_TRY) {
                // DO_TRY: extract virtual _TRY_BODY wrapper first, then handler children
                String wrapperId = rn.getId() + "._try_body";
                ElkNode wrapperElk = ctx.elkNodeMap.get(wrapperId);
                if (wrapperElk != null) {
                    List<PositionedNode> wrapperChildren = new ArrayList<>();
                    for (RouteNode child : rn.getChildren()) {
                        if (child.getType() != NodeType.DO_CATCH && child.getType() != NodeType.DO_FINALLY) {
                            ElkNode childElk = ctx.elkNodeMap.get(child.getId());
                            if (childElk != null) {
                                wrapperChildren.add(extractPositionedNode(child, childElk, rootNode, ctx));
                            }
                        }
                    }
                    children.add(new PositionedNode(
                            wrapperId, "", "_TRY_BODY",
                            getAbsoluteX(wrapperElk, rootNode),
                            getAbsoluteY(wrapperElk, rootNode),
                            wrapperElk.getWidth(), wrapperElk.getHeight(),
                            wrapperChildren, null));
                    ctx.compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
                }
                // Handler children in order: DO_FINALLY first, then DO_CATCH
                for (RouteNode handler : orderedHandlerChildren(rn.getChildren())) {
                    ElkNode childElk = ctx.elkNodeMap.get(handler.getId());
                    if (childElk != null) {
                        children.add(extractPositionedNode(handler, childElk, rootNode, ctx));
                    }
                }
            } else if (rn.getType() == NodeType.EIP_CIRCUIT_BREAKER) {
                // CIRCUIT_BREAKER: extract _CB_MAIN wrapper, then onFallback section
                String mainWrapperId = rn.getId() + "._cb_main";
                ElkNode mainWrapperElk = ctx.elkNodeMap.get(mainWrapperId);
                if (mainWrapperElk != null) {
                    List<PositionedNode> wrapperChildren = new ArrayList<>();
                    for (RouteNode child : rn.getChildren()) {
                        if (!"onFallback".equals(child.getLabel())) {
                            ElkNode childElk = ctx.elkNodeMap.get(child.getId());
                            if (childElk != null) {
                                wrapperChildren.add(extractPositionedNode(child, childElk, rootNode, ctx));
                            }
                        }
                    }
                    children.add(new PositionedNode(
                            mainWrapperId, "", "_CB_MAIN",
                            getAbsoluteX(mainWrapperElk, rootNode),
                            getAbsoluteY(mainWrapperElk, rootNode),
                            mainWrapperElk.getWidth(), mainWrapperElk.getHeight(),
                            wrapperChildren, null));
                    ctx.compoundInfos.put(mainWrapperId, new CompoundInfo(mainWrapperId, Color.WHITE));
                }
                // onFallback section with its children, type overridden to _CB_FALLBACK
                for (RouteNode child : rn.getChildren()) {
                    if ("onFallback".equals(child.getLabel())) {
                        ElkNode childElk = ctx.elkNodeMap.get(child.getId());
                        if (childElk != null) {
                            List<PositionedNode> fallbackChildren = new ArrayList<>();
                            if (child.getChildren() != null) {
                                for (RouteNode fc : child.getChildren()) {
                                    ElkNode fcElk = ctx.elkNodeMap.get(fc.getId());
                                    if (fcElk != null) {
                                        fallbackChildren.add(extractPositionedNode(fc, fcElk, rootNode, ctx));
                                    }
                                }
                            }
                            children.add(new PositionedNode(
                                    child.getId(),
                                    child.getLabel() != null ? child.getLabel() : "",
                                    "_CB_FALLBACK",
                                    getAbsoluteX(childElk, rootNode),
                                    getAbsoluteY(childElk, rootNode),
                                    childElk.getWidth(), childElk.getHeight(),
                                    fallbackChildren, null));
                            ctx.compoundInfos.put(child.getId(),
                                    new CompoundInfo(child.getId(), colorForType(rn.getType())));
                        }
                    }
                }
            } else {
                for (RouteNode child : rn.getChildren()) {
                    ElkNode childElk = ctx.elkNodeMap.get(child.getId());
                    if (childElk != null) {
                        children.add(extractPositionedNode(child, childElk, rootNode, ctx));
                    }
                }
            }
            ctx.compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType())));
        }

        return new PositionedNode(
                rn.getId(),
                rn.getLabel() != null ? rn.getLabel() : "",
                rn.getType() != null ? rn.getType().name() : "UNKNOWN",
                absX, absY,
                elkNode.getWidth(), elkNode.getHeight(),
                children,
                rn.getEndpointUri()
        );
    }

    // ----------------------------------------------------------------
    // ELK graph helpers
    // ----------------------------------------------------------------

    /** Return handler children in section order: DO_FINALLY first, then DO_CATCH. */
    private static List<RouteNode> orderedHandlerChildren(List<RouteNode> children) {
        List<RouteNode> ordered = new ArrayList<>();
        for (RouteNode c : children) {
            if (c.getType() == NodeType.DO_FINALLY) ordered.add(c);
        }
        for (RouteNode c : children) {
            if (c.getType() == NodeType.DO_CATCH) ordered.add(c);
        }
        return ordered;
    }

    /** Check if 'child' is a descendant of 'ancestor' in the ELK node hierarchy. */
    private boolean isDescendantOf(ElkNode child, ElkNode ancestor) {
        ElkNode current = child.getParent();
        while (current != null) {
            if (current == ancestor) return true;
            current = current.getParent();
        }
        return false;
    }

    private ElkNode getElkRoot(ElkNode node) {
        if (node == null) return null;
        ElkNode current = node;
        while (current.getParent() != null) {
            current = current.getParent();
        }
        return current;
    }

    /** Proper lowest common ancestor of two ELK nodes. */
    private ElkNode findCommonParent(ElkNode a, ElkNode b) {
        Set<ElkNode> ancestorsOfA = new HashSet<>();
        ElkNode current = a;
        while (current != null) {
            ancestorsOfA.add(current);
            current = current.getParent();
        }
        current = b;
        while (current != null) {
            if (ancestorsOfA.contains(current)) {
                return current;
            }
            current = current.getParent();
        }
        return getElkRoot(a);
    }

    private double getAbsoluteX(ElkNode node, ElkNode root) {
        double x = 0;
        ElkNode current = node;
        while (current != null && current != root) {
            x += current.getX();
            current = current.getParent();
        }
        return x;
    }

    private double getAbsoluteY(ElkNode node, ElkNode root) {
        double y = 0;
        ElkNode current = node;
        while (current != null && current != root) {
            y += current.getY();
            current = current.getParent();
        }
        return y;
    }

    private List<ElkEdge> collectAllEdges(ElkNode node) {
        List<ElkEdge> edges = new ArrayList<>(node.getContainedEdges());
        for (ElkNode child : node.getChildren()) {
            edges.addAll(collectAllEdges(child));
        }
        return edges;
    }

    /** Recursively find a PositionedNode by ID. */
    private PositionedNode findNode(List<PositionedNode> nodes, String id) {
        for (PositionedNode n : nodes) {
            if (n.id().equals(id)) return n;
            if (n.children() != null) {
                PositionedNode found = findNode(n.children(), id);
                if (found != null) return found;
            }
        }
        return null;
    }

    /** Recursively flatten a PositionedNode tree. */
    private List<PositionedNode> allNodes(List<PositionedNode> nodes) {
        List<PositionedNode> all = new ArrayList<>();
        for (PositionedNode n : nodes) {
            all.add(n);
            if (n.children() != null && !n.children().isEmpty()) {
                all.addAll(allNodes(n.children()));
            }
        }
        return all;
    }

    /** Collect IDs of all RouteNode descendants (for handler separation). */
    private void collectDescendantIds(List<RouteNode> nodes, Set<String> ids) {
        for (RouteNode n : nodes) {
            ids.add(n.getId());
            if (n.getChildren() != null) {
                collectDescendantIds(n.getChildren(), ids);
            }
        }
    }

    private void collectAllIds(PositionedNode node, Set<String> ids) {
        ids.add(node.id());
        if (node.children() != null) {
            for (PositionedNode child : node.children()) {
                collectAllIds(child, ids);
            }
        }
    }

    // ----------------------------------------------------------------
    // Internal data classes
    // ----------------------------------------------------------------

    private record LayoutResult(
            DiagramLayout layout,
            Map<String, Color> nodeColors,
            Map<String, CompoundInfo> compoundInfos
    ) {}

    private record CompoundInfo(String nodeId, Color color) {}

    /** Mutable state accumulated during ELK graph construction and extraction. */
    private static class LayoutContext {
        final ElkGraphFactory factory;
        final Map<String, RouteNode> nodeById;
        final Map<String, ElkNode> elkNodeMap = new HashMap<>();
        final Map<String, Color> nodeColors = new HashMap<>();
        final Set<String> compoundNodeIds = new HashSet<>();
        final Set<String> childNodeIds = new HashSet<>();
        final Set<String> doTryNodeIds = new HashSet<>();
        final Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>();
        final Map<String, CompoundInfo> compoundInfos = new HashMap<>();

        LayoutContext(ElkGraphFactory factory, Map<String, RouteNode> nodeById) {
            this.factory = factory;
            this.nodeById = nodeById;
        }
    }
}
