import type { Connection, Edge, Node } from '@xyflow/react';
import type { LogicNode, ConditionDSL } from './strategyTypes';
import { nodeToText, type DefType } from './strategyEditorShared';
import {
  START_NODE_ID,
  isStartNodeId,
  normalizeStartCandleType,
  STRATEGY_CANDLE_TYPES,
  type StartNodeMeta,
} from './strategyStartNodes';

export { START_NODE_ID, isStartNodeId, STRATEGY_CANDLE_TYPES };
export type { StartNodeMeta };

const H_GAP = 280;
const V_GAP = 130;

export type HandleSide = 'top' | 'right' | 'bottom' | 'left';

const OPPOSE_SIDE: Record<HandleSide, HandleSide> = {
  top: 'bottom',
  bottom: 'top',
  left: 'right',
  right: 'left',
};

export type StrategyFlowNodeData = {
  logicNode?: LogicNode;
  label?: string;
  def?: DefType;
  signalTab?: 'buy' | 'sell';
  selected?: boolean;
  /** START 노드 — 조건 판별 시간봉 */
  candleType?: string;
  onStartCandleTypeChange?: (startId: string, candleType: string) => void;
  onDeleteStart?: (startId: string) => void;
  onDelete?: (id: string) => void;
  onUpdateCondition?: (nodeId: string, condition: ConditionDSL) => void;
  onDropTarget?: (targetId: string, data: { type: string; value: string; label: string }, flowPos: { x: number; y: number }) => void;
  onDragOverTarget?: (targetId: string, flowPos: { x: number; y: number }) => void;
  onDragLeaveTarget?: (targetId: string) => void;
  dragOver?: boolean;
  activeSourceSide?: HandleSide | null;
  /** 논리 트리상 자식을 가질 수 있으면 true (START·AND·OR·빈 NOT) */
  canSourceConnect?: boolean;
  /** 논리 트리상 부모를 가질 수 있으면 true (조건·연산자, START 제외) */
  canTargetConnect?: boolean;
  /** 전략 트리 미연결 — 편집기에만 표시, 수식·저장 제외 */
  isOrphan?: boolean;
};

export function isOrphanNode(orphans: LogicNode[], id: string): boolean {
  return orphans.some(o => o.id === id);
}

export function findLogicNode(
  root: LogicNode | null,
  orphans: LogicNode[],
  id: string,
  extraRoots: Record<string, LogicNode | null> = {},
): LogicNode | null {
  if (isStartNodeId(id)) return null;
  const inTree = findNodeInTree(root, id);
  if (inTree) return inTree;
  for (const extraRoot of Object.values(extraRoots)) {
    const hit = findNodeInTree(extraRoot, id);
    if (hit) return hit;
  }
  return orphans.find(o => o.id === id) ?? null;
}

/** 팔레트 드롭 시 트리에 즉시 연결되는 대상(START·트리 내 노드) */
export function isPaletteConnectTarget(
  anchorId: string,
  root: LogicNode | null,
  orphans: LogicNode[],
): boolean {
  if (isOrphanNode(orphans, anchorId)) return false;
  if (isStartNodeId(anchorId)) return true;
  if (!root) return false;
  return !!findNodeInTree(root, anchorId);
}

/** 노드별 연결 가능 역할 (DSL 트리 규칙) */
export function getNodeConnectionCaps(
  nodeId: string,
  logicNode: LogicNode | undefined,
): { canSource: boolean; canTarget: boolean } {
  if (nodeId === START_NODE_ID || isStartNodeId(nodeId)) return { canSource: true, canTarget: false };
  if (!logicNode) return { canSource: false, canTarget: false };
  switch (logicNode.type) {
    case 'CONDITION':
      return { canSource: false, canTarget: true };
    case 'NOT':
      return { canSource: (logicNode.children?.length ?? 0) < 1, canTarget: true };
    case 'AND':
    case 'OR':
      return { canSource: true, canTarget: true };
    default:
      return { canSource: false, canTarget: false };
  }
}

/** 팔레트 드롭 시 부모(출발) 노드로 쓸 수 있는지 */
export function canAcceptPaletteAsParent(anchorId: string, root: LogicNode | null): boolean {
  if (isStartNodeId(anchorId)) return true;
  if (!root) return false;
  const node = findNodeInTree(root, anchorId);
  if (!node || node.type === 'CONDITION') return false;
  return getNodeConnectionCaps(anchorId, node).canSource;
}

