/**
 * FlowCanvas - Workflow graph canvas with nodes and connection lines.
 * Nodes have 4 connection handles (one per side), drag nodes to add, connect with arrows.
 */

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { NodeType } from '../../../api/workflowApi';
import styles from './Automation2FlowEditor.module.css';

import { useLanguage } from '../../../providers/language/LanguageContext';

export interface CanvasNode {
  id: string;
  type: string;
  x: number;
  y: number;
  label?: string;
  title?: string;
  comment?: string;
  color?: string;
  inputs: number;
  outputs: number;
  parameters?: Record<string, unknown>;
  inputPorts?: Array<{ name: string; schema: string; accepts?: string[] }>;
  outputPorts?: Array<{ name: string; schema: string }>;
}

export interface CanvasConnection {
  id: string;
  sourceId: string;
  sourceHandle: number;
  targetId: string;
  targetHandle: number;
}

const NODE_WIDTH = 200;
const NODE_HEIGHT = 72;
const HANDLE_SIZE = 12;
const HANDLE_OFFSET = HANDLE_SIZE / 2;
const LAYOUT_V_GAP = 80;
const LAYOUT_H_GAP = 60;

/**
 * Topological-sort based auto-layout: arranges nodes top-to-bottom in layers.
 * Disconnected nodes are appended as extra roots.
 */
export function computeAutoLayout(
  nodes: CanvasNode[],
  connections: CanvasConnection[],
): CanvasNode[] {
  if (nodes.length === 0) return nodes;

  const inDegree = new Map<string, number>();
  const children = new Map<string, string[]>();
  for (const n of nodes) {
    inDegree.set(n.id, 0);
    children.set(n.id, []);
  }
  for (const c of connections) {
    inDegree.set(c.targetId, (inDegree.get(c.targetId) ?? 0) + 1);
    children.get(c.sourceId)?.push(c.targetId);
  }

  const layers: string[][] = [];
  const layerOf = new Map<string, number>();
  const queue: string[] = [];
  for (const n of nodes) {
    if ((inDegree.get(n.id) ?? 0) === 0) queue.push(n.id);
  }

  while (queue.length > 0) {
    const batch: string[] = [...queue];
    queue.length = 0;
    const layerIdx = layers.length;
    layers.push(batch);
    for (const id of batch) {
      layerOf.set(id, layerIdx);
      for (const childId of children.get(id) ?? []) {
        const deg = (inDegree.get(childId) ?? 1) - 1;
        inDegree.set(childId, deg);
        if (deg === 0) queue.push(childId);
      }
    }
  }

  const placed = new Set(layerOf.keys());
  for (const n of nodes) {
    if (!placed.has(n.id)) {
      const layerIdx = layers.length;
      layers.push([n.id]);
      layerOf.set(n.id, layerIdx);
    }
  }

  const startX = 40;
  const startY = 40;

  return nodes.map((n) => {
    const layer = layerOf.get(n.id) ?? 0;
    const siblings = layers[layer];
    const idxInLayer = siblings.indexOf(n.id);
    return {
      ...n,
      x: startX + idxInLayer * (NODE_WIDTH + LAYOUT_H_GAP),
      y: startY + layer * (NODE_HEIGHT + LAYOUT_V_GAP),
    };
  });
}

/** Soft port compatibility check: returns 'ok' | 'warning' | 'error' */
function _checkConnectionCompatibility(
  sourceNode: CanvasNode,
  sourceOutputIdx: number,
  targetNode: CanvasNode,
  targetInputIdx: number,
  nodeTypes: NodeType[],
): 'ok' | 'warning' {
  const srcType = nodeTypes.find((nt) => nt.id === sourceNode.type);
  const tgtType = nodeTypes.find((nt) => nt.id === targetNode.type);
  if (!srcType?.outputPorts || !tgtType?.inputPorts) return 'ok';

  const srcPort = srcType.outputPorts[sourceOutputIdx];
  const tgtPort = tgtType.inputPorts[targetInputIdx];
  if (!srcPort || !tgtPort) return 'ok';

  const srcSchema = srcPort.schema;
  const accepts = tgtPort.accepts;
  if (!accepts || accepts.length === 0) return 'ok';
  if (accepts.includes('Transit')) return 'ok';
  if (accepts.includes(srcSchema)) return 'ok';
  return 'warning';
}

