import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  ReactFlow,
  Background,
  Controls,
  MiniMap,
  Panel,
  useNodesState,
  useEdgesState,
  useReactFlow,
  ConnectionMode,
  SelectionMode,
  type Connection,
  type Edge,
  type Node,
  type OnNodesChange,
  type OnSelectionChangeFunc,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import type { Theme } from '../../types/index';
import type { LogicNode, ConditionDSL } from '../../utils/strategyTypes';
import {
  addChild,
  deleteNode,
  mergeAtRoot,
  makeNode,
  updateNode,
  type DefType,
} from '../../utils/strategyEditorShared';
import {
  START_NODE_ID,
  applyEdgeHandles,
  buildConnectionFromPositions,
  canAcceptPaletteAsParent,
  connectionSidesFromDrop,
  disconnectTreeEdge,
  extractNode,
  findNodeAtFlowPosition,
  findNodeInTree,
  FLOW_NODE_H,
  FLOW_NODE_W,
  getNodeDimensions,
  isDescendant,
  isOrphanNode,
  isPaletteConnectTarget,
  isStartNodeId,
  isValidStrategyConnection,
  resolveStrategyConnection,
  resolveOrphanDragConnection,
  logicNodeToFlow,
  computeAutoFlowLayout,
  spawnPositionNearAnchor,
  subtreeToFlatOrphans,
  type EdgeHandleBinding,
} from '../../utils/strategyFlowLayout';
import {
  createStartNodeId,
  defaultStartMeta,
  DEFAULT_START_CANDLE,
  normalizeStartCandleType,
  type StartNodeMeta,
} from '../../utils/strategyStartNodes';
import { ConditionFlowNode, LogicGateNode, StartNode } from './FlowNodes';
import { StrategyFlowEdge } from './StrategyFlowEdge';
import { edgeDisconnectRef, edgeBindingUpdateRef, layoutFlushRef } from './strategyEditorCallbacks';
import { MultiSelectionDeleteButton } from './MultiSelectionDeleteButton';
import { getMinimapColors } from './minimapTheme';
import {
  loadCanvasInteractionMode,
  saveCanvasInteractionMode,
  type StrategyCanvasInteractionMode,
} from '../../utils/strategyCanvasInteractionStorage';
import type { FlowLayoutChangePayload } from '../../utils/strategyEditorLayoutStorage';

export type FlowLayoutSeed = {
  seedKey: string;
  positions: Record<string, { x: number; y: number }>;
  edgeHandles: Record<string, EdgeHandleBinding>;
};

const nodeTypes = {
  start: StartNode,
  logic: LogicGateNode,
  condition: ConditionFlowNode,
};

const edgeTypes = {
  strategy: StrategyFlowEdge,
};

interface Props {
  theme: Theme;
  root: LogicNode | null;
  orphans: LogicNode[];
  onOrphansChange: (nodes: LogicNode[]) => void;
  def: DefType;
  signalTab: 'buy' | 'sell';
  onChange: (root: LogicNode | null) => void;
  selectedNodeId: string | null;
  onSelectNode: (id: string | null) => void;
  layoutSeed: FlowLayoutSeed;
  onLayoutChange?: (snapshot: FlowLayoutChangePayload) => void;
  startMeta?: Record<string, StartNodeMeta>;
  extraStartIds?: string[];
  extraRoots?: Record<string, LogicNode | null>;
  onStartMetaChange?: (meta: Record<string, StartNodeMeta>) => void;
  onExtraStartIdsChange?: (ids: string[]) => void;
  onExtraRootsChange?: (roots: Record<string, LogicNode | null>) => void;
}

function collectTreeIds(node: LogicNode | null): string[] {
  if (!node) return [];
  const ids = [node.id];
  for (const c of node.children ?? []) ids.push(...collectTreeIds(c));
  return ids;
}

function saveEdgeHandles(
  handles: Map<string, EdgeHandleBinding>,
  edgeId: string,
  conn: Pick<Connection, 'sourceHandle' | 'targetHandle'>,
  lockHandles = false,
) {
  handles.set(edgeId, {
    sourceHandle: conn.sourceHandle,
    targetHandle: conn.targetHandle,
    manual: lockHandles,
    manualSource: lockHandles,
    manualTarget: lockHandles,
  });
}