/**
 * DSL 부모→자식 방향으로 연결 가능한지 (source=부모, target=자식).
 * React Flow Loose 모드에서는 conn.source/target이 드래그 방향에 따라 뒤집힐 수 있음.
 */
export function isValidStrategyConnectionDirected(
  parentId: string,
  childId: string,
  root: LogicNode | null,
  orphans: LogicNode[] = [],
  extraRoots: Record<string, LogicNode | null> = {},
): boolean {
  if (!parentId || !childId || parentId === childId) return false;
  if (isStartNodeId(childId)) return false;

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

  if (childOrphan) {
    if (isStartNodeId(parentId)) return true;
    const parentNode = findNodeInTree(root, parentId)
      ?? Object.values(extraRoots).map(er => findNodeInTree(er, parentId)).find(Boolean)
      ?? null;
    if (!parentNode || !getNodeConnectionCaps(parentId, parentNode).canSource) return false;
    return true;
  }

  if (parentOrphan) {
    if (!getNodeConnectionCaps(parentId, parentOrphan).canSource) return false;
    const childNode = findNodeInTree(root, childId)
      ?? Object.values(extraRoots).map(er => findNodeInTree(er, childId)).find(Boolean)
      ?? null;
    if (!childNode || !getNodeConnectionCaps(childId, childNode).canTarget) return false;
    if (root && isDescendant(root, childId, parentId)) return false;
    for (const er of Object.values(extraRoots)) {
      if (er && isDescendant(er, childId, parentId)) return false;
    }
    return true;
  }

  if (!root && Object.values(extraRoots).every(r => !r)) return false;

  const parentNode = isStartNodeId(parentId)
    ? null
    : findNodeInTree(root, parentId)
      ?? Object.values(extraRoots).map(er => findNodeInTree(er, parentId)).find(Boolean)
      ?? null;
  const childNode = findNodeInTree(root, childId)
    ?? Object.values(extraRoots).map(er => findNodeInTree(er, childId)).find(Boolean)
    ?? null;
  if (!isStartNodeId(parentId) && !parentNode) return false;
  if (!childNode) return false;

  const parentCaps = getNodeConnectionCaps(parentId, parentNode ?? undefined);
  const childCaps = getNodeConnectionCaps(childId, childNode);
  if (!parentCaps.canSource || !childCaps.canTarget) return false;

  if (!isStartNodeId(parentId)) {
    if (root && isDescendant(root, childId, parentId)) return false;
    for (const er of Object.values(extraRoots)) {
      if (er && isDescendant(er, childId, parentId)) return false;
    }
  }
  return true;
}

/** 수동 연결·재연결 — 드래그 방향과 무관하게 부모→자식이 성립하면 유효 */
export function isValidStrategyConnection(
  conn: Connection,
  root: LogicNode | null,
  orphans: LogicNode[] = [],
  extraRoots: Record<string, LogicNode | null> = {},
): boolean {
  const { source, target } = conn;
  if (!source || !target) return false;
  return (
    isValidStrategyConnectionDirected(source, target, root, orphans, extraRoots)
    || isValidStrategyConnectionDirected(target, source, root, orphans, extraRoots)
  );
}

export function resolveStrategyConnection(
  conn: Connection,
  root: LogicNode | null,
  orphans: LogicNode[] = [],
  extraRoots: Record<string, LogicNode | null> = {},
): { parentId: string; childId: string } | null {
  const { source, target } = conn;
  if (!source || !target) return null;
  if (isValidStrategyConnectionDirected(source, target, root, orphans, extraRoots)) {
    return { parentId: source, childId: target };
  }
  if (isValidStrategyConnectionDirected(target, source, root, orphans, extraRoots)) {
    return { parentId: target, childId: source };
  }
  return null;
}

export function getNodeDimensions(nodeId: string): { w: number; h: number } {
  return isStartNodeId(nodeId)
    ? { w: FLOW_START_W, h: FLOW_START_H }
    : { w: FLOW_NODE_W, h: FLOW_NODE_H };
}