interface FlowCanvasProps {
  nodes: CanvasNode[];
  connections: CanvasConnection[];
  nodeTypes: NodeType[];
  onNodesChange: (nodes: CanvasNode[]) => void;
  onConnectionsChange: (connections: CanvasConnection[]) => void;
  onDropNodeType: (nodeTypeId: string, x: number, y: number) => void;
  getLabel: (node: CanvasNode) => string;
  getCategoryIcon: (category: string) => React.ReactNode;
  onSelectionChange?: (node: CanvasNode | null) => void;
  highlightedNodeIds?: Record<string, string>;
}

const HIGHLIGHT_COLORS: Record<string, string> = {
  running: '#f0ad4e',
  completed: '#28a745',
  failed: '#dc3545',
  skipped: '#6c757d',
};

export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
  connections,
  nodeTypes,
  onNodesChange,
  onConnectionsChange,
  onDropNodeType,
  getLabel,
  getCategoryIcon,
  onSelectionChange,
  highlightedNodeIds,
}) => {
  const { t } = useLanguage();
  const containerRef = useRef<HTMLDivElement>(null);
  const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set());
  const selectedNodeId = selectedNodeIds.size === 1 ? [...selectedNodeIds][0] : null;
  const [selectedConnectionId, setSelectedConnectionId] = useState<string | null>(null);
  const [connectionWarnings, setConnectionWarnings] = useState<Record<string, boolean>>({});
  const [selectionBox, setSelectionBox] = useState<{
    startX: number;
    startY: number;
    endX: number;
    endY: number;
  } | null>(null);
  const [editingNodeId, setEditingNodeId] = useState<string | null>(null);
  const [editingField, setEditingField] = useState<'title' | null>(null);
  const [connectingFrom, setConnectingFrom] = useState<{
    nodeId: string;
    handleIndex: number;
    x: number;
    y: number;
  } | null>(null);
  const [dragPos, setDragPos] = useState<{ x: number; y: number } | null>(null);
  const [draggingNodeId, setDraggingNodeId] = useState<string | null>(null);
  const [dragOffset, setDragOffset] = useState({
    startClientX: 0,
    startClientY: 0,
    nodesInitial: {} as Record<string, { x: number; y: number }>,
  });
  const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);
  const [panning, setPanning] = useState<{
    startX: number;
    startY: number;
    startPanX: number;
    startPanY: number;
  } | null>(null);

  const nodeTypeMap = useMemo(() => {
    const m: Record<string, NodeType> = {};
    nodeTypes.forEach((nt) => {
      m[nt.id] = nt;
    });
    return m;
  }, [nodeTypes]);

  useEffect(() => {
    if (onSelectionChange) {
      const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null;
      onSelectionChange(node);
    }
  }, [selectedNodeId, nodes, onSelectionChange]);

  const handleConnectionClick = useCallback((e: React.MouseEvent, connId: string) => {
    e.stopPropagation();
    setSelectedConnectionId(connId);
    setSelectedNodeIds(new Set());
  }, []);

  const handleDeleteConnection = useCallback(() => {
    if (!selectedConnectionId) return;
    onConnectionsChange(connections.filter((c) => c.id !== selectedConnectionId));
    setSelectedConnectionId(null);
  }, [selectedConnectionId, connections, onConnectionsChange]);

  const getHandlePosition = useCallback(
    (node: CanvasNode, handleIndex: number): { x: number; y: number; side: string } => {
      const isOutput = handleIndex >= node.inputs;
      const ioIndex = isOutput ? handleIndex - node.inputs : handleIndex;
      const ioCount = isOutput ? node.outputs : node.inputs;

      const w = NODE_WIDTH;
      const h = NODE_HEIGHT;
      const centerX = node.x + w / 2;

      if (isOutput) {
        if (ioCount === 1) return { x: centerX, y: node.y + h, side: 'bottom' };
        const step = w / (ioCount + 1);
        return { x: node.x + step * (ioIndex + 1), y: node.y + h, side: 'bottom' };
      } else {
        if (ioCount === 1) return { x: centerX, y: node.y, side: 'top' };
        const step = w / (ioCount + 1);
        return { x: node.x + step * (ioIndex + 1), y: node.y, side: 'top' };
      }
    },
    []
  );

  const getUsedTargetHandles = useMemo(() => {
    const used = new Set<string>();
    connections.forEach((c) => used.add(`${c.targetId}-${c.targetHandle}`));
    return used;
  }, [connections]);

  const handleDrop = useCallback(
    (e: React.DragEvent) => {
      e.preventDefault();
      const raw = e.dataTransfer.getData('application/json');
      if (!raw || !containerRef.current) return;
      try {
        const { type } = JSON.parse(raw);
        const el = containerRef.current;
        const rect = el.getBoundingClientRect();
        const x = (e.clientX - rect.left - panOffset.x) / zoom - NODE_WIDTH / 2;
        const y = (e.clientY - rect.top - panOffset.y) / zoom - NODE_HEIGHT / 2;
        onDropNodeType(type, Math.max(0, x), Math.max(0, y));
      } catch (_) {}
    },
    [onDropNodeType, panOffset, zoom]
  );

  const handleHandleMouseDown = useCallback(
    (e: React.MouseEvent, nodeId: string, handleIndex: number, isOutput: boolean) => {
      e.stopPropagation();
      if (!isOutput) return;
      const node = nodes.find((n) => n.id === nodeId);
      if (!node) return;
      const pos = getHandlePosition(node, handleIndex);
      setConnectingFrom({ nodeId, handleIndex, x: pos.x, y: pos.y });
      setDragPos({ x: e.clientX, y: e.clientY });
    },
    [nodes, getHandlePosition]
  );

  const handleHandleMouseUp = useCallback(
    (e: React.MouseEvent, targetNodeId: string, targetHandleIndex: number) => {
      e.stopPropagation();
      const targetNode = nodes.find((n) => n.id === targetNodeId);
      if (!targetNode || targetHandleIndex >= targetNode.inputs) return;

      if (selectedConnectionId) {
        const sel = connections.find((c) => c.id === selectedConnectionId);
        if (sel) {
          const key = `${targetNodeId}-${targetHandleIndex}`;
          const currentTargetKey = `${sel.targetId}-${sel.targetHandle}`;
          if (key === currentTargetKey) {
            setSelectedConnectionId(null);
            return;
          }
          if (getUsedTargetHandles.has(key)) {
            setSelectedConnectionId(null);
            return;
          }
          onConnectionsChange(
            connections.map((c) =>
              c.id === selectedConnectionId
                ? { ...c, targetId: targetNodeId, targetHandle: targetHandleIndex }
                : c
            )
          );
          setSelectedConnectionId(null);
        }
        return;
      }

      if (!connectingFrom || connectingFrom.nodeId === targetNodeId) {
        setConnectingFrom(null);
        setDragPos(null);
        return;
      }
      const key = `${targetNodeId}-${targetHandleIndex}`;
      if (getUsedTargetHandles.has(key)) {
        setConnectingFrom(null);
        setDragPos(null);
        return;
      }
      const newConn: CanvasConnection = {
        id: `c_${Date.now()}`,
        sourceId: connectingFrom.nodeId,
        sourceHandle: connectingFrom.handleIndex,
        targetId: targetNodeId,
        targetHandle: targetHandleIndex,
      };

      const srcNode = nodes.find((n) => n.id === connectingFrom.nodeId);
      const tgtNode = nodes.find((n) => n.id === targetNodeId);
      if (srcNode && tgtNode) {
        const sourceOutputIdx = connectingFrom.handleIndex >= srcNode.inputs
          ? connectingFrom.handleIndex - srcNode.inputs : 0;
        const compat = _checkConnectionCompatibility(srcNode, sourceOutputIdx, tgtNode, targetHandleIndex, nodeTypes);
        if (compat === 'warning') {
          setConnectionWarnings((prev) => ({ ...prev, [newConn.id]: true }));
        }
      }

      onConnectionsChange([...connections, newConn]);
      setConnectingFrom(null);
      setDragPos(null);
    },
    [connectingFrom, connections, nodes, getUsedTargetHandles, onConnectionsChange, selectedConnectionId]
  );

  React.useEffect(() => {
    if (!connectingFrom || !dragPos) return;
    const onMove = (e: MouseEvent) => setDragPos({ x: e.clientX, y: e.clientY });
    const onUp = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      if (target.closest(`.${styles.handleOutput}`)) {
        return;
      }
      if (target.closest(`.${styles.handleInput}`)) {
        return;
      }
      setConnectingFrom(null);
      setDragPos(null);
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [connectingFrom, dragPos]);

  const handleNodeMouseDown = useCallback(
    (e: React.MouseEvent, nodeId: string) => {
      const node = nodes.find((n) => n.id === nodeId);
      if (!node) return;

      const idsToMove = selectedNodeIds.has(nodeId)
        ? selectedNodeIds
        : new Set([nodeId]);
      if (!selectedNodeIds.has(nodeId)) {
        setSelectedNodeIds(idsToMove);
      }

      const nodesInitial: Record<string, { x: number; y: number }> = {};
      nodes.forEach((n) => {
        if (idsToMove.has(n.id)) nodesInitial[n.id] = { x: n.x, y: n.y };
      });

      setDraggingNodeId(nodeId);
      setDragOffset({
        startClientX: e.clientX,
        startClientY: e.clientY,
        nodesInitial,
      });
    },
    [nodes, selectedNodeIds]
  );

  React.useEffect(() => {
    if (!draggingNodeId) return;
    const onMove = (e: MouseEvent) => {
      const dx = (e.clientX - dragOffset.startClientX) / zoom;
      const dy = (e.clientY - dragOffset.startClientY) / zoom;
      onNodesChange(
        nodes.map((n) => {
          const init = dragOffset.nodesInitial[n.id];
          if (!init) return n;
          return { ...n, x: init.x + dx, y: init.y + dy };
        })
      );
    };
    const onUp = () => setDraggingNodeId(null);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [draggingNodeId, dragOffset, nodes, onNodesChange, zoom]);

  const [containerBounds, setContainerBounds] = useState({ left: 0, top: 0 });
  React.useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const update = () => {
      const r = el.getBoundingClientRect();
      setContainerBounds({ left: r.left, top: r.top });
    };
    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  const clientToCanvas = useCallback(
    (clientX: number, clientY: number) => ({
      x: (clientX - containerBounds.left - panOffset.x) / zoom,
      y: (clientY - containerBounds.top - panOffset.y) / zoom,
    }),
    [containerBounds, panOffset, zoom]
  );

  const handleCanvasMouseDown = useCallback(
    (e: React.MouseEvent) => {
      const hitNode = (e.target as HTMLElement).closest(`.${styles.canvasNode}`);
      if (hitNode || connectingFrom) return;
      if (e.shiftKey) {
        e.preventDefault();
        const pt = clientToCanvas(e.clientX, e.clientY);
        setSelectionBox({ startX: pt.x, startY: pt.y, endX: pt.x, endY: pt.y });
        setSelectedNodeIds(new Set());
        setSelectedConnectionId(null);
      } else {
        setPanning({
          startX: e.clientX,
          startY: e.clientY,
          startPanX: panOffset.x,
          startPanY: panOffset.y,
        });
      }
    },
    [connectingFrom, panOffset, clientToCanvas]
  );

  const handleWheel = useCallback((e: WheelEvent) => {
    e.preventDefault();
    const delta = e.deltaY > 0 ? -0.1 : 0.1;
    setZoom((z) => Math.min(2, Math.max(0.25, z + delta)));
  }, []);

  React.useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    el.addEventListener('wheel', handleWheel, { passive: false });
    return () => el.removeEventListener('wheel', handleWheel);
  }, [handleWheel]);

  React.useEffect(() => {
    if (!panning) return;
    const onMove = (e: MouseEvent) => {
      setPanOffset({
        x: panning.startPanX + (e.clientX - panning.startX),
        y: panning.startPanY + (e.clientY - panning.startY),
      });
    };
    const onUp = () => setPanning(null);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [panning]);

  const selectionBoxRef = useRef<typeof selectionBox>(null);
  const marqueeJustEndedRef = useRef(false);
  selectionBoxRef.current = selectionBox;

  React.useEffect(() => {
    if (!selectionBox) return;
    const onMove = (e: MouseEvent) => {
      const pt = clientToCanvas(e.clientX, e.clientY);
      setSelectionBox((prev) =>
        prev ? { ...prev, endX: pt.x, endY: pt.y } : null
      );
    };
    const onUp = () => {
      const box = selectionBoxRef.current;
      setSelectionBox(null);
      marqueeJustEndedRef.current = true;
      if (!box) return;
      const minX = Math.min(box.startX, box.endX);
      const maxX = Math.max(box.startX, box.endX);
      const minY = Math.min(box.startY, box.endY);
      const maxY = Math.max(box.startY, box.endY);
      const ids = new Set<string>();
      nodes.forEach((n) => {
        const nx = n.x;
        const ny = n.y;
        const nRight = nx + NODE_WIDTH;
        const nBottom = ny + NODE_HEIGHT;
        const overlaps =
          nx < maxX && nRight > minX && ny < maxY && nBottom > minY;
        if (overlaps) ids.add(n.id);
      });
      setSelectedNodeIds(ids);
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [selectionBox, nodes, clientToCanvas]);

  const CANVAS_SIZE = 8000;
  const svgBounds = useMemo(() => {
    if (nodes.length === 0) return { width: CANVAS_SIZE, height: CANVAS_SIZE };
    let maxX = 0, maxY = 0;
    nodes.forEach((n) => {
      maxX = Math.max(maxX, n.x + NODE_WIDTH + 200);
      maxY = Math.max(maxY, n.y + NODE_HEIGHT + 200);
    });
    return { width: Math.max(maxX, CANVAS_SIZE), height: Math.max(maxY, CANVAS_SIZE) };
  }, [nodes]);

  const handleDeleteNode = useCallback(() => {
    if (selectedNodeIds.size === 0) return;
    const ids = selectedNodeIds;
    onNodesChange(nodes.filter((n) => !ids.has(n.id)));
    onConnectionsChange(
      connections.filter(
        (c) => !ids.has(c.sourceId) && !ids.has(c.targetId)
      )
    );
    setSelectedNodeIds(new Set());
    setEditingNodeId(null);
    setEditingField(null);
  }, [selectedNodeIds, nodes, connections, onNodesChange, onConnectionsChange]);

  React.useEffect(() => {
    const onKeyDown = (e: KeyboardEvent) => {
      const target = e.target as HTMLElement;
      if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
      if (e.key === 'Escape') {
        setConnectingFrom(null);
        setDragPos(null);
        setSelectedConnectionId(null);
      }
      if (e.key === 'Delete' || e.key === 'Backspace') {
        if (selectedConnectionId) {
          e.preventDefault();
          handleDeleteConnection();
        } else if (selectedNodeIds.size > 0) {
          e.preventDefault();
          handleDeleteNode();
        }
      }
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [handleDeleteNode, handleDeleteConnection, selectedNodeIds.size, selectedConnectionId]);

  const handleNodeUpdate = useCallback(
    (nodeId: string, updates: Partial<Pick<CanvasNode, 'title' | 'comment'>>) => {
      onNodesChange(
        nodes.map((n) => (n.id === nodeId ? { ...n, ...updates } : n))
      );
    },
    [nodes, onNodesChange]
  );

  return (
    <div
      ref={containerRef}
      className={`${styles.canvasDropZone} ${panning ? styles.canvasPanning : styles.canvasGrab} ${selectionBox || draggingNodeId ? styles.canvasSelecting : ''}`}
      style={{
        backgroundSize: `${20 * zoom}px ${20 * zoom}px`,
        backgroundPosition: `${-panOffset.x}px ${-panOffset.y}px`,
      }}
      onDragOver={(e) => e.preventDefault()}
      onDrop={handleDrop}
      onMouseDown={handleCanvasMouseDown}
      tabIndex={0}
      onClick={() => {
        if (marqueeJustEndedRef.current) {
          marqueeJustEndedRef.current = false;
          return;
        }
        setSelectedNodeIds(new Set());
        setSelectedConnectionId(null);
        setConnectingFrom(null);
        setDragPos(null);
      }}
    >
      {selectedNodeIds.size > 1 && !selectedConnectionId && !connectingFrom && (
        <div className={styles.connectionHint}>
          {selectedNodeIds.size} {t('Knoten ausgewählt')}
          {' · '}
          <kbd>Entf</kbd> {t('zum Löschen')}
          {' · '}
          {t('Ziehen zum Verschieben')}
          {' · '}
          <kbd>Shift</kbd>
          {t('+Klick zum Hinzufügen oder Entfernen')}
        </div>
      )}
      {connectingFrom && !selectedConnectionId && (
        <div className={styles.connectionHint}>
          {t('Ziehen Sie zum Eingang oder klicken Sie auf einen Eingang')}
          {' · '}
          <kbd>Esc</kbd> {t('zum Abbrechen')}
        </div>
      )}
      {selectedConnectionId && (
        <div className={styles.connectionHint}>
          {t('Verbindungspfeil ausgewählt')}
          {' · '}
          <kbd>Entf</kbd> {t('zum Löschen')}
          {' · '}
          {t('Anderen Eingang anklicken zum Umleiten')}
        </div>
      )}
      <div
        className={styles.canvasContent}
        style={{
          width: svgBounds.width,
          height: svgBounds.height,
          transform: `translate(${panOffset.x}px, ${panOffset.y}px) scale(${zoom})`,
          transformOrigin: '0 0',
        }}
      >
      <svg
        className={styles.connectionsLayer}
        width={svgBounds.width}
        height={svgBounds.height}
        style={{ position: 'absolute', left: 0, top: 0 }}
      >
        <defs>
          <marker
            id="arrowhead"
            markerWidth="10"
            markerHeight="7"
            refX="9"
            refY="3.5"
            orient="auto"
          >
            <polygon points="0 0, 10 3.5, 0 7" fill="var(--text-secondary, #666)" />
          </marker>
          <marker
            id="arrowhead-selected"
            markerWidth="10"
            markerHeight="7"
            refX="9"
            refY="3.5"
            orient="auto"
          >
            <polygon points="0 0, 10 3.5, 0 7" fill="var(--primary-color, #007bff)" />
          </marker>
          <marker
            id="arrowhead-warning"
            markerWidth="10"
            markerHeight="7"
            refX="9"
            refY="3.5"
            orient="auto"
          >
            <polygon points="0 0, 10 3.5, 0 7" fill="#FF9800" />
          </marker>
        </defs>
        {connections.map((c) => {
          const srcNode = nodes.find((n) => n.id === c.sourceId);
          const tgtNode = nodes.find((n) => n.id === c.targetId);
          if (!srcNode || !tgtNode) return null;
          const src = getHandlePosition(srcNode, c.sourceHandle);
          const tgt = getHandlePosition(tgtNode, c.targetHandle);
          const dy = tgt.y - src.y;
          const pathD = `M ${src.x} ${src.y} C ${src.x} ${src.y + Math.abs(dy) / 2}, ${tgt.x} ${tgt.y - Math.abs(dy) / 2}, ${tgt.x} ${tgt.y}`;
          const isSelected = selectedConnectionId === c.id;
          const isWarning = connectionWarnings[c.id];
          const strokeColor = isSelected
            ? 'var(--primary-color, #007bff)'
            : isWarning
              ? '#FF9800'
              : 'var(--text-secondary, #666)';
          return (
            <g
              key={c.id}
              onClick={(e) => handleConnectionClick(e, c.id)}
              style={{ cursor: 'pointer' }}
              role="button"
              tabIndex={-1}
              aria-label={t('Verbindung auswählen, Entf zum Löschen')}
            >
              <path
                d={pathD}
                fill="none"
                stroke="transparent"
                strokeWidth="16"
                pointerEvents="stroke"
              />
              <path
                d={pathD}
                fill="none"
                stroke={strokeColor}
                strokeWidth={isSelected ? 3 : 2}
                strokeDasharray={isWarning && !isSelected ? '6 3' : undefined}
                markerEnd={isSelected ? 'url(#arrowhead-selected)' : 'url(#arrowhead)'}
                pointerEvents="none"
              />
              {isWarning && !isSelected && (
                <title>{t('Typeninkompatibilität: Ausgabetyp')}</title>
              )}
            </g>
          );
        })}
        {connectingFrom && dragPos && (() => {
          const end = clientToCanvas(dragPos.x, dragPos.y);
          return (
            <path
              d={`M ${connectingFrom.x} ${connectingFrom.y} L ${end.x} ${end.y}`}
              fill="none"
              stroke="var(--primary-color, #007bff)"
              strokeWidth="2"
              strokeDasharray="4 4"
              pointerEvents="none"
            />
          );
        })()}
      </svg>
      {nodes.map((node) => {
        const nt = nodeTypeMap[node.type];
        const category = nt?.category ?? 'io';
        const color = node.color ?? nt?.meta?.color ?? '#00BCD4';
        const handles: Array<{ index: number; isOutput: boolean }> = [];
        for (let i = 0; i < node.inputs; i++) handles.push({ index: i, isOutput: false });
        for (let i = 0; i < node.outputs; i++) handles.push({ index: node.inputs + i, isOutput: true });

        const isSelected = selectedNodeIds.has(node.id);
        const isEditingTitle = editingNodeId === node.id && editingField === 'title';
        const displayTitle = node.title ?? node.label ?? getLabel(node);
        const hlStatus = highlightedNodeIds?.[node.id];
        const hlColor = hlStatus ? HIGHLIGHT_COLORS[hlStatus] : null;

        return (
          <div
            key={node.id}
            className={`${styles.canvasNode} ${isSelected ? styles.canvasNodeSelected : ''} ${hlStatus ? styles.canvasNodeHighlighted : ''}`}
            style={{
              left: node.x,
              top: node.y,
              width: NODE_WIDTH,
              height: NODE_HEIGHT,
              borderColor: hlColor || color,
              backgroundColor: hlColor ? `${hlColor}20` : `${color}15`,
              boxShadow: hlStatus === 'running' ? `0 0 12px ${hlColor}80` : undefined,
            }}
            onClick={(e) => e.stopPropagation()}
            onMouseDown={(e) => {
              e.stopPropagation();
              setSelectedConnectionId(null);
              if (e.shiftKey) {
                setSelectedNodeIds((prev) => {
                  const next = new Set(prev);
                  if (next.has(node.id)) next.delete(node.id);
                  else next.add(node.id);
                  return next;
                });
                return;
              }
              if (!selectedNodeIds.has(node.id)) {
                setSelectedNodeIds(new Set([node.id]));
              }
              handleNodeMouseDown(e, node.id);
            }}
          >
            {handles.map(({ index, isOutput }) => {
              const pos = getHandlePosition(node, index);
              const used = !isOutput && getUsedTargetHandles.has(`${node.id}-${index}`);
              const selConn = selectedConnectionId ? connections.find((c) => c.id === selectedConnectionId) : null;
              const isCurrentTargetOfSelection =
                selConn && selConn.targetId === node.id && selConn.targetHandle === index;
              const canConnect =
                isOutput ||
                (!used && connectingFrom) ||
                (!!selectedConnectionId && !isOutput && (!used || isCurrentTargetOfSelection));
              const nt = nodeTypeMap[node.type];
              const outputLabel = isOutput && nt?.outputLabels ? nt.outputLabels[index - node.inputs] : undefined;
              return (
                <div
                  key={index}
                  className={styles.handleWrapper}
                  style={{
                    top: pos.side === 'top' ? -HANDLE_OFFSET : undefined,
                    bottom: pos.side === 'bottom' ? -HANDLE_OFFSET : undefined,
                    left: pos.x - node.x - HANDLE_OFFSET,
                  }}
                >
                  {outputLabel && pos.side === 'bottom' && (
                    <span className={styles.handleLabel}>{outputLabel}</span>
                  )}
                  <div
                    className={`${styles.handle} ${isOutput ? styles.handleOutput : styles.handleInput} ${canConnect ? styles.handleConnectable : ''}`}
                    style={{ width: HANDLE_SIZE, height: HANDLE_SIZE }}
                    onMouseDown={(e) => handleHandleMouseDown(e, node.id, index, isOutput)}
                    onMouseUp={(e) => !isOutput && handleHandleMouseUp(e, node.id, index)}
                    title={
                      outputLabel ??
                      (selectedConnectionId && !isOutput
                        ? used
                          ? t('Aktuelles Ziel klicken, um abzuwählen')
                          : t('Klicken zum Umleiten')
                        : undefined)
                    }
                  />
                  {outputLabel && pos.side === 'top' && (
                    <span className={styles.handleLabel}>{outputLabel}</span>
                  )}
                </div>
              );
            })}
            <div className={styles.canvasNodeContent}>
              <div
                className={styles.canvasNodeIcon}
                style={{ backgroundColor: `${color}40`, color }}
              >
                {getCategoryIcon(category)}
              </div>
              <div className={styles.canvasNodeText}>
                {isEditingTitle ? (
                  <input
                    type="text"
                    className={styles.canvasNodeInput}
                    value={node.title ?? displayTitle}
                    autoFocus
                    onClick={(e) => e.stopPropagation()}
                    onKeyDown={(e) => {
                      if (e.key === 'Enter') {
                        handleNodeUpdate(node.id, { title: (e.target as HTMLInputElement).value });
                        setEditingNodeId(null);
                        setEditingField(null);
                      }
                      if (e.key === 'Escape') {
                        setEditingNodeId(null);
                        setEditingField(null);
                      }
                    }}
                    onBlur={(e) => {
                      handleNodeUpdate(node.id, { title: e.target.value });
                      setEditingNodeId(null);
                      setEditingField(null);
                    }}
                    onChange={(e) => handleNodeUpdate(node.id, { title: e.target.value })}
                  />
                ) : (
                  <span
                    className={styles.canvasNodeTitle}
                    onDoubleClick={(e) => {
                      e.stopPropagation();
                      setEditingNodeId(node.id);
                      setEditingField('title');
                    }}
                  >
                    {displayTitle}
                  </span>
                )}
                {node.comment && (
                  <span className={styles.canvasNodeComment}>{node.comment}</span>
                )}
              </div>
            {node.comment && (
              <div className={styles.canvasNodeCommentTooltip}>{node.comment}</div>
            )}
            </div>
          </div>
        );
      })}
      {selectionBox && (
        <div
          className={styles.selectionBox}
          style={{
            left: Math.min(selectionBox.startX, selectionBox.endX),
            top: Math.min(selectionBox.startY, selectionBox.endY),
            width: Math.abs(selectionBox.endX - selectionBox.startX),
            height: Math.abs(selectionBox.endY - selectionBox.startY),
          }}
        />
      )}
      {nodes.length === 0 && (
        <div className={styles.canvasPlaceholder}>
          <p>{t('Knoten aus der Liste links ziehen')}</p>
        </div>
      )}
      </div>
    </div>
  );
};
