import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import dagre from 'dagre';
import './UMLVisualization.css';

export type DiagramType = 'mermaid-class' | 'mermaid-er' | 'plantuml' | 'graphviz';

export interface UMLNode {
  id: string;
  name: string;
  type: 'class' | 'enum' | 'entity';
  attributes?: { name: string; type: string }[];
  methods?: { name: string; returnType?: string }[];
  x?: number;
  y?: number;
  width?: number;
  height?: number;
}

export interface UMLLink {
  source: string;
  target: string;
  type: 'inheritance' | 'composition' | 'aggregation' | 'association' | 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many';
  label?: string;
  cardinality?: string;
  bidirectional?: boolean;  // Whether this edge can be reversed
  isReversed?: boolean;     // Track if edge has been reversed by user
  reverseLabel?: string;    // Label for reverse direction (e.g., "refers_to_custodian" vs "has_collection")
  reverseCardinality?: string;  // Cardinality for reverse direction
}

export interface UMLDiagram {
  nodes: UMLNode[];
  links: UMLLink[];
  title?: string;
}

export type DagreDirection = 'TB' | 'BT' | 'LR' | 'RL';
export type DagreAlignment = 'UL' | 'UR' | 'DL' | 'DR' | undefined;
export type DagreRanker = 'network-simplex' | 'tight-tree' | 'longest-path';

interface UMLVisualizationProps {
  diagram: UMLDiagram;
  width?: number;
  height?: number;
  diagramType?: DiagramType;
  layoutType?: 'force' | 'dagre';
  dagreDirection?: DagreDirection;
  dagreAlignment?: DagreAlignment;
  dagreRanker?: DagreRanker;
}