/** 커서/드롭 위치가 노드 중심 기준 어느 방향인지 */
export function pickSideFromPoint(
  flowPos: { x: number; y: number },
  nodePos: { x: number; y: number },
  dim: { w: number; h: number },
): HandleSide {
  const cx = nodePos.x + dim.w / 2;
  const cy = nodePos.y + dim.h / 2;
  const dx = flowPos.x - cx;
  const dy = flowPos.y - cy;
  if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? 'right' : 'left';
  return dy >= 0 ? 'bottom' : 'top';
}

/** 드롭이 앵커의 좌/우/상/하 쪽이면 해당 면 연결점 활성화 */
export function connectionSidesFromDrop(
  anchorPos: { x: number; y: number },
  anchorDim: { w: number; h: number },
  dropPos: { x: number; y: number },
): { sourceSide: HandleSide; targetSide: HandleSide; sourceHandle: string; targetHandle: string } {
  const sourceSide = pickSideFromPoint(dropPos, anchorPos, anchorDim);
  const targetSide = OPPOSE_SIDE[sourceSide];
  return {
    sourceSide,
    targetSide,
    sourceHandle: `s-${sourceSide}`,
    targetHandle: `t-${targetSide}`,
  };
}

export function spawnPositionNearAnchor(
  anchorPos: { x: number; y: number },
  anchorDim: { w: number; h: number },
  side: HandleSide,
): { x: number; y: number } {
  const gap = 48;
  const child = { w: FLOW_NODE_W, h: FLOW_NODE_H };
  switch (side) {
    case 'right':
      return { x: anchorPos.x + anchorDim.w + gap, y: anchorPos.y + anchorDim.h / 2 - child.h / 2 };
    case 'left':
      return { x: anchorPos.x - child.w - gap, y: anchorPos.y + anchorDim.h / 2 - child.h / 2 };
    case 'bottom':
      return { x: anchorPos.x + anchorDim.w / 2 - child.w / 2, y: anchorPos.y + anchorDim.h + gap };
    case 'top':
      return { x: anchorPos.x + anchorDim.w / 2 - child.w / 2, y: anchorPos.y - child.h - gap };
  }
}

export function findNodeAtFlowPosition(
  flowPos: { x: number; y: number },
  nodes: { id: string; position: { x: number; y: number } }[],
  padding = 16,
): { id: string; position: { x: number; y: number } } | null {
  for (let i = nodes.length - 1; i >= 0; i--) {
    const n = nodes[i];
    const dim = getNodeDimensions(n.id);
    if (
      flowPos.x >= n.position.x - padding
      && flowPos.x <= n.position.x + dim.w + padding
      && flowPos.y >= n.position.y - padding
      && flowPos.y <= n.position.y + dim.h + padding
    ) {
      return n;
    }
  }
  return null;
}

/** 미연결 고아 노드를 드래그해 다른 노드 위에 올렸을 때 연결 방향 해석 */
export function resolveOrphanDragConnection(
  orphanId: string,
  orphanPos: { x: number; y: number },
  nodes: { id: string; position: { x: number; y: number } }[],
  root: LogicNode | null,
  orphans: LogicNode[],
  extraRoots: Record<string, LogicNode | null> = {},
): { parentId: string; childId: string } | null {
  const dim = getNodeDimensions(orphanId);
  const center = {
    x: orphanPos.x + dim.w / 2,
    y: orphanPos.y + dim.h / 2,
  };
  const hit = findNodeAtFlowPosition(
    center,
    nodes.filter(n => n.id !== orphanId),
  );
  if (!hit) return null;
  if (isOrphanNode(orphans, hit.id)) return null;

  return resolveStrategyConnection(
    { source: hit.id, target: orphanId, sourceHandle: null, targetHandle: null },
    root,
    orphans,
    extraRoots,
  ) ?? resolveStrategyConnection(
    { source: orphanId, target: hit.id, sourceHandle: null, targetHandle: null },
    root,
    orphans,
    extraRoots,
  );
}

/** 드래그 연결 시 부모→자식 핸들 방향 */
export function buildConnectionFromPositions(
  parentId: string,
  childId: string,
  positions: Map<string, { x: number; y: number }>,
): Connection {
  const parentPos = positions.get(parentId) ?? { x: 0, y: 0 };
  const childPos = positions.get(childId) ?? { x: 0, y: 0 };
  const parentDim = getNodeDimensions(parentId);
  const childDim = getNodeDimensions(childId);
  const childCenter = {
    x: childPos.x + childDim.w / 2,
    y: childPos.y + childDim.h / 2,
  };
  const { sourceHandle, targetHandle } = connectionSidesFromDrop(parentPos, parentDim, childCenter);
  return {
    source: parentId,
    target: childId,
    sourceHandle,
    targetHandle,
  };
}