function applyTreeConnection(
  sourceId: string,
  targetId: string,
  root: LogicNode | null,
  extraRoots: Record<string, LogicNode | null>,
  onChange: (root: LogicNode | null) => void,
  onExtraRootsChange: (roots: Record<string, LogicNode | null>) => void,
) {
  const promoteToStart = (startId: string, node: LogicNode, remainder: LogicNode | null) => {
    if (startId === START_NODE_ID) {
      onChange(remainder ? mergeAtRoot(node, remainder, false) : node);
      return;
    }
    const branch = extraRoots[startId] ?? null;
    onExtraRootsChange({
      ...extraRoots,
      [startId]: branch ? mergeAtRoot(branch, node, false) : node,
    });
  };

  if (isStartNodeId(sourceId)) {
    if (root && (root.id === targetId || findNodeInTree(root, targetId))) {
      const { tree, node } = extractNode(root, targetId);
      if (!node) return;
      onChange(tree);
      promoteToStart(sourceId, node, tree);
      return;
    }
    for (const [sid, branch] of Object.entries(extraRoots)) {
      if (!branch || !findNodeInTree(branch, targetId)) continue;
      const { tree, node } = extractNode(branch, targetId);
      if (!node) return;
      onExtraRootsChange({ ...extraRoots, [sid]: tree });
      promoteToStart(sourceId, node, null);
      return;
    }
    return;
  }

  if (!root) return;
  const sourceNode = findNodeInTree(root, sourceId)
    ?? Object.values(extraRoots).map(br => findNodeInTree(br, sourceId)).find(Boolean);
  if (!sourceNode || !['AND', 'OR', 'NOT'].includes(sourceNode.type)) return;

  if (findNodeInTree(root, targetId)) {
    if (isDescendant(root, targetId, sourceId)) return;
    const { tree: afterRemove, node: moved } = extractNode(root, targetId);
    if (!moved) return;
    onChange(addChild(afterRemove ?? root, sourceId, moved));
    return;
  }

  for (const [sid, branch] of Object.entries(extraRoots)) {
    if (!branch || !findNodeInTree(branch, targetId)) continue;
    if (isDescendant(branch, targetId, sourceId)) return;
    const { tree, node: moved } = extractNode(branch, targetId);
    if (!moved) return;
    onExtraRootsChange({ ...extraRoots, [sid]: tree });
    if (findNodeInTree(root, sourceId)) {
      onChange(addChild(root, sourceId, moved));
    } else {
      const parentBranch = Object.entries(extraRoots).find(([, br]) => br && findNodeInTree(br, sourceId));
      if (parentBranch) {
        const [psid, pbr] = parentBranch;
        onExtraRootsChange({
          ...extraRoots,
          [sid]: tree,
          [psid]: addChild(pbr!, sourceId, moved),
        });
      }
    }
    return;
  }
}

function pruneEdgeHandles(handles: Map<string, EdgeHandleBinding>, edges: Edge[]) {
  const ids = new Set(edges.map(e => e.id));
  for (const id of handles.keys()) {
    if (!ids.has(id)) handles.delete(id);
  }
}

function connectOrphanIntoTree(
  conn: Connection,
  orphan: LogicNode,
  root: LogicNode | null,
  extraRoots: Record<string, LogicNode | null>,
  onChange: (root: LogicNode | null) => void,
  onExtraRootsChange: (roots: Record<string, LogicNode | null>) => void,
): void {
  const sourceId = conn.source!;
  if (isStartNodeId(sourceId)) {
    if (sourceId === START_NODE_ID) {
      onChange(root ? mergeAtRoot(root, orphan, false) : orphan);
      return;
    }
    const branch = extraRoots[sourceId] ?? null;
    onExtraRootsChange({
      ...extraRoots,
      [sourceId]: branch ? mergeAtRoot(branch, orphan, false) : orphan,
    });
    return;
  }
  if (!root) return;
  onChange(addChild(root, sourceId, orphan));
}

function connectOrphanAsParent(
  conn: Connection,
  orphan: LogicNode,
  root: LogicNode | null,
  onChange: (root: LogicNode | null) => void,
): void {
  if (!root) return;
  const { tree, node: moved } = extractNode(root, conn.target!);
  if (!moved) return;
  const isOp = ['AND', 'OR', 'NOT'].includes(orphan.type);
  const combined = { ...orphan, children: [...(orphan.children ?? []), moved] };
  onChange(tree ? mergeAtRoot(tree, combined, isOp) : combined);
}