export const UMLVisualization: React.FC<UMLVisualizationProps> = ({ 
  diagram, 
  width = 1200, 
  height = 800,
  // diagramType = 'mermaid-class',  // Unused for now
  layoutType = 'force',
  dagreDirection = 'TB',
  dagreAlignment = undefined,
  dagreRanker = 'network-simplex'
}) => {
  const svgRef = useRef<SVGSVGElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const [selectedNode, setSelectedNode] = useState<UMLNode | null>(null);
  const [zoom, setZoom] = useState(1);
  const zoomTransformRef = useRef<d3.ZoomTransform>(d3.zoomIdentity);
  const previousDiagramRef = useRef<string | null>(null);  // Track diagram changes for auto-fit

  useEffect(() => {
    if (!svgRef.current || !diagram) return;

    // Determine if this is a new diagram (should auto-fit) or same diagram with layout change (preserve zoom)
    const diagramId = diagram.title || JSON.stringify(diagram.nodes.map(n => n.id).sort());
    const isNewDiagram = previousDiagramRef.current !== diagramId;
    previousDiagramRef.current = diagramId;

    // Store current zoom transform before clearing (only if same diagram)
    const currentSvg = d3.select(svgRef.current);
    const currentTransform = d3.zoomTransform(currentSvg.node() as Element);
    if (!isNewDiagram && currentTransform && (currentTransform.k !== 1 || currentTransform.x !== 0 || currentTransform.y !== 0)) {
      zoomTransformRef.current = currentTransform;
    } else if (isNewDiagram) {
      // Reset zoom ref for new diagrams
      zoomTransformRef.current = d3.zoomIdentity;
    }

    // Clear previous content
    d3.select(svgRef.current).selectAll('*').remove();

    // Create SVG with zoom behavior
    const svg = d3.select(svgRef.current)
      .attr('width', width)
      .attr('height', height);

    const g = svg.append('g');

    // Add zoom behavior
    const zoomBehavior = d3.zoom<SVGSVGElement, unknown>()
      .scaleExtent([0.1, 4])
      .on('zoom', (event) => {
        g.attr('transform', event.transform);
        setZoom(event.transform.k);
      });

    svg.call(zoomBehavior);
    
    // Restore previous zoom transform (preserve camera position across layout changes)
    if (zoomTransformRef.current) {
      svg.call(zoomBehavior.transform as any, zoomTransformRef.current);
    }

    // Add custom event listeners for toolbar controls
    const handleZoomIn = () => {
      svg.transition().duration(300).call(zoomBehavior.scaleBy as any, 1.3);
    };

    const handleZoomOut = () => {
      svg.transition().duration(300).call(zoomBehavior.scaleBy as any, 0.7);
    };

    const handleResetView = () => {
      svg.transition().duration(500).call(
        zoomBehavior.transform as any,
        d3.zoomIdentity
      );
    };

    const handleFitToScreen = () => {
      // Get bounding box of all nodes
      const bounds = g.node()?.getBBox();
      if (!bounds) return;

      const fullWidth = width;
      const fullHeight = height;
      const midX = bounds.x + bounds.width / 2;
      const midY = bounds.y + bounds.height / 2;
      
      const scale = 0.9 / Math.max(bounds.width / fullWidth, bounds.height / fullHeight);
      const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];

      svg.transition().duration(500).call(
        zoomBehavior.transform as any,
        d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
      );
    };

    const handleExportPNG = async (event: Event) => {
      const customEvent = event as CustomEvent<{ filename: string }>;
      const filename = customEvent.detail?.filename || 'diagram';
      
      const svgElement = svgRef.current;
      if (!svgElement) return;

      try {
        // Get SVG bounding box to determine export size
        const bbox = g.node()?.getBBox();
        if (!bbox) return;

        // Create a canvas with the SVG content
        const canvas = document.createElement('canvas');
        const padding = 40;
        canvas.width = bbox.width + padding * 2;
        canvas.height = bbox.height + padding * 2;
        
        const ctx = canvas.getContext('2d');
        if (!ctx) return;

        // Fill white background
        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Get SVG data
        const svgData = new XMLSerializer().serializeToString(svgElement);
        const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
        const url = URL.createObjectURL(svgBlob);

        // Load SVG as image
        const img = new Image();
        img.onload = () => {
          // Get current transform
          const transform = d3.zoomTransform(svg.node() as any);
          
          // Draw with transform applied
          ctx.save();
          ctx.translate(padding - bbox.x * transform.k, padding - bbox.y * transform.k);
          ctx.scale(transform.k, transform.k);
          ctx.drawImage(img, 0, 0);
          ctx.restore();

          // Convert to PNG and download
          canvas.toBlob((blob) => {
            if (!blob) return;
            const pngUrl = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = pngUrl;
            a.download = `${filename}.png`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(pngUrl);
          });

          URL.revokeObjectURL(url);
        };
        img.src = url;
      } catch (error) {
        console.error('Error exporting PNG:', error);
        alert('Failed to export PNG. Please try again.');
      }
    };

    const handleExportSVG = (event: Event) => {
      const customEvent = event as CustomEvent<{ filename: string }>;
      const filename = customEvent.detail?.filename || 'diagram';
      
      const svgElement = svgRef.current;
      if (!svgElement) return;

      try {
        // Clone SVG to avoid modifying original
        const svgClone = svgElement.cloneNode(true) as SVGSVGElement;
        
        // Get bounding box and set viewBox
        const bbox = g.node()?.getBBox();
        if (bbox) {
          const padding = 40;
          svgClone.setAttribute('viewBox', 
            `${bbox.x - padding} ${bbox.y - padding} ${bbox.width + padding * 2} ${bbox.height + padding * 2}`
          );
          svgClone.setAttribute('width', `${bbox.width + padding * 2}`);
          svgClone.setAttribute('height', `${bbox.height + padding * 2}`);
        }

        // Add XML declaration and styling
        const svgData = new XMLSerializer().serializeToString(svgClone);
        const fullSvg = '<?xml version="1.0" encoding="UTF-8"?>\n' + svgData;
        
        // Create blob and download
        const blob = new Blob([fullSvg], { type: 'image/svg+xml;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${filename}.svg`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
      } catch (error) {
        console.error('Error exporting SVG:', error);
        alert('Failed to export SVG. Please try again.');
      }
    };

    window.addEventListener('uml-zoom-in', handleZoomIn);
    window.addEventListener('uml-zoom-out', handleZoomOut);
    window.addEventListener('uml-reset-view', handleResetView);
    window.addEventListener('uml-fit-to-screen', handleFitToScreen);
    window.addEventListener('uml-export-png', handleExportPNG as EventListener);
    window.addEventListener('uml-export-svg', handleExportSVG as EventListener);

    // Cleanup event listeners
    const cleanup = () => {
      window.removeEventListener('uml-zoom-in', handleZoomIn);
      window.removeEventListener('uml-zoom-out', handleZoomOut);
      window.removeEventListener('uml-reset-view', handleResetView);
      window.removeEventListener('uml-fit-to-screen', handleFitToScreen);
      window.removeEventListener('uml-export-png', handleExportPNG as EventListener);
      window.removeEventListener('uml-export-svg', handleExportSVG as EventListener);
    };

    // Calculate node dimensions
    const minNodeWidth = 150;
    const maxNodeWidth = 400;
    const nodeHeaderHeight = 40;
    const attributeHeight = 24;
    const methodHeight = 24;
    const nodePadding = 10;
    const textPadding = 20; // Horizontal padding for text inside boxes

    // Helper function to measure text width using a temporary SVG text element
    // fontFamily defaults to monospace to match the CSS for attributes/methods
    const measureTextWidth = (
      text: string, 
      fontSize: string, 
      fontWeight: string = 'normal',
      fontFamily: string = "'Monaco', 'Courier New', monospace"
    ): number => {
      const tempText = g.append('text')
        .attr('visibility', 'hidden')
        .attr('font-size', fontSize)
        .attr('font-weight', fontWeight)
        .attr('font-family', fontFamily)
        .text(text);
      
      const bbox = (tempText.node() as SVGTextElement)?.getBBox();
      const width = bbox?.width || 0;
      tempText.remove();
      
      return width;
    };

    // Calculate dynamic width for each node based on its content
    const calculateNodeWidth = (node: UMLNode): number => {
      let maxWidth = minNodeWidth;
      
      // Font families matching CSS
      const sansSerifFont = 'system-ui, -apple-system, sans-serif';
      const monospaceFont = "'Monaco', 'Courier New', monospace";

      // Measure node name (header text - bold, 14px, sans-serif)
      const nameWidth = measureTextWidth(node.name, '14px', 'bold', sansSerifFont) + textPadding * 2;
      maxWidth = Math.max(maxWidth, nameWidth);

      // Measure type badge width (10px, italic, sans-serif)
      const typeBadgeWidth = measureTextWidth(`«${node.type}»`, '10px', 'normal', sansSerifFont) + textPadding;
      maxWidth = Math.max(maxWidth, typeBadgeWidth);

      // Measure attribute widths (12px, monospace - wider font!)
      if (node.attributes) {
        node.attributes.forEach(attr => {
          const attrText = `${attr.name}: ${attr.type}`;
          const attrWidth = measureTextWidth(attrText, '12px', 'normal', monospaceFont) + textPadding * 2;
          maxWidth = Math.max(maxWidth, attrWidth);
        });
      }

      // Measure method widths (12px, monospace - wider font!)
      if (node.methods) {
        node.methods.forEach(method => {
          const methodText = method.returnType 
            ? `${method.name}(): ${method.returnType}`
            : `${method.name}()`;
          const methodWidth = measureTextWidth(methodText, '12px', 'normal', monospaceFont) + textPadding * 2;
          maxWidth = Math.max(maxWidth, methodWidth);
        });
      }

      // Clamp to max width
      return Math.min(maxWidth, maxNodeWidth);
    };

    // Helper function to calculate intersection point of a line with a rectangular box border
    // Given a line from (x1,y1) to (x2,y2) and a rectangle centered at (cx,cy) with width w and height h,
    // returns the point where the line intersects the rectangle border (from center outward)
    const getBoxBorderIntersection = (
      cx: number, cy: number,  // Rectangle center
      w: number, h: number,    // Rectangle width and height
      targetX: number, targetY: number  // Target point (other end of line)
    ): { x: number; y: number } => {
      const dx = targetX - cx;
      const dy = targetY - cy;
      
      // If target is at center, return center
      if (dx === 0 && dy === 0) {
        return { x: cx, y: cy };
      }
      
      const halfW = w / 2;
      const halfH = h / 2;
      
      // Calculate intersection with each edge and find the closest one
      // The line exits through the edge that has the smallest positive t value
      let t = Infinity;
      
      // Right edge (x = cx + halfW)
      if (dx > 0) {
        const tRight = halfW / dx;
        if (tRight < t && Math.abs(dy * tRight) <= halfH) {
          t = tRight;
        }
      }
      
      // Left edge (x = cx - halfW)
      if (dx < 0) {
        const tLeft = -halfW / dx;
        if (tLeft < t && Math.abs(dy * tLeft) <= halfH) {
          t = tLeft;
        }
      }
      
      // Bottom edge (y = cy + halfH)
      if (dy > 0) {
        const tBottom = halfH / dy;
        if (tBottom < t && Math.abs(dx * tBottom) <= halfW) {
          t = tBottom;
        }
      }
      
      // Top edge (y = cy - halfH)
      if (dy < 0) {
        const tTop = -halfH / dy;
        if (tTop < t && Math.abs(dx * tTop) <= halfW) {
          t = tTop;
        }
      }
      
      // If no valid intersection found (shouldn't happen), return center
      if (t === Infinity) {
        return { x: cx, y: cy };
      }
      
      return {
        x: cx + dx * t,
        y: cy + dy * t
      };
    };

    // Default node width for fallback (used where node.width is not yet set)
    const defaultNodeWidth = minNodeWidth;

    // IMPORTANT: Create deep copies to avoid mutating the original diagram prop
    // This prevents issues when switching layouts (force simulation mutates source/target to objects)
    const workingNodes = diagram.nodes.map(node => ({ ...node }));
    const workingLinks = diagram.links.map(link => ({ 
      ...link,
      // Always ensure source/target are strings (force simulation may have converted them to objects)
      source: typeof link.source === 'string' ? link.source : (link.source as any).id,
      target: typeof link.target === 'string' ? link.target : (link.target as any).id
    }));

    workingNodes.forEach(node => {
      const attributeCount = node.attributes?.length || 0;
      const methodCount = node.methods?.length || 0;
      
      // Calculate dynamic width based on content
      node.width = calculateNodeWidth(node);
      node.height = nodeHeaderHeight + 
                    (attributeCount > 0 ? attributeCount * attributeHeight + nodePadding : 0) +
                    (methodCount > 0 ? methodCount * methodHeight + nodePadding : 0);
    });

    // Layout algorithm: Force simulation or Dagre grid
    let simulation: d3.Simulation<any, undefined> | null = null;
    
    if (layoutType === 'dagre') {
      // Dagre grid layout (tight, hierarchical like Mermaid)
      const g = new dagre.graphlib.Graph();
      
      // Calculate spacing based on direction
      const isVertical = dagreDirection === 'TB' || dagreDirection === 'BT';
      const nodesep = isVertical ? 80 : 100;  // Horizontal spacing (or vertical if LR/RL)
      const ranksep = isVertical ? 120 : 150; // Vertical spacing between ranks (or horizontal if LR/RL)
      
      g.setGraph({ 
        rankdir: dagreDirection,     // Direction: TB, BT, LR, RL
        align: dagreAlignment,       // Alignment: UL, UR, DL, DR, or undefined
        nodesep: nodesep,            // Node spacing
        ranksep: ranksep,            // Rank spacing
        ranker: dagreRanker,         // Ranking algorithm
        marginx: 50,
        marginy: 50
      });
      g.setDefaultEdgeLabel(() => ({}));

      // Add nodes to dagre graph
      workingNodes.forEach(node => {
        g.setNode(node.id, { 
          width: node.width || defaultNodeWidth, 
          height: node.height || nodeHeaderHeight 
        });
      });

      // Add edges to dagre graph (source/target are always strings now)
      workingLinks.forEach(link => {
        g.setEdge(link.source as string, link.target as string);
      });

      // Run dagre layout
      dagre.layout(g);

      // Apply computed positions to nodes
      workingNodes.forEach(node => {
        const dagreNode = g.node(node.id);
        if (dagreNode) {
          node.x = dagreNode.x;
          node.y = dagreNode.y;
          // Lock positions (no physics simulation)
          (node as any).fx = dagreNode.x;
          (node as any).fy = dagreNode.y;
        }
      });
      
      // No simulation needed - positions are fixed
      simulation = null;
      
    } else {
      // Force simulation for layout (original scattered physics-based layout)
      simulation = d3.forceSimulation(workingNodes as any)
        .force('link', d3.forceLink(workingLinks)
          .id((d: any) => d.id)
          .distance(250)
          .strength(0.5))
        .force('charge', d3.forceManyBody().strength(-1000))
        .force('center', d3.forceCenter(width / 2, height / 2))
        .force('collision', d3.forceCollide().radius(150));
    }

    // Create arrow markers for different relationship types with highlighted versions
    const defs = g.append('defs');

    // Define arrow types with both normal and highlighted versions
    const arrowTypes = [
      { id: 'inheritance', path: 'M0,0 L0,6 L9,3 z', fill: 'white', stroke: '#172a59', strokeWidth: 1.5, size: [10, 10, 9, 3] },
      { id: 'composition', path: 'M0,3 L6,0 L12,3 L6,6 z', fill: '#172a59', stroke: '', strokeWidth: 0, size: [12, 12, 11, 3] },
      { id: 'aggregation', path: 'M0,3 L6,0 L12,3 L6,6 z', fill: 'white', stroke: '#172a59', strokeWidth: 1.5, size: [12, 12, 11, 3] },
      { id: 'association', path: 'M0,0 L0,6 L9,3 z', fill: '#172a59', stroke: '', strokeWidth: 0, size: [10, 10, 9, 3] },
    ];

    arrowTypes.forEach(arrow => {
      // Normal state arrow (semi-transparent)
      const marker = defs.append('marker')
        .attr('id', `arrow-${arrow.id}`)
        .attr('markerWidth', arrow.size[0])
        .attr('markerHeight', arrow.size[1])
        .attr('refX', arrow.size[2])
        .attr('refY', arrow.size[3])
        .attr('orient', 'auto');
      
      const path = marker.append('path')
        .attr('d', arrow.path)
        .attr('fill', arrow.fill)
        .attr('opacity', 0.8);
      
      if (arrow.stroke) {
        path.attr('stroke', arrow.stroke).attr('stroke-width', arrow.strokeWidth);
      }

      // Highlighted state arrow (fully opaque, larger)
      const highlightMarker = defs.append('marker')
        .attr('id', `arrow-${arrow.id}-highlight`)
        .attr('markerWidth', arrow.size[0] * 1.3)
        .attr('markerHeight', arrow.size[1] * 1.3)
        .attr('refX', arrow.size[2])
        .attr('refY', arrow.size[3])
        .attr('orient', 'auto');
      
      const highlightPath = highlightMarker.append('path')
        .attr('d', arrow.path)
        .attr('fill', arrow.fill)
        .attr('opacity', 1);
      
      if (arrow.stroke) {
        highlightPath.attr('stroke', arrow.stroke).attr('stroke-width', arrow.strokeWidth * 1.2);
      }
    });

    // Initialize link state on working copies (parser now handles bidirectional detection)
    workingLinks.forEach(link => {
      // Keep bidirectional and isReversed from parser, with defaults
      link.bidirectional = link.bidirectional || false;
      link.isReversed = link.isReversed || false;
    });
    
    // Debug: Log node and link counts
    const bidirectionalCount = workingLinks.filter(l => l.bidirectional).length;
    console.log(`[UMLVisualization] Nodes: ${workingNodes.length}, Total links: ${workingLinks.length}, Bidirectional: ${bidirectionalCount}`);
    
    // Debug: Check if nodes have positions
    const nodesWithPositions = workingNodes.filter(n => n.x !== undefined && n.y !== undefined).length;
    console.log(`[UMLVisualization] Nodes with positions: ${nodesWithPositions}/${workingNodes.length}`);

    // Draw links first (edges between nodes)
    const links = g.append('g')
      .attr('class', 'links')
      .selectAll('g')
      .data(workingLinks)
      .join('g')
      .attr('class', 'link-group');

    links.append('line')
      .attr('class', (d) => `link link-${d.type}${d.bidirectional ? ' link-bidirectional' : ''}`)
      .attr('stroke', (d) => d.bidirectional ? '#6366f1' : '#0a3dfa')  // Indigo for bidirectional
      .attr('stroke-width', 2)
      .attr('stroke-opacity', 0.7)
      .attr('stroke-dasharray', (d) => d.bidirectional ? '5,3' : 'none')  // Dashed for bidirectional
      .attr('marker-end', (d) => {
        const arrowType = d.type === 'one-to-one' || d.type === 'one-to-many' || 
                          d.type === 'many-to-one' || d.type === 'many-to-many' 
                          ? 'association' : d.type;
        return `url(#arrow-${arrowType})`;
      })
      .style('cursor', (d) => d.bidirectional ? 'pointer' : 'default')
      .on('click', function(event, d: any) {
        // Toggle direction for bidirectional edges on left-click
        if (!d.bidirectional) return;
        
        event.stopPropagation();  // Prevent triggering svg click (deselect)
        
        // Toggle reversed state
        d.isReversed = !d.isReversed;
        
        // Swap source and target for line direction
        const temp = d.source;
        d.source = d.target;
        d.target = temp;
        
        // Update the label text to show the correct direction
        const linkIndex = workingLinks.indexOf(d);
        const labelSelection = linkLabels.filter((_: any, i: number) => i === linkIndex);
        
        // Get the appropriate label based on current direction
        const currentLabel = d.isReversed ? (d.reverseLabel || d.label) : d.label;
        const currentCardinality = d.isReversed ? (d.reverseCardinality || d.cardinality) : d.cardinality;
        let displayLabel = currentLabel || '';
        if (currentCardinality) {
          displayLabel = displayLabel ? `${displayLabel} [${currentCardinality}]` : currentCardinality;
        }
        
        labelSelection.text(displayLabel);
        
        // Visual feedback - flash the edge
        const arrowType = d.type === 'one-to-one' || d.type === 'one-to-many' || 
                          d.type === 'many-to-one' || d.type === 'many-to-many' 
                          ? 'association' : d.type;
        d3.select(this)
          .attr('marker-end', `url(#arrow-${arrowType}-highlight)`)
          .transition()
          .duration(300)
          .attr('stroke-width', 4)
          .attr('stroke', '#22c55e')  // Green flash on toggle
          .transition()
          .duration(300)
          .attr('stroke-width', 2)
          .attr('stroke', '#6366f1')  // Back to indigo
          .attr('marker-end', `url(#arrow-${arrowType})`);
        
        // Flash the label then hide it again
        labelSelection
          .transition()
          .duration(200)
          .style('opacity', 1)
          .attr('font-weight', 'bold')
          .attr('fill', '#22c55e')  // Green text on toggle
          .transition()
          .delay(500)
          .duration(300)
          .style('opacity', 0)  // Hide label again after flash
          .attr('font-weight', 'normal')
          .attr('fill', '#172a59');
        
        // Restart simulation if using force layout, or update positions for dagre
        if (simulation) {
          simulation.alpha(0.3).restart();
        } else {
          // For dagre, manually update the link positions
          updateLinkPositions();
        }
      })
      .on('mouseenter', function(_event, d: any) {
        // Highlight edge on hover
        const arrowType = d.type === 'one-to-one' || d.type === 'one-to-many' || 
                          d.type === 'many-to-one' || d.type === 'many-to-many' 
                          ? 'association' : d.type;
        
        d3.select(this)
          .transition()
          .duration(200)
          .attr('stroke-opacity', 1)
          .attr('stroke-width', 3)
          .attr('marker-end', `url(#arrow-${arrowType}-highlight)`);
        
        // Show label more prominently
        const linkIndex = workingLinks.indexOf(d);
        linkLabels.filter((_: any, i: number) => i === linkIndex)
          .transition()
          .duration(200)
          .style('opacity', 1)
          .attr('font-weight', 'bold');
        
        // Show tooltip for bidirectional edges
        if (d.bidirectional && this.parentNode) {
          d3.select(this.parentNode as Element)
            .append('title')
            .text('Click to reverse direction');
        }
      })
      .on('mouseleave', function(_event, d: any) {
        // Reset edge appearance
        const arrowType = d.type === 'one-to-one' || d.type === 'one-to-many' || 
                          d.type === 'many-to-one' || d.type === 'many-to-many' 
                          ? 'association' : d.type;
        
        d3.select(this)
          .transition()
          .duration(200)
          .attr('stroke-opacity', 0.7)
          .attr('stroke-width', 2)
          .attr('marker-end', `url(#arrow-${arrowType})`);
        
        // Reset label - hide it again
        const linkIndex = workingLinks.indexOf(d);
        linkLabels.filter((_: any, i: number) => i === linkIndex)
          .transition()
          .duration(200)
          .style('opacity', 0)  // Hide label again
          .attr('font-weight', 'normal');
        
        // Remove tooltip
        if (this.parentNode) {
          d3.select(this.parentNode as Element).select('title').remove();
        }
      });

    // Add link labels with background for readability
    // First add background rects (will be positioned in updateLinkPositions)
    const linkLabelBackgrounds = links.append('rect')
      .attr('class', 'link-label-bg')
      .attr('rx', 3)
      .attr('ry', 3)
      .attr('fill', 'white')
      .attr('stroke', '#e5e7eb')
      .attr('stroke-width', 1)
      .style('opacity', 0)  // HIDDEN by default
      .style('pointer-events', 'none');
    
    // Add link labels - HIDDEN by default (opacity: 0), shown on hover to prevent overlap
    const linkLabels = links.append('text')
      .attr('class', (d) => `link-label${d.bidirectional ? ' link-label-bidirectional' : ''}`)
      .attr('text-anchor', 'middle')
      .attr('dy', -5)
      .attr('fill', '#172a59')
      .attr('font-size', '11px')
      .attr('font-weight', '500')
      .style('opacity', 0)  // HIDDEN by default - prevents label overlap
      .style('pointer-events', 'none')  // Don't interfere with click events
      .text((d) => {
        // Show label based on current direction (isReversed state)
        const currentLabel = d.isReversed ? (d.reverseLabel || d.label) : d.label;
        const currentCardinality = d.isReversed ? (d.reverseCardinality || d.cardinality) : d.cardinality;
        let label = currentLabel || '';
        if (currentCardinality) {
          label = label ? `${label} [${currentCardinality}]` : currentCardinality;
        }
        return label;
      });

    // Draw nodes (on top of links so boxes appear above edge lines)
    const nodes = g.append('g')
      .attr('class', 'nodes')
      .selectAll('g')
      .data(workingNodes)
      .join('g')
      .attr('class', (d) => `node node-${d.type}`)
      .call(d3.drag<any, any>()
        .on('start', dragstarted)
        .on('drag', dragged)
        .on('end', dragended) as any);

    // Node background
    nodes.append('rect')
      .attr('class', 'node-rect')
      .attr('width', (d) => d.width || defaultNodeWidth)
      .attr('height', (d) => d.height || nodeHeaderHeight)
      .attr('rx', 8)
      .attr('fill', 'white')
      .attr('stroke', (d) => d.type === 'enum' ? '#ffc107' : '#0a3dfa')
      .attr('stroke-width', 2)
      .on('click', (event, d) => {
        event.stopPropagation();
        setSelectedNode(d);
      });

    // Node header background
    nodes.append('rect')
      .attr('class', 'node-header')
      .attr('width', (d) => d.width || defaultNodeWidth)
      .attr('height', nodeHeaderHeight)
      .attr('rx', 8)
      .attr('fill', (d) => d.type === 'enum' ? '#ffc107' : '#0a3dfa')
      .attr('opacity', 0.1);

    // Node name
    nodes.append('text')
      .attr('class', 'node-name')
      .attr('x', (d) => (d.width || defaultNodeWidth) / 2)
      .attr('y', nodeHeaderHeight / 2)
      .attr('text-anchor', 'middle')
      .attr('dominant-baseline', 'middle')
      .attr('fill', '#172a59')
      .attr('font-weight', 'bold')
      .attr('font-size', '14px')
      .text((d) => d.name);

    // Node type badge
    nodes.append('text')
      .attr('class', 'node-type')
      .attr('x', 8)
      .attr('y', 12)
      .attr('fill', '#666')
      .attr('font-size', '10px')
      .attr('font-style', 'italic')
      .text((d) => `«${d.type}»`);

    // Draw attributes section
    nodes.each(function(d) {
      if (!d.attributes || d.attributes.length === 0) return;

      const nodeGroup = d3.select(this);
      let yOffset = nodeHeaderHeight + nodePadding;

      // Attributes divider
      nodeGroup.append('line')
        .attr('x1', 0)
        .attr('y1', nodeHeaderHeight)
        .attr('x2', d.width || defaultNodeWidth)
        .attr('y2', nodeHeaderHeight)
        .attr('stroke', '#0a3dfa')
        .attr('stroke-width', 1);

      // Attribute entries
      d.attributes.forEach((attr, i) => {
        nodeGroup.append('text')
          .attr('class', 'node-attribute')
          .attr('x', 10)
          .attr('y', yOffset + i * attributeHeight)
          .attr('fill', '#172a59')
          .attr('font-size', '12px')
          .attr('font-family', "'Monaco', 'Courier New', monospace")
          .text(`${attr.name}: ${attr.type}`);
      });

      yOffset += d.attributes.length * attributeHeight;
    });

    // Draw methods section
    nodes.each(function(d) {
      if (!d.methods || d.methods.length === 0) return;

      const nodeGroup = d3.select(this);
      const attributeCount = d.attributes?.length || 0;
      let yOffset = nodeHeaderHeight + nodePadding +
                    (attributeCount > 0 ? attributeCount * attributeHeight + nodePadding : 0);

      // Methods divider
      nodeGroup.append('line')
        .attr('x1', 0)
        .attr('y1', yOffset - nodePadding)
        .attr('x2', d.width || defaultNodeWidth)
        .attr('y2', yOffset - nodePadding)
        .attr('stroke', '#0a3dfa')
        .attr('stroke-width', 1);

      // Method entries
      d.methods.forEach((method, i) => {
        const methodText = method.returnType 
          ? `${method.name}(): ${method.returnType}`
          : `${method.name}()`;
          
        nodeGroup.append('text')
          .attr('class', 'node-method')
          .attr('x', 10)
          .attr('y', yOffset + i * methodHeight)
          .attr('fill', '#172a59')
          .attr('font-size', '12px')
          .attr('font-family', "'Monaco', 'Courier New', monospace")
          .text(methodText);
      });
    });

    // Shared function to update all link positions - ensures edges stay attached to box borders
    const updateLinkPositions = () => {
      // Build node lookup map for O(1) access
      const nodeMap = new Map<string, UMLNode>();
      workingNodes.forEach(n => nodeMap.set(n.id, n));
      
      links.each(function(linkData: any) {
        // Get source and target nodes - handle both string IDs and object references
        let source: UMLNode | undefined;
        let target: UMLNode | undefined;
        
        if (typeof linkData.source === 'string') {
          source = nodeMap.get(linkData.source);
          target = nodeMap.get(linkData.target as string);
        } else {
          // Force simulation converts source/target to node objects
          source = linkData.source as UMLNode;
          target = linkData.target as UMLNode;
        }
        
        if (!source || !target || source.x === undefined || target.x === undefined) return;
        
        const sourceW = source.width || defaultNodeWidth;
        const sourceH = source.height || nodeHeaderHeight;
        const targetW = target.width || defaultNodeWidth;
        const targetH = target.height || nodeHeaderHeight;
        
        // Calculate border intersections once
        const sourceIntersection = getBoxBorderIntersection(
          source.x, source.y!, sourceW, sourceH, target.x, target.y!
        );
        const targetIntersection = getBoxBorderIntersection(
          target.x, target.y!, targetW, targetH, source.x, source.y!
        );
        
        // Update line - set all attributes together for consistency
        const linkGroup = d3.select(this);
        linkGroup.select('line')
          .attr('x1', sourceIntersection.x)
          .attr('y1', sourceIntersection.y)
          .attr('x2', targetIntersection.x)
          .attr('y2', targetIntersection.y);
        
        // Update label position at midpoint
        const midX = (source.x + target.x) / 2;
        const midY = (source.y! + target.y!) / 2;
        
        const textElement = linkGroup.select('text');
        textElement
          .attr('x', midX)
          .attr('y', midY);
        
        // Update background rect position and size based on text bounds
        const textNode = textElement.node() as SVGTextElement | null;
        if (textNode) {
          const bbox = textNode.getBBox();
          const padding = 4;
          linkGroup.select('.link-label-bg')
            .attr('x', bbox.x - padding)
            .attr('y', bbox.y - padding)
            .attr('width', bbox.width + padding * 2)
            .attr('height', bbox.height + padding * 2);
        }
      });
    };
    
    // Shared function to update all node positions
    const updateNodePositions = () => {
      nodes.attr('transform', (d: any) => 
        `translate(${d.x - (d.width || defaultNodeWidth) / 2}, ${d.y - (d.height || nodeHeaderHeight) / 2})`
      );
    };

    // Update positions on tick (force simulation) or immediately (dagre)
    if (simulation) {
      simulation.on('tick', () => {
        updateNodePositions();
        updateLinkPositions();
      });
      
      // Auto-fit after simulation settles (for new diagrams only)
      if (isNewDiagram) {
        simulation.on('end', () => {
          // Delay slightly to ensure DOM is fully updated
          setTimeout(() => handleFitToScreen(), 100);
        });
      }
    } else {
      // Dagre layout - positions are already computed, update immediately
      updateNodePositions();
      updateLinkPositions();
      
      // Auto-fit to screen for new diagrams (delay to ensure DOM is ready)
      if (isNewDiagram) {
        setTimeout(() => handleFitToScreen(), 100);
      }
    }

    // Drag functions
    function dragstarted(event: any, d: any) {
      if (simulation && !event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

    function dragged(this: SVGGElement, event: any, d: any) {
      // Update position data immediately
      d.fx = event.x;
      d.fy = event.y;
      d.x = event.x;
      d.y = event.y;
      
      // Update THIS node's transform directly and synchronously (no delay)
      d3.select(this).attr('transform', 
        `translate(${d.x - (d.width || defaultNodeWidth) / 2}, ${d.y - (d.height || nodeHeaderHeight) / 2})`
      );
      
      // Always update links immediately during drag for perfect synchronization
      // (Force simulation tick will also update, but we need immediate response)
      updateLinkPositions();
    }

    function dragended(event: any, d: any) {
      if (simulation && !event.active) simulation.alphaTarget(0);
      // Keep fixed position after drag
      d.fx = d.x;
      d.fy = d.y;
    }

    // Click outside to deselect
    svg.on('click', () => setSelectedNode(null));

    // Cleanup
    return () => {
      if (simulation) simulation.stop();
      cleanup();
    };
  }, [diagram, width, height, layoutType, dagreDirection, dagreAlignment, dagreRanker]);

  return (
    <div className="uml-visualization" ref={containerRef}>
      <div className="uml-visualization__header">
        {diagram.title && <h3 className="uml-visualization__title">{diagram.title}</h3>}
        <div className="uml-visualization__controls">
          <span className="uml-visualization__zoom">Zoom: {(zoom * 100).toFixed(0)}%</span>
          <button 
            className="uml-visualization__button"
            onClick={() => {
              const svg = d3.select(svgRef.current);
              svg.transition().duration(750).call(
                d3.zoom<SVGSVGElement, unknown>().transform as any,
                d3.zoomIdentity
              );
            }}
          >
            Reset View
          </button>
        </div>
      </div>
      
      <div className="uml-visualization__canvas">
        <svg ref={svgRef} />
      </div>

      {selectedNode && (
        <div className="uml-visualization__details">
          <div className="uml-visualization__details-header">
            <h4>{selectedNode.name}</h4>
            <button 
              className="uml-visualization__close"
              onClick={() => setSelectedNode(null)}
            >
              ×
            </button>
          </div>
          <div className="uml-visualization__details-content">
            <p><strong>Type:</strong> {selectedNode.type}</p>
            
            {selectedNode.attributes && selectedNode.attributes.length > 0 && (
              <>
                <p><strong>Attributes ({selectedNode.attributes.length}):</strong></p>
                <ul>
                  {selectedNode.attributes.map((attr, i) => (
                    <li key={i}>{attr.name}: {attr.type}</li>
                  ))}
                </ul>
              </>
            )}

            {selectedNode.methods && selectedNode.methods.length > 0 && (
              <>
                <p><strong>Methods ({selectedNode.methods.length}):</strong></p>
                <ul>
                  {selectedNode.methods.map((method, i) => (
                    <li key={i}>
                      {method.name}(){method.returnType ? `: ${method.returnType}` : ''}
                    </li>
                  ))}
                </ul>
              </>
            )}
          </div>
        </div>
      )}
    </div>
  );
};