export type EdgeHandleBinding = {
  sourceHandle?: string | null;
  targetHandle?: string | null;
  /** smooth-step 경로 굽힘 중심 (중간 영역 드래그) */
  centerX?: number | null;
  centerY?: number | null;
  manual?: boolean;
  manualSource?: boolean;
  manualTarget?: boolean;
  manualCenter?: boolean;
};

export const FLOW_NODE_W = 200;
export const FLOW_NODE_H = 72;
export const FLOW_START_W = 168;
export const FLOW_START_H = 56;

function nodeCenter(
  id: string,
  positions: Map<string, { x: number; y: number }>,
): { x: number; y: number } {
  const p = positions.get(id) ?? { x: 0, y: 0 };
  const w = isStartNodeId(id) ? FLOW_START_W : FLOW_NODE_W;
  const h = isStartNodeId(id) ? FLOW_START_H : FLOW_NODE_H;
  return { x: p.x + w / 2, y: p.y + h / 2 };
}

/** 두 노드 상대 위치에 따라 최적 연결 방향(핸들) 선택 */
export function pickHandleSides(
  sourceId: string,
  targetId: string,
  positions: Map<string, { x: number; y: number }>,
): { sourceHandle: string; targetHandle: string } {
  const s = nodeCenter(sourceId, positions);
  const t = nodeCenter(targetId, positions);
  const dx = t.x - s.x;
  const dy = t.y - s.y;

  if (Math.abs(dx) >= Math.abs(dy)) {
    return dx >= 0
      ? { sourceHandle: 's-right', targetHandle: 't-left' }
      : { sourceHandle: 's-left', targetHandle: 't-right' };
  }
  return dy >= 0
    ? { sourceHandle: 's-bottom', targetHandle: 't-top' }
    : { sourceHandle: 's-top', targetHandle: 't-bottom' };
}

/** 경로 굽힘 중심 기준 — 양쪽 노드에 가장 자연스러운 핸들 */
export function pickHandlesFromCenter(
  sourceId: string,
  targetId: string,
  center: { x: number; y: number },
  positions: Map<string, { x: number; y: number }>,
): { sourceHandle: string; targetHandle: string } {
  const sourcePos = positions.get(sourceId) ?? { x: 0, y: 0 };
  const targetPos = positions.get(targetId) ?? { x: 0, y: 0 };
  const sourceDim = getNodeDimensions(sourceId);
  const targetDim = getNodeDimensions(targetId);
  const sourceSide = pickSideFromPoint(center, sourcePos, sourceDim);
  const targetSide = pickSideFromPoint(center, targetPos, targetDim);
  return {
    sourceHandle: `s-${sourceSide}`,
    targetHandle: `t-${targetSide}`,
  };
}

export function applyEdgeHandles(
  edges: Edge[],
  positions: Map<string, { x: number; y: number }>,
  saved: Map<string, EdgeHandleBinding>,
): Edge[] {
  return edges.map(edge => {
    const binding = saved.get(edge.id) ?? {};
    const picked = pickHandleSides(edge.source, edge.target, positions);
    let sourceHandle = picked.sourceHandle;
    let targetHandle = picked.targetHandle;

    if (
      binding.manualCenter
      && binding.centerX != null
      && binding.centerY != null
    ) {
      const natural = pickHandlesFromCenter(
        edge.source,
        edge.target,
        { x: binding.centerX, y: binding.centerY },
        positions,
      );
      if (!binding.manualSource) sourceHandle = natural.sourceHandle;
      else sourceHandle = binding.sourceHandle ?? picked.sourceHandle;
      if (!binding.manualTarget) targetHandle = natural.targetHandle;
      else targetHandle = binding.targetHandle ?? picked.targetHandle;
    } else {
      if (binding.manualSource) sourceHandle = binding.sourceHandle ?? picked.sourceHandle;
      if (binding.manualTarget) targetHandle = binding.targetHandle ?? picked.targetHandle;
    }

    const nextBinding: EdgeHandleBinding = {
      ...binding,
      sourceHandle,
      targetHandle,
    };
    saved.set(edge.id, nextBinding);

    return {
      ...edge,
      sourceHandle,
      targetHandle,
      data: { ...(edge.data as object), binding: nextBinding },
    };
  });
}