function StrategyEditorCanvasInner({
  theme,
  root,
  orphans,
  onOrphansChange,
  def,
  signalTab,
  onChange,
  selectedNodeId,
  onSelectNode,
  layoutSeed,
  onLayoutChange,
  startMeta = defaultStartMeta(),
  extraStartIds = [],
  extraRoots = {},
  onStartMetaChange,
  onExtraStartIdsChange,
  onExtraRootsChange,
}: Props) {
  const { fitView, screenToFlowPosition } = useReactFlow();
  const positionsRef = useRef(new Map<string, { x: number; y: number }>());
  const edgeHandlesRef = useRef(new Map<string, EdgeHandleBinding>());
  const structureKeyRef = useRef<string | null>(null);
  const orphanKeyRef = useRef<string | null>(null);
  const layoutEmitTimerRef = useRef<number | null>(null);
  const [interactionMode, setInteractionMode] = useState<StrategyCanvasInteractionMode>(
    () => loadCanvasInteractionMode(),
  );

  const handleInteractionModeChange = useCallback((mode: StrategyCanvasInteractionMode) => {
    setInteractionMode(mode);
    saveCanvasInteractionMode(mode);
  }, []);

  const [nodes, setNodes, onNodesChangeBase] = useNodesState<Node>([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
  const nodesRef = useRef(nodes);
  const edgesRef = useRef(edges);
  nodesRef.current = nodes;
  edgesRef.current = edges;

  const treeStructureKey = useMemo(() => {
    const parts = [collectTreeIds(root).join('|')];
    for (const sid of extraStartIds) parts.push(sid);
    for (const br of Object.values(extraRoots)) parts.push(collectTreeIds(br).join('|'));
    return parts.join('::');
  }, [root, extraStartIds, extraRoots]);
  const orphanKey = useMemo(() => orphans.map(o => o.id).join('|'), [orphans]);

  /** START에서 연결된 트리 전체 (미연결 고아 제외) */
  const treeLinkedIds = useMemo(() => {
    const ids = new Set<string>([START_NODE_ID, ...extraStartIds]);
    for (const id of collectTreeIds(root)) ids.add(id);
    for (const br of Object.values(extraRoots)) {
      for (const id of collectTreeIds(br)) ids.add(id);
    }
    return ids;
  }, [root, extraStartIds, extraRoots]);

  const minimapColors = useMemo(() => getMinimapColors(theme, signalTab), [theme, signalTab]);

  const minimapNodeColor = useCallback((node: Node) => {
    if (isStartNodeId(node.id)) return minimapColors.start;
    if (node.type === 'condition') return minimapColors.condition;
    return minimapColors.logic;
  }, [minimapColors]);

  const minimapNodeStrokeColor = useCallback((node: Node) => {
    if (isStartNodeId(node.id)) return minimapColors.start;
    return minimapColors.stroke;
  }, [minimapColors]);

  const signalTabRef = useRef(signalTab);
  signalTabRef.current = signalTab;

  const emitLayoutChange = useCallback((tab: 'buy' | 'sell') => {
    if (!onLayoutChange) return;
    onLayoutChange({
      tab,
      positions: Object.fromEntries(positionsRef.current),
      edgeHandles: Object.fromEntries(edgeHandlesRef.current),
    });
  }, [onLayoutChange]);

  const scheduleLayoutEmit = useCallback(() => {
    if (!onLayoutChange) return;
    const tabAtSchedule = signalTabRef.current;
    if (layoutEmitTimerRef.current != null) window.clearTimeout(layoutEmitTimerRef.current);
    layoutEmitTimerRef.current = window.setTimeout(() => {
      layoutEmitTimerRef.current = null;
      emitLayoutChange(tabAtSchedule);
    }, 120);
  }, [onLayoutChange, emitLayoutChange]);

  useEffect(() => () => {
    if (layoutEmitTimerRef.current != null) window.clearTimeout(layoutEmitTimerRef.current);
  }, []);

  useEffect(() => {
    positionsRef.current = new Map(Object.entries(layoutSeed.positions));
    edgeHandlesRef.current = new Map(Object.entries(layoutSeed.edgeHandles));
  }, [layoutSeed.edgeHandles, layoutSeed.positions, layoutSeed.seedKey]);

  useEffect(() => {
    layoutFlushRef.current = () => {
      if (layoutEmitTimerRef.current != null) {
        window.clearTimeout(layoutEmitTimerRef.current);
        layoutEmitTimerRef.current = null;
      }
      for (const n of nodesRef.current) {
        positionsRef.current.set(n.id, { x: n.position.x, y: n.position.y });
      }
      emitLayoutChange(signalTabRef.current);
    };
    return () => {
      layoutFlushRef.current = null;
    };
  }, [emitLayoutChange]);

  const handleStartCandleTypeChange = useCallback((startId: string, candleType: string) => {
    onStartMetaChange?.({
      ...startMeta,
      [startId]: { candleType: normalizeStartCandleType(candleType) },
    });
  }, [startMeta, onStartMetaChange]);

  const handleDeleteStart = useCallback((startId: string) => {
    if (startId === START_NODE_ID) return;
    const branch = extraRoots[startId];
    if (branch) {
      onOrphansChange([...orphans, ...subtreeToFlatOrphans(branch)]);
    }
    onExtraStartIdsChange?.(extraStartIds.filter(id => id !== startId));
    const nextMeta = { ...startMeta };
    delete nextMeta[startId];
    onStartMetaChange?.(nextMeta);
    const nextRoots = { ...extraRoots };
    delete nextRoots[startId];
    onExtraRootsChange?.(nextRoots);
    positionsRef.current.delete(startId);
    scheduleLayoutEmit();
  }, [
    extraRoots, extraStartIds, startMeta, orphans,
    onOrphansChange, onExtraStartIdsChange, onStartMetaChange, onExtraRootsChange, scheduleLayoutEmit,
  ]);

  const addStartAt = useCallback((flowPos: { x: number; y: number }) => {
    const newId = createStartNodeId();
    onExtraStartIdsChange?.([...extraStartIds, newId]);
    onStartMetaChange?.({
      ...startMeta,
      [newId]: { candleType: DEFAULT_START_CANDLE },
    });
    onExtraRootsChange?.({ ...extraRoots, [newId]: null });
    positionsRef.current.set(newId, {
      x: flowPos.x - getNodeDimensions(newId).w / 2,
      y: flowPos.y - getNodeDimensions(newId).h / 2,
    });
    scheduleLayoutEmit();
  }, [
    extraStartIds, extraRoots, startMeta,
    onExtraStartIdsChange, onStartMetaChange, onExtraRootsChange, scheduleLayoutEmit,
  ]);

  const handleDelete = useCallback((id: string) => {
    if (isStartNodeId(id)) return;
    if (isOrphanNode(orphans, id)) {
      onOrphansChange(orphans.filter(o => o.id !== id));
      positionsRef.current.delete(id);
      if (selectedNodeId === id) onSelectNode(null);
      return;
    }
    if (root && (root.id === id || findNodeInTree(root, id))) {
      if (root.id === id) {
        onChange(null);
      } else {
        onChange(deleteNode(root, id));
      }
      positionsRef.current.delete(id);
      if (selectedNodeId === id) onSelectNode(null);
      scheduleLayoutEmit();
      return;
    }
    for (const [sid, branch] of Object.entries(extraRoots)) {
      if (!branch || !findNodeInTree(branch, id)) continue;
      if (branch.id === id) {
        onExtraRootsChange?.({ ...extraRoots, [sid]: null });
      } else {
        onExtraRootsChange?.({ ...extraRoots, [sid]: deleteNode(branch, id) });
      }
      positionsRef.current.delete(id);
      if (selectedNodeId === id) onSelectNode(null);
      scheduleLayoutEmit();
      return;
    }
  }, [
    root, extraRoots, orphans, onChange, onOrphansChange, onExtraRootsChange,
    onSelectNode, selectedNodeId, scheduleLayoutEmit,
  ]);

  const deleteSelectedNodes = useCallback(() => {
    const ids = nodesRef.current
      .filter(n => n.selected && !isStartNodeId(n.id))
      .map(n => n.id);
    if (!ids.length) return;

    const orphanIds = new Set(ids.filter(id => isOrphanNode(orphans, id)));
    if (orphanIds.size) {
      onOrphansChange(orphans.filter(o => !orphanIds.has(o.id)));
      orphanIds.forEach(id => positionsRef.current.delete(id));
    }

    const treeIds = ids.filter(id => !orphanIds.has(id));
    if (treeIds.length && root) {
      let tree: LogicNode | null = root;
      for (const id of treeIds) {
        if (!tree) break;
        if (tree.id === id) {
          tree = null;
          positionsRef.current.delete(id);
          continue;
        }
        const next = deleteNode(tree, id);
        if (next !== tree) positionsRef.current.delete(id);
        tree = next;
      }
      onChange(tree);
    }
    onSelectNode(null);
    scheduleLayoutEmit();
  }, [root, orphans, onChange, onOrphansChange, onSelectNode, scheduleLayoutEmit]);

  const clearDropPreview = useCallback(() => {
    setNodes(nds => nds.map(n => ({
      ...n,
      data: {
        ...n.data,
        dragOver: false,
        activeSourceSide: undefined,
      },
    })));
  }, [setNodes]);

  const updateDropPreview = useCallback((anchorId: string, flowPos: { x: number; y: number }) => {
    const anchor = nodesRef.current.find(n => n.id === anchorId);
    if (!anchor) return;

    const canParent = canAcceptPaletteAsParent(anchorId, root);
    if (!canParent) {
      setNodes(nds => nds.map(n => ({
        ...n,
        data: { ...n.data, dragOver: false, activeSourceSide: undefined },
      })));
      return;
    }

    const dim = getNodeDimensions(anchorId);
    const { sourceSide } = connectionSidesFromDrop(anchor.position, dim, flowPos);
    setNodes(nds => nds.map(n => ({
      ...n,
      data: {
        ...n.data,
        dragOver: n.id === anchorId,
        activeSourceSide: n.id === anchorId ? sourceSide : undefined,
      },
    })));
  }, [setNodes, root]);

  const orphanDragTargetRef = useRef<{ parentId: string; childId: string } | null>(null);

  const updateOrphanConnectPreview = useCallback((
    resolved: { parentId: string; childId: string },
    draggedOrphanId: string,
    orphanPos: { x: number; y: number },
  ) => {
    const { parentId, childId } = resolved;
    const nodesSnapshot = nodesRef.current;
    const parentNode = nodesSnapshot.find(n => n.id === parentId);
    const childNode = nodesSnapshot.find(n => n.id === childId);
    if (!parentNode || !childNode) return;

    const parentDim = getNodeDimensions(parentId);
    const childDim = getNodeDimensions(childId);
    const parentPos = parentId === draggedOrphanId ? orphanPos : parentNode.position;
    const childPos = childId === draggedOrphanId ? orphanPos : childNode.position;
    const childCenter = {
      x: childPos.x + childDim.w / 2,
      y: childPos.y + childDim.h / 2,
    };
    const { sourceSide } = connectionSidesFromDrop(parentPos, parentDim, childCenter);

    setNodes(nds => nds.map(n => ({
      ...n,
      data: {
        ...n.data,
        dragOver: n.id === parentId,
        activeSourceSide: n.id === parentId ? sourceSide : undefined,
      },
    })));
  }, [setNodes]);

  const addOrphanAt = useCallback((
    data: { type: string; value: string; label: string; composite?: boolean },
    flowPos: { x: number; y: number },
  ) => {
    const newNode = makeNode(data.type, data.value, signalTab, def, data.composite ? { composite: true } : undefined);
    positionsRef.current.set(newNode.id, {
      x: flowPos.x - FLOW_NODE_W / 2,
      y: flowPos.y - FLOW_NODE_H / 2,
    });
    onOrphansChange([...orphans, newNode]);
    scheduleLayoutEmit();
  }, [orphans, onOrphansChange, signalTab, def, scheduleLayoutEmit]);

  const applyDropWithAttach = useCallback((
    anchorId: string,
    data: { type: string; value: string; label: string; composite?: boolean },
    flowPos: { x: number; y: number },
  ) => {
    if (data.type === 'start') {
      addStartAt(flowPos);
      return;
    }

    const newNode = makeNode(data.type, data.value, signalTab, def, data.composite ? { composite: true } : undefined);
    const anchor = nodesRef.current.find(n => n.id === anchorId);
    if (!anchor) return;

    const anchorDim = getNodeDimensions(anchorId);
    const { sourceHandle, targetHandle, sourceSide } = connectionSidesFromDrop(anchor.position, anchorDim, flowPos);
    positionsRef.current.set(newNode.id, spawnPositionNearAnchor(anchor.position, anchorDim, sourceSide));

    const saveAttachHandles = (parentId: string) => {
      saveEdgeHandles(edgeHandlesRef.current, `${parentId}-${newNode.id}`, { sourceHandle, targetHandle });
    };

    if (isStartNodeId(anchorId)) {
      if (anchorId === START_NODE_ID) {
        onChange(root ? mergeAtRoot(root, newNode, data.type === 'operator') : newNode);
      } else {
        const branch = extraRoots[anchorId] ?? null;
        onExtraRootsChange?.({
          ...extraRoots,
          [anchorId]: branch ? mergeAtRoot(branch, newNode, data.type === 'operator') : newNode,
        });
      }
      saveAttachHandles(anchorId);
      scheduleLayoutEmit();
      return;
    }

    const anchorNode = findNodeInTree(root, anchorId)
      ?? Object.values(extraRoots).map(br => findNodeInTree(br, anchorId)).find(Boolean);
    if (anchorNode?.type === 'CONDITION') {
      onChange(mergeAtRoot(root, newNode, data.type === 'operator'));
      return;
    }

    if (!canAcceptPaletteAsParent(anchorId, root)) return;

    if (root && findNodeInTree(root, anchorId)) {
      onChange(addChild(root, anchorId, newNode));
    } else {
      const parentBranch = Object.entries(extraRoots).find(([, br]) => br && findNodeInTree(br, anchorId));
      if (parentBranch) {
        const [sid, br] = parentBranch;
        onExtraRootsChange?.({
          ...extraRoots,
          [sid]: addChild(br!, anchorId, newNode),
        });
      }
    }
    saveAttachHandles(anchorId);
    scheduleLayoutEmit();
  }, [root, extraRoots, signalTab, def, onChange, onExtraRootsChange, addStartAt, scheduleLayoutEmit]);

  const handleDragOverTarget = useCallback((anchorId: string, flowPos: { x: number; y: number }) => {
    updateDropPreview(anchorId, flowPos);
  }, [updateDropPreview]);

  const handleDragLeaveTarget = useCallback((anchorId: string) => {
    setNodes(nds => nds.map(n => (
      n.id === anchorId
        ? { ...n, data: { ...n.data, dragOver: false, activeSourceSide: undefined } }
        : n
    )));
  }, [setNodes]);

  const handleDropTarget = useCallback((
    anchorId: string,
    data: { type: string; value: string; label: string },
    flowPos: { x: number; y: number },
  ) => {
    clearDropPreview();
    if (isPaletteConnectTarget(anchorId, root, orphans)) {
      applyDropWithAttach(anchorId, data, flowPos);
    } else {
      addOrphanAt(data, flowPos);
    }
  }, [clearDropPreview, applyDropWithAttach, addOrphanAt, root, orphans]);

  const handleUpdateCondition = useCallback((id: string, condition: ConditionDSL) => {
    if (isOrphanNode(orphans, id)) {
      onOrphansChange(orphans.map(o => (
        o.id === id ? { ...o, condition } : o
      )));
      return;
    }
    if (root && findNodeInTree(root, id)) {
      onChange(updateNode(root, id, n => ({ ...n, condition })));
      return;
    }
    for (const [sid, branch] of Object.entries(extraRoots)) {
      if (branch && findNodeInTree(branch, id)) {
        onExtraRootsChange?.({
          ...extraRoots,
          [sid]: updateNode(branch, id, n => ({ ...n, condition })),
        });
        return;
      }
    }
  }, [root, extraRoots, orphans, onChange, onOrphansChange, onExtraRootsChange]);

  const flowCallbacks = useMemo(() => ({
    onDelete: handleDelete,
    onUpdateCondition: handleUpdateCondition,
    onDropTarget: handleDropTarget,
    onDragOverTarget: handleDragOverTarget,
    onDragLeaveTarget: handleDragLeaveTarget,
    onStartCandleTypeChange: handleStartCandleTypeChange,
    onDeleteStart: handleDeleteStart,
  }), [
    handleDelete, handleUpdateCondition, handleDropTarget,
    handleDragOverTarget, handleDragLeaveTarget,
    handleStartCandleTypeChange, handleDeleteStart,
  ]);

  const rebuildFlow = useCallback(() => logicNodeToFlow(
    root,
    def,
    signalTab,
    positionsRef.current,
    flowCallbacks,
    null,
    orphans,
    startMeta,
    extraStartIds,
    extraRoots,
  ), [root, def, signalTab, flowCallbacks, orphans, startMeta, extraStartIds, extraRoots]);

  // 트리 구조 변경 → 전체 재배치 / 조건 편집 → 라벨만 갱신(위치 유지)
  useEffect(() => {
    const { nodes: layoutNodes, edges: layoutEdges } = rebuildFlow();
    const mergedEdges = applyEdgeHandles(layoutEdges, positionsRef.current, edgeHandlesRef.current);
    pruneEdgeHandles(edgeHandlesRef.current, mergedEdges);
    const structureChanged = structureKeyRef.current !== treeStructureKey;
    const orphansChanged = orphanKeyRef.current !== orphanKey;
    structureKeyRef.current = treeStructureKey;
    orphanKeyRef.current = orphanKey;

    const syncNodesFromLayout = () => setNodes(layoutNodes.map(n => {
      const prev = nodesRef.current.find(p => p.id === n.id);
      const savedPos = layoutSeed.positions[n.id];
      return {
        ...n,
        position: savedPos ? { x: savedPos.x, y: savedPos.y } : n.position,
        selected: prev?.selected ?? false,
      };
    }));

    const hasSavedLayout = layoutNodes.some(
      n => n.id !== START_NODE_ID && layoutSeed.positions[n.id] != null,
    );

    const needsNodeSync = layoutNodes.length !== nodesRef.current.length
      || !nodesRef.current.some(n => n.id === START_NODE_ID);

    if (structureChanged || orphansChanged || needsNodeSync) {
      syncNodesFromLayout();
      setEdges(mergedEdges);
      if (structureChanged && !hasSavedLayout) {
        requestAnimationFrame(() => {
          fitView({ padding: 0.4, duration: 200 });
        });
      }
    } else {
      setNodes(prev => prev.map(n => {
        const fresh = layoutNodes.find(ln => ln.id === n.id);
        if (!fresh) return n;
        const savedPos = layoutSeed.positions[n.id];
        return {
          ...n,
          position: savedPos ? { x: savedPos.x, y: savedPos.y } : fresh.position,
          data: fresh.data,
        };
      }));
      setEdges(mergedEdges);
    }
  }, [treeStructureKey, orphanKey, root, orphans, def, rebuildFlow, setNodes, setEdges, fitView, layoutSeed]);

  const handleDisconnectEdge = useCallback((edgeId: string) => {
    const edge = edgesRef.current.find(e => e.id === edgeId);
    if (!edge?.source || !edge.target) return;

    edgeHandlesRef.current.delete(edgeId);
    const { root: nextRoot, orphans: nextOrphans, extraRoots: nextExtraRoots } = disconnectTreeEdge(
      edge.source,
      edge.target,
      root,
      orphans,
      extraRoots,
    );

    if (nextRoot === root && nextOrphans.length === orphans.length && nextExtraRoots === extraRoots) return;

    onChange(nextRoot);
    onOrphansChange(nextOrphans);
    onExtraRootsChange?.(nextExtraRoots);
    onSelectNode(null);
    setEdges(prev => prev.map(e => ({ ...e, selected: false })));
  }, [root, orphans, extraRoots, onChange, onOrphansChange, onExtraRootsChange, onSelectNode, setEdges]);

  useEffect(() => {
    edgeDisconnectRef.current = handleDisconnectEdge;
    edgeBindingUpdateRef.current = (edgeId, patch) => {
      const prev = edgeHandlesRef.current.get(edgeId) ?? {};
      const next: EdgeHandleBinding = { ...prev, ...patch };
      if (patch.sourceHandle != null) next.manualSource = true;
      if (patch.targetHandle != null) next.manualTarget = true;
      if (patch.centerX != null || patch.centerY != null) next.manualCenter = true;
      if (next.manualSource && next.manualTarget) next.manual = true;
      edgeHandlesRef.current.set(edgeId, next);
      setEdges(prevEdges => applyEdgeHandles(prevEdges, positionsRef.current, edgeHandlesRef.current));
      scheduleLayoutEmit();
    };
    return () => {
      edgeDisconnectRef.current = null;
      edgeBindingUpdateRef.current = null;
    };
  }, [handleDisconnectEdge, setEdges, scheduleLayoutEmit]);

  const handleAutoLayout = useCallback(() => {
    const nextPositions = computeAutoFlowLayout(root, extraStartIds, extraRoots, orphans);
    positionsRef.current = new Map(Object.entries(nextPositions));
    edgeHandlesRef.current.clear();

    const { nodes: layoutNodes, edges: layoutEdges } = rebuildFlow();
    const mergedEdges = applyEdgeHandles(layoutEdges, positionsRef.current, edgeHandlesRef.current);

    setNodes(layoutNodes.map(n => ({
      ...n,
      position: positionsRef.current.get(n.id) ?? n.position,
      selected: nodesRef.current.find(p => p.id === n.id)?.selected ?? false,
    })));
    setEdges(mergedEdges);
    scheduleLayoutEmit();
    requestAnimationFrame(() => {
      fitView({ padding: 0.35, duration: 320 });
    });
  }, [
    root, extraStartIds, extraRoots, orphans, rebuildFlow, setNodes, setEdges,
    scheduleLayoutEmit, fitView,
  ]);

  const onSelectionChange: OnSelectionChangeFunc = useCallback(({ nodes: selNodes, edges: selEdges }) => {
    const picked = selNodes.filter(n => !isStartNodeId(n.id));
    if (picked.length === 1) onSelectNode(picked[0].id);
    else onSelectNode(null);
    if (selEdges.length === 1) {
      // 연결선만 선택된 경우 노드 선택 해제
      if (picked.length === 0) onSelectNode(null);
    }
  }, [onSelectNode]);

  const isValidConnection = useCallback(
    (conn: Connection | Edge) => isValidStrategyConnection(
      { source: conn.source, target: conn.target, sourceHandle: conn.sourceHandle ?? null, targetHandle: conn.targetHandle ?? null },
      root,
      orphans,
      extraRoots,
    ),
    [root, orphans, extraRoots],
  );

  const applyResolvedConnection = useCallback((
    parentId: string,
    childId: string,
    conn: Connection,
  ) => {
    const wired: Connection = { ...conn, source: parentId, target: childId };
    saveEdgeHandles(edgeHandlesRef.current, `${parentId}-${childId}`, wired);

    const childOrphan = orphans.find(o => o.id === childId);
    const parentOrphan = orphans.find(o => o.id === parentId);

    if (childOrphan) {
      onOrphansChange(orphans.filter(o => o.id !== childId));
      connectOrphanIntoTree(
        wired,
        childOrphan,
        root,
        extraRoots,
        onChange,
        next => onExtraRootsChange?.(next),
      );
      scheduleLayoutEmit();
      return;
    }
    if (parentOrphan) {
      onOrphansChange(orphans.filter(o => o.id !== parentId));
      connectOrphanAsParent(wired, parentOrphan, root, onChange);
      scheduleLayoutEmit();
      return;
    }

    applyTreeConnection(
      parentId,
      childId,
      root,
      extraRoots,
      onChange,
      next => onExtraRootsChange?.(next),
    );
    scheduleLayoutEmit();
  }, [root, orphans, extraRoots, onChange, onExtraRootsChange, onOrphansChange, scheduleLayoutEmit]);

  const onNodesChange: OnNodesChange = useCallback((changes) => {
    const positionChanges = changes.filter(
      (ch): ch is Extract<typeof ch, { type: 'position' }> => ch.type === 'position' && !!ch.position,
    );
    const startChange = positionChanges.find(ch => ch.id === START_NODE_ID);
    const otherTreePositionChanges = positionChanges.filter(
      ch => ch.id !== START_NODE_ID && treeLinkedIds.has(ch.id),
    );

    let startGroupDelta: { dx: number; dy: number } | null = null;
    const startPos = startChange?.position;
    if (startChange && startPos && otherTreePositionChanges.length === 0) {
      const prev = positionsRef.current.get(START_NODE_ID);
      if (prev) {
        startGroupDelta = {
          dx: startPos.x - prev.x,
          dy: startPos.y - prev.y,
        };
      }
    }

    onNodesChangeBase(changes);

    let positionsChanged = false;

    if (startGroupDelta && startPos && (startGroupDelta.dx !== 0 || startGroupDelta.dy !== 0)) {
      positionsRef.current.set(START_NODE_ID, { x: startPos.x, y: startPos.y });
      for (const id of treeLinkedIds) {
        if (id === START_NODE_ID) continue;
        const p = positionsRef.current.get(id);
        if (p) {
          positionsRef.current.set(id, {
            x: p.x + startGroupDelta.dx,
            y: p.y + startGroupDelta.dy,
          });
        }
      }
      setNodes(nds => nds.map(n => {
        if (!treeLinkedIds.has(n.id)) return n;
        const p = positionsRef.current.get(n.id);
        return p ? { ...n, position: { x: p.x, y: p.y } } : n;
      }));
      positionsChanged = true;
    }

    for (const ch of positionChanges) {
      if (!ch.position) continue;
      if (startGroupDelta && treeLinkedIds.has(ch.id)) continue;

      if (isOrphanNode(orphans, ch.id)) {
        positionsRef.current.set(ch.id, { x: ch.position.x, y: ch.position.y });

        if (ch.dragging === true) {
          positionsChanged = true;
          const nodesSnapshot = nodesRef.current.map(n => (
            n.id === ch.id
              ? { id: n.id, position: ch.position! }
              : { id: n.id, position: n.position }
          ));
          const resolved = resolveOrphanDragConnection(
            ch.id,
            ch.position,
            nodesSnapshot,
            root,
            orphans,
            extraRoots,
          );
          orphanDragTargetRef.current = resolved;
          if (resolved) {
            updateOrphanConnectPreview(resolved, ch.id, ch.position);
          } else {
            clearDropPreview();
          }
        } else if (ch.dragging === false) {
          const pending = orphanDragTargetRef.current;
          orphanDragTargetRef.current = null;
          clearDropPreview();
          if (pending) {
            const conn = buildConnectionFromPositions(
              pending.parentId,
              pending.childId,
              positionsRef.current,
            );
            applyResolvedConnection(pending.parentId, pending.childId, conn);
          }
          positionsChanged = true;
        }
        continue;
      }

      positionsRef.current.set(ch.id, { x: ch.position.x, y: ch.position.y });
      positionsChanged = true;
    }

    if (positionsChanged) {
      setEdges(prev => applyEdgeHandles(prev, positionsRef.current, edgeHandlesRef.current));
      scheduleLayoutEmit();
    }
  }, [
    onNodesChangeBase,
    setNodes,
    setEdges,
    treeLinkedIds,
    orphans,
    root,
    clearDropPreview,
    updateOrphanConnectPreview,
    applyResolvedConnection,
    scheduleLayoutEmit,
  ]);

  const onConnect = useCallback((conn: Connection) => {
    const resolved = resolveStrategyConnection(conn, root, orphans, extraRoots);
    if (!resolved) return;
    applyResolvedConnection(resolved.parentId, resolved.childId, conn);
  }, [root, orphans, extraRoots, applyResolvedConnection]);

  const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => {
    const resolved = resolveStrategyConnection(newConnection, root, orphans, extraRoots);
    if (!resolved) return;

    edgeHandlesRef.current.delete(oldEdge.id);

    const endpointsChanged = oldEdge.source !== resolved.parentId || oldEdge.target !== resolved.childId;
    if (endpointsChanged) {
      applyResolvedConnection(resolved.parentId, resolved.childId, newConnection);
      return;
    }

    saveEdgeHandles(edgeHandlesRef.current, `${resolved.parentId}-${resolved.childId}`, newConnection, true);
    setEdges(prev => applyEdgeHandles(
      prev.map(e => (
        e.id === oldEdge.id
          ? {
            ...e,
            sourceHandle: newConnection.sourceHandle,
            targetHandle: newConnection.targetHandle,
          }
          : e
      )),
      positionsRef.current,
      edgeHandlesRef.current,
    ));
  }, [root, orphans, extraRoots, applyResolvedConnection, setEdges]);

  const isPaletteDrag = (e: React.DragEvent) => e.dataTransfer.types.includes('application/json');

  const resolvePaletteDrop = useCallback((flowPos: { x: number; y: number }) => {
    const hit = findNodeAtFlowPosition(flowPos, nodesRef.current);
    if (hit && isPaletteConnectTarget(hit.id, root, orphans)) {
      return { mode: 'connect' as const, anchorId: hit.id };
    }
    return { mode: 'orphan' as const };
  }, [root, orphans]);

  const onPaneDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    clearDropPreview();
    try {
      const data = JSON.parse(e.dataTransfer.getData('application/json'));
      const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
      if (data.type === 'start') {
        addStartAt(flowPos);
        return;
      }
      const drop = resolvePaletteDrop(flowPos);
      if (drop.mode === 'connect') {
        applyDropWithAttach(drop.anchorId, data, flowPos);
      } else {
        addOrphanAt(data, flowPos);
      }
    } catch { /* ignore */ }
  }, [clearDropPreview, screenToFlowPosition, applyDropWithAttach, addOrphanAt, addStartAt, resolvePaletteDrop]);

  const onPaneDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    if (!isPaletteDrag(e)) return;
    e.dataTransfer.dropEffect = 'copy';
    const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
    const drop = resolvePaletteDrop(flowPos);
    if (drop.mode === 'connect') {
      updateDropPreview(drop.anchorId, flowPos);
    } else {
      clearDropPreview();
    }
  }, [screenToFlowPosition, resolvePaletteDrop, updateDropPreview, clearDropPreview]);

  const onPaneDragLeave = useCallback((e: React.DragEvent) => {
    const rel = e.relatedTarget as Element | null;
    if (rel && e.currentTarget.contains(rel)) return;
    clearDropPreview();
  }, [clearDropPreview]);

  const onKeyDown = useCallback((e: React.KeyboardEvent) => {
    if (e.key !== 'Delete' && e.key !== 'Backspace') return;

    const selectedEdge = edgesRef.current.find(ed => ed.selected);
    if (selectedEdge) {
      e.preventDefault();
      handleDisconnectEdge(selectedEdge.id);
      return;
    }

    const selectedCount = nodesRef.current.filter(n => n.selected && n.id !== START_NODE_ID).length;
    if (selectedCount > 1) {
      e.preventDefault();
      deleteSelectedNodes();
    } else if (selectedNodeId && selectedNodeId !== START_NODE_ID) {
      e.preventDefault();
      handleDelete(selectedNodeId);
    }
  }, [selectedNodeId, handleDelete, deleteSelectedNodes, handleDisconnectEdge]);

  return (
    <div
      className={`se-canvas-wrap se-canvas-wrap--${signalTab} se-canvas-wrap--${interactionMode}`}
      onKeyDown={onKeyDown}
      tabIndex={0}
    >
      <ReactFlow
        nodes={nodes}
        edges={edges}
        nodeTypes={nodeTypes}
        edgeTypes={edgeTypes}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onSelectionChange={onSelectionChange}
        onConnect={onConnect}
        isValidConnection={isValidConnection}
        onReconnect={onReconnect}
        onDrop={onPaneDrop}
        onDragOver={onPaneDragOver}
        onDragLeave={onPaneDragLeave}
        connectionMode={ConnectionMode.Loose}
        edgesReconnectable
        reconnectRadius={24}
        minZoom={0.25}
        maxZoom={1.8}
        nodesDraggable
        nodesConnectable
        elementsSelectable
        selectionOnDrag={interactionMode === 'select'}
        selectionMode={SelectionMode.Partial}
        panOnDrag={interactionMode === 'pan' ? true : [1, 2]}
        selectNodesOnDrag={false}
        multiSelectionKeyCode={['Meta', 'Control', 'Shift']}
        deleteKeyCode={null}
        defaultEdgeOptions={{
          type: 'strategy',
          className: 'se-flow-edge',
          style: { strokeWidth: 2 },
        }}
        proOptions={{ hideAttribution: true }}
      >
        <Background gap={24} size={1} className="se-flow-bg" />
        <Controls className="se-flow-controls" showInteractive={false} />
        <MiniMap
          className="se-flow-minimap"
          zoomable
          pannable
          nodeStrokeWidth={2}
          bgColor={minimapColors.bg}
          maskColor={minimapColors.mask}
          maskStrokeColor={minimapColors.maskStroke}
          nodeColor={minimapNodeColor}
          nodeStrokeColor={minimapNodeStrokeColor}
        />
        <Panel position="top-right" className="se-canvas-auto-layout">
          <button
            type="button"
            className="se-canvas-auto-layout-btn"
            title="자동 정렬"
            aria-label="노드 자동 정렬"
            onClick={handleAutoLayout}
          >
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
              <rect x="3" y="3" width="7" height="7" rx="1" />
              <rect x="14" y="3" width="7" height="7" rx="1" />
              <rect x="3" y="14" width="7" height="7" rx="1" />
              <path d="M14 17h7M17.5 14v7" />
            </svg>
          </button>
        </Panel>
        <Panel position="top-left" className="se-canvas-toolbar">
          <span className="se-canvas-toolbar-title">전략 빌더</span>
          <div className="se-canvas-interaction" role="group" aria-label="캔버스 조작 모드">
            <button
              type="button"
              className={`se-canvas-interaction-btn${interactionMode === 'pan' ? ' se-canvas-interaction-btn--on' : ''}`}
              title="빈 영역 드래그로 그래프 이동"
              onClick={() => handleInteractionModeChange('pan')}
            >
              이동
            </button>
            <button
              type="button"
              className={`se-canvas-interaction-btn${interactionMode === 'select' ? ' se-canvas-interaction-btn--on' : ''}`}
              title="드래그로 전략 요소 다중 선택"
              onClick={() => handleInteractionModeChange('select')}
            >
              드래그
            </button>
          </div>
          <span className="se-canvas-toolbar-hint">
            {interactionMode === 'pan'
              ? '빈 영역 드래그 → 그래프 이동 · 노드 드래그 → 위치 변경'
              : '빈 영역 드래그 → 다중 선택 · Del 삭제 · 연결선 × 끊기'}
            {orphans.length > 0 ? ` · 미연결 ${orphans.length}` : ''}
          </span>
        </Panel>
        {!root && (
          <Panel position="top-center" className="se-canvas-empty">
            우측에서 지표·연산자를 드래그하거나 클릭하여 조건을 구성하세요
          </Panel>
        )}
        <MultiSelectionDeleteButton onDelete={deleteSelectedNodes} />
      </ReactFlow>
    </div>
  );
}

export default function StrategyEditorCanvas(props: Props) {
  return <StrategyEditorCanvasInner {...props} />;
}