function layoutTree(
  node: LogicNode,
  parentId: string,
  depth: number,
  siblingIndex: number,
  siblingCount: number,
  nodes: Node[],
  edges: Edge[],
  positions: Map<string, { x: number; y: number }>,
  def: DefType,
  signalTab: 'buy' | 'sell',
  callbacks: Pick<StrategyFlowNodeData, 'onDelete' | 'onUpdateCondition' | 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'>,
  dragOverId: string | null,
): void {
  const yBase = 220;
  const y = yBase + (siblingIndex - (siblingCount - 1) / 2) * V_GAP;
  const pos = positions.get(node.id) ?? { x: 80 + depth * H_GAP, y };
  const flowType = node.type === 'CONDITION' ? 'condition' : 'logic';
  const caps = getNodeConnectionCaps(node.id, node);

  nodes.push({
    id: node.id,
    type: flowType,
    position: pos,
    data: {
      logicNode: node,
      label: nodeToText(node, def),
      def,
      signalTab,
      onDelete: callbacks.onDelete,
      onUpdateCondition: callbacks.onUpdateCondition,
      onDropTarget: callbacks.onDropTarget,
      onDragOverTarget: callbacks.onDragOverTarget,
      onDragLeaveTarget: callbacks.onDragLeaveTarget,
      dragOver: dragOverId === node.id,
      canSourceConnect: caps.canSource,
      canTargetConnect: caps.canTarget,
    } satisfies StrategyFlowNodeData,
    draggable: true,
  });

  edges.push({
    id: `${parentId}-${node.id}`,
    source: parentId,
    target: node.id,
    type: 'strategy',
    animated: node.type === 'CONDITION',
    className: 'se-flow-edge',
  });

  const children = node.children ?? [];
  children.forEach((child, i) => {
    layoutTree(child, node.id, depth + 1, i, children.length, nodes, edges, positions, def, signalTab, callbacks, dragOverId);
  });
}

const AUTO_LAYOUT_ORIGIN_X = 48;
const AUTO_LAYOUT_ORIGIN_Y = 48;
const AUTO_LAYOUT_SECTION_GAP = 72;
const AUTO_LAYOUT_START_TREE_GAP = 72;
const AUTO_LAYOUT_ORPHAN_GAP = 100;

function measureSubtreeHeight(node: LogicNode): number {
  const children = node.children ?? [];
  if (children.length === 0) return FLOW_NODE_H;
  const childHeights = children.map(measureSubtreeHeight);
  return childHeights.reduce((sum, h) => sum + h, 0) + (children.length - 1) * V_GAP;
}

function placeSubtree(
  node: LogicNode,
  x: number,
  centerY: number,
  positions: Map<string, { x: number; y: number }>,
): void {
  const children = node.children ?? [];
  if (children.length === 0) {
    positions.set(node.id, { x, y: centerY - FLOW_NODE_H / 2 });
    return;
  }
  const childHeights = children.map(measureSubtreeHeight);
  const totalHeight = childHeights.reduce((a, b) => a + b, 0) + (children.length - 1) * V_GAP;
  let y = centerY - totalHeight / 2;
  children.forEach((child, i) => {
    const ch = childHeights[i];
    placeSubtree(child, x + H_GAP, y + ch / 2, positions);
    y += ch + V_GAP;
  });
  positions.set(node.id, { x, y: centerY - FLOW_NODE_H / 2 });
}

function maxLayoutX(positions: Map<string, { x: number; y: number }>): number {
  let max = 0;
  for (const [id, pos] of positions) {
    const dim = getNodeDimensions(id);
    max = Math.max(max, pos.x + dim.w);
  }
  return max;
}

/** 그래프 캔버스 자동 정렬 — START별 트리를 좌→우, 형제는 위→아래 배치 */
export function computeAutoFlowLayout(
  root: LogicNode | null,
  extraStartIds: string[],
  extraRoots: Record<string, LogicNode | null>,
  orphans: LogicNode[],
): Record<string, { x: number; y: number }> {
  const positions = new Map<string, { x: number; y: number }>();
  let yCursor = AUTO_LAYOUT_ORIGIN_Y;

  const placeStartSection = (startId: string, branchRoot: LogicNode | null) => {
    const startDim = getNodeDimensions(startId);
    const branchHeight = branchRoot ? measureSubtreeHeight(branchRoot) : 0;
    const sectionHeight = Math.max(startDim.h, branchHeight);
    const centerY = yCursor + sectionHeight / 2;

    positions.set(startId, {
      x: AUTO_LAYOUT_ORIGIN_X,
      y: centerY - startDim.h / 2,
    });

    if (branchRoot) {
      placeSubtree(
        branchRoot,
        AUTO_LAYOUT_ORIGIN_X + startDim.w + AUTO_LAYOUT_START_TREE_GAP,
        centerY,
        positions,
      );
    }

    yCursor += sectionHeight + AUTO_LAYOUT_SECTION_GAP;
  };

  placeStartSection(START_NODE_ID, root);
  for (const startId of extraStartIds) {
    placeStartSection(startId, extraRoots[startId] ?? null);
  }

  if (orphans.length > 0) {
    let orphanY = AUTO_LAYOUT_ORIGIN_Y;
    const orphanX = (positions.size > 0 ? maxLayoutX(positions) : AUTO_LAYOUT_ORIGIN_X) + AUTO_LAYOUT_ORPHAN_GAP;
    for (const orphan of orphans) {
      const h = measureSubtreeHeight(orphan);
      positions.set(orphan.id, { x: orphanX, y: orphanY });
      orphanY += h + V_GAP;
    }
  }

  return Object.fromEntries(positions);
}

function appendOrphanFlowNodes(
  orphans: LogicNode[],
  nodes: Node[],
  positions: Map<string, { x: number; y: number }>,
  def: DefType,
  signalTab: 'buy' | 'sell',
  callbacks: Pick<StrategyFlowNodeData, 'onDelete' | 'onUpdateCondition' | 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'>,
): void {
  let orphanY = 80;
  for (const node of orphans) {
    const pos = positions.get(node.id) ?? { x: 40, y: orphanY };
    orphanY += V_GAP;
    const flowType = node.type === 'CONDITION' ? 'condition' : 'logic';
    const caps = getNodeConnectionCaps(node.id, node);
    nodes.push({
      id: node.id,
      type: flowType,
      position: pos,
      className: 'se-flow-node-orphan',
      data: {
        logicNode: node,
        label: nodeToText(node, def),
        def,
        signalTab,
        onDelete: callbacks.onDelete,
        onUpdateCondition: callbacks.onUpdateCondition,
        onDropTarget: callbacks.onDropTarget,
        onDragOverTarget: callbacks.onDragOverTarget,
        onDragLeaveTarget: callbacks.onDragLeaveTarget,
        canSourceConnect: caps.canSource,
        canTargetConnect: caps.canTarget,
        isOrphan: true,
      } satisfies StrategyFlowNodeData,
      draggable: true,
      selectable: true,
    });
  }
}

export function logicNodeToFlow(
  root: LogicNode | null,
  def: DefType,
  signalTab: 'buy' | 'sell',
  positions: Map<string, { x: number; y: number }>,
  callbacks: Pick<
    StrategyFlowNodeData,
    | 'onDelete'
    | 'onUpdateCondition'
    | 'onDropTarget'
    | 'onDragOverTarget'
    | 'onDragLeaveTarget'
    | 'onStartCandleTypeChange'
    | 'onDeleteStart'
  >,
  dragOverId: string | null = null,
  orphans: LogicNode[] = [],
  startMeta: Record<string, StartNodeMeta> = { [START_NODE_ID]: { candleType: '1m' } },
  extraStartIds: string[] = [],
  extraRoots: Record<string, LogicNode | null> = {},
): { nodes: Node[]; edges: Edge[] } {
  const nodes: Node[] = [];
  const edges: Edge[] = [];

  const appendStart = (startId: string, branchRoot: LogicNode | null, defaultY: number) => {
    const candleType = normalizeStartCandleType(startMeta[startId]?.candleType);
    nodes.push({
      id: startId,
      type: 'start',
      position: positions.get(startId) ?? { x: 0, y: defaultY },
      data: {
        label: 'START',
        signalTab,
        candleType,
        onStartCandleTypeChange: callbacks.onStartCandleTypeChange,
        onDeleteStart: startId === START_NODE_ID ? undefined : callbacks.onDeleteStart,
        onDropTarget: callbacks.onDropTarget,
        onDragOverTarget: callbacks.onDragOverTarget,
        onDragLeaveTarget: callbacks.onDragLeaveTarget,
        canSourceConnect: true,
        canTargetConnect: false,
      } satisfies StrategyFlowNodeData,
      draggable: true,
      selectable: true,
    });

    if (branchRoot) {
      layoutTree(
        branchRoot,
        startId,
        1,
        0,
        1,
        nodes,
        edges,
        positions,
        def,
        signalTab,
        callbacks,
        dragOverId,
      );
    }
  };

  appendStart(START_NODE_ID, root, 220);
  extraStartIds.forEach((startId, index) => {
    appendStart(startId, extraRoots[startId] ?? null, 80 + index * 140);
  });

  appendOrphanFlowNodes(orphans, nodes, positions, def, signalTab, callbacks);

  return { nodes, edges };
}

export function getIndicatorPeriodLabel(indicator: string, def: DefType): string {
  switch (indicator) {
    case 'RSI': return String(def.rsiPeriod);
    case 'MACD': return `${def.macdFast}/${def.macdSlow}`;
    case 'CCI': return String(def.cciPeriod);
    case 'STOCHASTIC': return `${def.stochK}/${def.stochD}`;
    case 'ADX': return String(def.adxPeriod);
    case 'MA': return String(def.maLines[0] ?? 5);
    case 'EMA': return String(def.maLines[0] ?? 5);
    case 'BOLLINGER': return String(def.bbPeriod);
    default: return '';
  }
}

/** 트리에서 노드 추출 (부모에서 제거 후 반환) */
export function extractNode(root: LogicNode, id: string): { tree: LogicNode | null; node: LogicNode | null } {
  if (root.id === id) return { tree: null, node: root };
  if (!root.children?.length) return { tree: root, node: null };

  for (let i = 0; i < root.children.length; i++) {
    const child = root.children[i];
    if (child.id === id) {
      return {
        tree: { ...root, children: root.children.filter(c => c.id !== id) },
        node: child,
      };
    }
    const sub = extractNode(child, id);
    if (sub.node) {
      const newChildren = root.children.map(c => (c.id === child.id ? sub.tree! : c)).filter(Boolean) as LogicNode[];
      return { tree: { ...root, children: newChildren }, node: sub.node };
    }
  }
  return { tree: root, node: null };
}

export function findNodeInTree(root: LogicNode | null, id: string): LogicNode | null {
  if (!root) return null;
  if (root.id === id) return root;
  for (const c of root.children ?? []) {
    const hit = findNodeInTree(c, id);
    if (hit) return hit;
  }
  return null;
}

/** 트리에서 nodeId의 직계 부모 ID (루트의 부모는 START) */
export function findParentIdInTree(
  root: LogicNode,
  nodeId: string,
  parentId: string = START_NODE_ID,
): string | null {
  if (root.id === nodeId) return parentId;
  for (const c of root.children ?? []) {
    const hit = findParentIdInTree(c, nodeId, root.id);
    if (hit) return hit;
  }
  return null;
}

/** 연결 해제 시 미연결 목록으로 펼침 — Logic Expression·캔버스 모두 트리에서 제외 */
export function subtreeToFlatOrphans(node: LogicNode): LogicNode[] {
  switch (node.type) {
    case 'CONDITION':
      return [{ ...node, children: undefined }];
    case 'NOT': {
      const inner = node.children?.[0];
      return inner ? subtreeToFlatOrphans(inner) : [];
    }
    case 'AND':
    case 'OR': {
      const kids = node.children ?? [];
      if (kids.length === 0) return [];
      return kids.flatMap(subtreeToFlatOrphans);
    }
    default:
      return [];
  }
}

/** 자식이 없는 AND/OR/NOT 제거 — 수식에서 (빈 AND) 등 방지 */
export function pruneEmptyGates(root: LogicNode | null): LogicNode | null {
  if (!root) return null;
  if (root.type === 'CONDITION') return root;

  const prunedChildren = (root.children ?? [])
    .map(c => pruneEmptyGates(c))
    .filter((c): c is LogicNode => c != null);

  if (root.type === 'NOT') {
    if (prunedChildren.length === 0) return null;
    return { ...root, children: [prunedChildren[0]] };
  }

  if (root.type === 'AND' || root.type === 'OR') {
    if (prunedChildren.length === 0) return null;
    return { ...root, children: prunedChildren };
  }

  return root;
}

function mergeOrphans(existing: LogicNode[], added: LogicNode[]): LogicNode[] {
  const seen = new Set(existing.map(o => o.id));
  const next = [...existing];
  for (const o of added) {
    if (!seen.has(o.id)) {
      seen.add(o.id);
      next.push(o);
    }
  }
  return next;
}

function resolveDisconnectEndpoints(
  sourceId: string,
  targetId: string,
  root: LogicNode | null,
  orphans: LogicNode[],
  extraRoots: Record<string, LogicNode | null>,
): { parentId: string; childId: string } | null {
  const conn = { source: sourceId, target: targetId, sourceHandle: null, targetHandle: null };
  const resolved = resolveStrategyConnection(conn, root, orphans, extraRoots);
  if (resolved) return resolved;

  if (isStartNodeId(sourceId)) {
    const branchRoot = sourceId === START_NODE_ID ? root : extraRoots[sourceId] ?? null;
    if (branchRoot?.id === targetId) {
      return { parentId: sourceId, childId: targetId };
    }
  }

  if (root) {
    const parentInTree = findParentIdInTree(root, targetId);
    if (parentInTree === sourceId) {
      return { parentId: sourceId, childId: targetId };
    }
  }

  for (const [startId, branchRoot] of Object.entries(extraRoots)) {
    if (!branchRoot) continue;
    const parentInTree = findParentIdInTree(branchRoot, targetId, startId);
    if (parentInTree === sourceId) {
      return { parentId: sourceId, childId: targetId };
    }
  }

  return null;
}

export function isDescendant(root: LogicNode, ancestorId: string, nodeId: string): boolean {
  const ancestor = findNodeInTree(root, ancestorId);
  if (!ancestor) return false;
  return findNodeInTree(ancestor, nodeId) != null && ancestorId !== nodeId;
}

export function disconnectTreeEdge(
  sourceId: string,
  targetId: string,
  root: LogicNode | null,
  orphans: LogicNode[],
  extraRoots: Record<string, LogicNode | null> = {},
): {
  root: LogicNode | null;
  orphans: LogicNode[];
  extraRoots: Record<string, LogicNode | null>;
} {
  const endpoints = resolveDisconnectEndpoints(sourceId, targetId, root, orphans, extraRoots);
  if (!endpoints) return { root, orphans, extraRoots };

  const { parentId, childId } = endpoints;
  if (isStartNodeId(childId)) return { root, orphans, extraRoots };

  const inPrimary = root && (root.id === childId || !!findNodeInTree(root, childId));
  const inExtraStartId = Object.entries(extraRoots).find(([, branch]) =>
    branch && (branch.id === childId || !!findNodeInTree(branch, childId)),
  )?.[0];

  if (!inPrimary && !inExtraStartId) return { root, orphans, extraRoots };

  if (isStartNodeId(parentId)) {
    if (parentId === START_NODE_ID && root?.id === childId) {
      return {
        root: null,
        orphans: mergeOrphans(orphans, subtreeToFlatOrphans(root)),
        extraRoots,
      };
    }
    const branchRoot = extraRoots[parentId] ?? null;
    if (branchRoot?.id === childId) {
      const nextExtra = { ...extraRoots, [parentId]: null };
      return {
        root,
        orphans: mergeOrphans(orphans, subtreeToFlatOrphans(branchRoot)),
        extraRoots: nextExtra,
      };
    }
  }

  const extractFrom = inPrimary ? root! : extraRoots[inExtraStartId!]!;
  const { tree, node } = extractNode(extractFrom, childId);
  if (!node) return { root, orphans, extraRoots };

  if (inPrimary) {
    return {
      root: pruneEmptyGates(tree),
      orphans: mergeOrphans(orphans, subtreeToFlatOrphans(node)),
      extraRoots,
    };
  }

  return {
    root,
    orphans: mergeOrphans(orphans, subtreeToFlatOrphans(node)),
    extraRoots: {
      ...extraRoots,
      [inExtraStartId!]: pruneEmptyGates(tree),
    },
  };
}
