<template>
  <section class="page-grid">
    <section class="panel dashboard-order-panel">
      <div class="panel-header dashboard-order-header">
        <div>
          <p class="eyebrow">01 经营总览</p>
          <span
            class="title-with-help"
            title="点击库存批次号，查看该批材料从入库成本、生产出库、生产报工、成品入库到后续发货的完整流转 DAG。"
          >
            <h3>库存批次号生命周期追踪</h3>
          </span>
        </div>
        <div class="workflow-inline-actions">
          <span class="panel-tag">原材料驱动全链路</span>
          <button class="ghost-button" type="button" :disabled="loadingMaterialLots" @click="loadMaterialLots">
            {{ loadingMaterialLots ? "刷新中..." : "刷新库存批次" }}
          </button>
        </div>
      </div>

      <TableControls
        v-model:search="materialControls.search.value"
        v-model:sort-key="materialControls.sortKey.value"
        v-model:sort-direction="materialControls.sortDirection.value"
        :sort-options="materialControls.sortOptions"
        placeholder="搜索库存批次号、编码、名称、仓库、状态"
      />

      <div class="table-wrap">
        <table class="data-table dashboard-order-table">
          <thead>
            <tr>
              <th>库存批次号</th>
              <th>原材料</th>
              <th>仓库</th>
              <th>入库重量</th>
              <th>剩余重量</th>
              <th>入库单价</th>
              <th>入库金额</th>
              <th>生产出库数</th>
              <th>已领重量</th>
              <th>成品批次</th>
              <th>成品数量</th>
              <th>发货次数</th>
              <th>状态</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="lot in paginatedMaterialLots" :key="lot.lot_id">
              <td>
                <button class="link-button strong-link" type="button" @click="openLifecycle(lot)">
                  {{ lot.lot_no }}
                </button>
              </td>
              <td>
                <strong>{{ lot.material_name }}</strong>
                <small>{{ lot.material_code }}</small>
              </td>
              <td>{{ lot.warehouse_name }}</td>
              <td>{{ formatWeight(lot.inbound_weight_kg) }}</td>
              <td>{{ formatWeight(lot.remaining_weight_kg) }}</td>
              <td>¥{{ formatAmount(lot.unit_cost) }}/kg</td>
              <td>¥{{ formatAmount(lot.inbound_amount) }}</td>
              <td>{{ lot.issued_batch_count }}</td>
              <td>{{ formatWeight(lot.issued_weight_kg) }}</td>
              <td>{{ lot.finished_batch_count }}</td>
              <td>{{ formatQty(lot.finished_qty) }}</td>
              <td>{{ lot.delivery_count }}</td>
              <td>
                <span class="table-chip" :class="statusToneClass(lot.status)">
                  {{ formatStatusLabel(lot.status) }}
                </span>
              </td>
            </tr>
            <tr v-if="!filteredMaterialLots.length">
              <td colspan="13" class="empty-row">暂无原材料库存批次。请先在 03 到货入库并完成质检放行。</td>
            </tr>
          </tbody>
        </table>
      </div>
      <PaginationBar v-model:page="materialPage" v-model:page-size="materialPageSize" :total="filteredMaterialLots.length" />
    </section>

    <Teleport to="body">
      <div v-if="lifecycleOpen" class="workflow-drawer-mask lifecycle-modal-mask" @click.self="closeLifecycle">
        <section class="lifecycle-modal">
          <header class="lifecycle-modal-head">
            <div>
              <p class="eyebrow">原材料生命周期 DAG</p>
              <h3>{{ lifecycle?.lot?.lot_no || activeMaterialLot?.lot_no || "库存批次号流转" }}</h3>
              <p>{{ lifecycle?.lot ? `${lifecycle.lot.material_code} · ${lifecycle.lot.material_name}` : activeMaterialLot?.material_name || "-" }}</p>
            </div>
            <div class="workflow-inline-actions">
              <label class="dag-click-lock-toggle">
                <input v-model="clickHighlightEnabled" type="checkbox" @change="handleClickHighlightModeChange" />
                <span>点击常亮</span>
              </label>
              <span class="table-chip">拖动画布 · 点击节点看详情</span>
              <button class="ghost-button" type="button" title="放大" @click="zoomCanvasBy(0.08)">放大</button>
              <button class="ghost-button" type="button" title="缩小" @click="zoomCanvasBy(-0.08)">缩小</button>
              <button class="ghost-button" type="button" @click="resetCanvas">重置视图</button>
              <button class="ghost-button" type="button" @click="closeLifecycle">关闭</button>
            </div>
          </header>

          <div v-if="loadingLifecycle" class="dag-loading-card">正在加载库存批次号全链路数据...</div>

          <div v-else class="lifecycle-content">
            <div
              ref="viewportRef"
              class="dag-viewport"
              @mousedown="startPan"
              @mousemove="movePan"
              @mouseup="stopPan"
              @mouseleave="handleDagViewportMouseLeave"
            >
              <div class="dag-transform" :style="canvasTransformStyle">
                <div class="dag-canvas" :style="{ width: `${canvasSize.width}px`, height: `${canvasSize.height}px` }">
                  <div
                    v-for="stage in stageBands"
                    :key="stage.level"
                    class="dag-stage-band"
                    :style="{ left: `${stage.left}px`, width: `${stage.width}px` }"
                  >
                    <span>{{ stage.label }}</span>
                  </div>

                  <svg class="dag-edge-layer" :width="canvasSize.width" :height="canvasSize.height">
                    <defs>
                      <marker
                        id="dagArrow"
                        markerWidth="14"
                        markerHeight="12"
                        refX="12"
                        refY="6"
                        orient="auto"
                        markerUnits="userSpaceOnUse"
                        viewBox="0 0 14 12"
                      >
                        <path d="M1,1 L13,6 L1,11 L4.2,6 Z" fill="#9fdbff" />
                      </marker>
                      <marker
                        id="dagArrowHot"
                        markerWidth="18"
                        markerHeight="16"
                        refX="16"
                        refY="8"
                        orient="auto"
                        markerUnits="userSpaceOnUse"
                        viewBox="0 0 18 16"
                      >
                        <path d="M1,1 L17,8 L1,15 L5.2,8 Z" fill="#f7fdff" />
                      </marker>
                    </defs>
                    <path
                      v-for="edge in layoutEdges"
                      :key="`${edge.id}-hit`"
                      class="dag-edge-hit"
                      :d="edge.path"
                      @mousedown.stop
                      @click.stop="handleDagEdgeClick(edge)"
                      @mouseenter="hoveredDagEdgeId = edge.id"
                      @mouseleave="hoveredDagEdgeId = null"
                    />
                    <path
                      v-for="edge in layoutEdges"
                      :key="edge.id"
                      class="dag-edge-path"
                      :class="[
                        `dag-edge-${edge.tone}`,
                        {
                          'dag-edge-highlighted': isEdgeHighlighted(edge),
                          'dag-edge-dimmed': isEdgeDimmed(edge)
                        }
                      ]"
                      :d="edge.path"
                      :marker-end="isEdgeHighlighted(edge) ? 'url(#dagArrowHot)' : 'url(#dagArrow)'"
                    />
                    <circle
                      v-for="edge in layoutEdges"
                      :key="`${edge.id}-start`"
                      class="dag-edge-start"
                      :class="[
                        `dag-edge-start-${edge.tone}`,
                        {
                          'dag-edge-dot-highlighted': isEdgeHighlighted(edge),
                          'dag-edge-dot-dimmed': isEdgeDimmed(edge)
                        }
                      ]"
                      :cx="edge.startX"
                      :cy="edge.startY"
                      :r="isEdgeHighlighted(edge) ? 7 : 4.4"
                    />
                    <circle
                      v-for="edge in layoutEdges"
                      :key="`${edge.id}-terminal`"
                      class="dag-edge-terminal"
                      :class="[
                        `dag-edge-terminal-${edge.tone}`,
                        {
                          'dag-edge-dot-highlighted': isEdgeHighlighted(edge),
                          'dag-edge-dot-dimmed': isEdgeDimmed(edge)
                        }
                      ]"
                      :cx="edge.endX - 7"
                      :cy="edge.endY"
                      :r="isEdgeHighlighted(edge) ? 6 : 3.2"
                    />
                    <text
                      v-for="edge in layoutEdgesWithLabel"
                      :key="`${edge.id}-label`"
                      class="dag-edge-label"
                      :class="{
                        'dag-edge-label-highlighted': isEdgeHighlighted(edge),
                        'dag-edge-label-dimmed': isEdgeDimmed(edge)
                      }"
                      :x="edge.labelX"
                      :y="edge.labelY"
                    >
                      {{ edge.label }}
                    </text>
                  </svg>

                  <button
                    v-for="node in layoutNodes"
                    :key="node.id"
                    class="dag-node-card"
                    :class="[
                      `dag-node-${node.tone}`,
                      {
                        'dag-node-active': selectedNode?.id === node.id,
                        'dag-node-locked': persistentDagNodeIds.has(node.id)
                      }
                    ]"
                    type="button"
                    :style="{ left: `${node.x}px`, top: `${node.y}px` }"
                    @mousedown.stop
                    @mouseenter="hoveredDagNodeId = node.id"
                    @mouseleave="hoveredDagNodeId = null"
                    @click="handleDagNodeClick(node)"
                  >
                    <span class="dag-node-stage">{{ node.stage }}</span>
                    <strong>{{ node.title }}</strong>
                    <small>{{ node.subtitle || formatDashboardNodeTypeLabel(node.type) }}</small>
                    <div class="dag-node-footer">
                      <span>{{ displayStatus(node.status) }}</span>
                      <span>{{ node.metrics?.[0]?.value || "" }}</span>
                    </div>
                  </button>
                </div>
              </div>
            </div>

            <aside class="dag-detail-panel">
              <div v-if="selectedNode">
                <p class="eyebrow">{{ selectedNode.stage }}</p>
                <h3>{{ selectedNode.title }}</h3>
                <span class="table-chip" :class="`node-chip-${selectedNode.tone}`">
                  {{ displayStatus(selectedNode.status) }}
                </span>
                <p class="dag-detail-summary">{{ selectedNode.summary || "该节点暂无摘要。" }}</p>

                <div class="dag-detail-metrics">
                  <article v-for="item in selectedNode.metrics" :key="`${selectedNode.id}-${item.label}`">
                    <span>{{ item.label }}</span>
                    <strong>{{ item.value }}</strong>
                  </article>
                </div>

                <dl class="dag-detail-list">
                  <template v-for="item in selectedNode.details" :key="`${selectedNode.id}-${item.label}`">
                    <dt>{{ item.label }}</dt>
                    <dd>{{ item.value }}</dd>
                  </template>
                </dl>
              </div>
              <div v-else class="workflow-empty-card">点击任意节点查看详细内容。</div>
            </aside>
          </div>
        </section>
      </div>
    </Teleport>
  </section>
</template>

<script setup>
import { computed, onMounted, ref } from "vue";

import PaginationBar from "../components/PaginationBar.vue";
import TableControls from "../components/TableControls.vue";
import { useBodyScrollLock } from "../composables/useBodyScrollLock";
import { requestJson } from "../services/api";
import { formatDashboardNodeTypeLabel, formatStatusLabel } from "../utils/dictionaries";
import { formatAmount, formatQty, formatWeight } from "../utils/formatters";
import { notifyError } from "../utils/notifications";
import { usePagination } from "../utils/pagination";
import { useTableControls } from "../utils/tableControls";

const NODE_WIDTH = 250;
const NODE_HEIGHT = 122;
const BASE_LEVEL_GAP = 430;
const MAX_LEVEL_GAP = 1080;
const EDGE_GAP_PER_LINE = 68;
const ROW_GAP = 168;
const LEFT_OFFSET = 78;
const TOP_OFFSET = 104;
const REPORT_ROW_GAP = 150;
const BRANCH_GAP = 46;

const materialLots = ref([]);
const loadingMaterialLots = ref(false);
const loadingLifecycle = ref(false);
const lifecycleOpen = ref(false);
const activeMaterialLot = ref(null);
const lifecycle = ref(null);
const selectedNode = ref(null);
const hoveredDagNodeId = ref(null);
const hoveredDagEdgeId = ref(null);
const clickHighlightEnabled = ref(false);
const persistentDagNodeIds = ref(new Set());
const persistentDagEdgeIds = ref(new Set());
const mutedPersistentDagEdgeIds = ref(new Set());
const viewportRef = ref(null);
const pan = ref({ x: 24, y: 38 });
const scale = ref(0.82);
const dragState = ref({ active: false, startX: 0, startY: 0, originX: 0, originY: 0 });

const materialControls = useTableControls(materialLots, {
  searchFields: ["lot_no", "material_code", "material_name", "specification", "warehouse_name", "status", "quality_status"],
  sortOptions: [
    { key: "lot_no", label: "库存批次号" },
    { key: "material_code", label: "原材料编码" },
    { key: "material_name", label: "原材料名称" },
    { key: "warehouse_name", label: "仓库" },
    { key: "inbound_weight_kg", label: "入库重量" },
    { key: "remaining_weight_kg", label: "剩余重量" },
    { key: "unit_cost", label: "入库单价" },
    { key: "inbound_amount", label: "入库金额" },
    { key: "issued_batch_count", label: "生产出库数" },
    { key: "issued_weight_kg", label: "已领重量" },
    { key: "finished_batch_count", label: "成品批次" },
    { key: "delivery_count", label: "发货次数" },
    { key: "status", label: "状态" }
  ],
  defaultSortKey: "created_at",
  defaultSortDirection: "desc"
});
const filteredMaterialLots = materialControls.rows;
const {
  page: materialPage,
  pageSize: materialPageSize,
  rows: paginatedMaterialLots
} = usePagination(filteredMaterialLots);

const layoutNodes = computed(() => {
  const nodes = lifecycle.value?.nodes || [];
  if (!nodes.length) {
    return [];
  }

  const edges = lifecycle.value?.edges || [];
  const nodeById = new Map(nodes.map((node, index) => [node.id, { ...node, __index: index }]));
  const childrenBySource = new Map();
  const parentByTarget = new Map();
  edges.forEach((edge) => {
    if (!childrenBySource.has(edge.source)) {
      childrenBySource.set(edge.source, []);
    }
    childrenBySource.get(edge.source).push(edge.target);
    if (!parentByTarget.has(edge.target)) {
      parentByTarget.set(edge.target, edge.source);
    }
  });

  const levelX = buildAdaptiveLevelX(nodes, edges);
  const yById = new Map();
  const operationNodes = nodes
    .filter((node) => node.type === "work_order_operation")
    .sort(compareDagNode);
  const reportNodes = nodes
    .filter((node) => node.type === "operation_report")
    .sort(compareDagNode);
  const reportGroups = new Map();
  reportNodes.forEach((node) => {
    const parentId = parentByTarget.get(node.id) || "__unlinked_reports";
    if (!reportGroups.has(parentId)) {
      reportGroups.set(parentId, []);
    }
    reportGroups.get(parentId).push(node);
  });

  let cursorY = TOP_OFFSET;
  operationNodes.forEach((operation) => {
    const reports = reportGroups.get(operation.id) || [];
    const rowCount = Math.max(1, reports.length);
    const groupHeight = Math.max(NODE_HEIGHT, rowCount * NODE_HEIGHT + (rowCount - 1) * (REPORT_ROW_GAP - NODE_HEIGHT));
    yById.set(operation.id, cursorY + Math.max(0, (groupHeight - NODE_HEIGHT) / 2));
    reports.forEach((report, index) => {
      yById.set(report.id, cursorY + index * REPORT_ROW_GAP);
    });
    cursorY += groupHeight + BRANCH_GAP;
  });

  const unlinkedReports = reportGroups.get("__unlinked_reports") || [];
  unlinkedReports.forEach((report) => {
    if (!yById.has(report.id)) {
      yById.set(report.id, cursorY);
      cursorY += REPORT_ROW_GAP;
    }
  });

  const branchHeight = Math.max(NODE_HEIGHT, cursorY - TOP_OFFSET - BRANCH_GAP);
  const levels = new Map();
  nodes.forEach((node) => {
    if (!levels.has(node.level)) {
      levels.set(node.level, []);
    }
    levels.get(node.level).push(node);
  });

  Array.from(levels.entries()).forEach(([level, rows]) => {
    const sortedRows = rows.slice().sort(compareDagNode);
    if (level <= 2) {
      const distributed = distributeLevelRows(sortedRows, branchHeight);
      distributed.forEach(({ node, y }) => yById.set(node.id, y));
      return;
    }

    sortedRows.forEach((node, index) => {
      if (yById.has(node.id)) {
        return;
      }
      const parent = nodeById.get(parentByTarget.get(node.id));
      const parentY = parent ? yById.get(parent.id) : null;
      yById.set(node.id, Number.isFinite(parentY) ? parentY : TOP_OFFSET + index * ROW_GAP);
    });
  });

  resolveLevelCollisions(nodes, yById);

  return nodes.map((node) => {
    return {
      ...node,
      x: levelX.get(node.level) ?? LEFT_OFFSET + node.level * BASE_LEVEL_GAP,
      y: Math.round(yById.get(node.id) ?? TOP_OFFSET),
      width: NODE_WIDTH,
      height: NODE_HEIGHT
    };
  });
});

const nodeMap = computed(() => new Map(layoutNodes.value.map((node) => [node.id, node])));

const canvasSize = computed(() => {
  const maxX = Math.max(...layoutNodes.value.map((node) => node.x + NODE_WIDTH), 900);
  const maxY = Math.max(...layoutNodes.value.map((node) => node.y + NODE_HEIGHT), 520);
  return {
    width: maxX + 180,
    height: maxY + 140
  };
});

const layoutEdges = computed(() =>
  {
    const edges = lifecycle.value?.edges || [];
    const edgeById = new Map(edges.map((edge) => [edge.id, edge]));
    const segmentLanes = buildSegmentLanes(edges, nodeMap.value);
    const outgoingIndex = new Map();
    const incomingIndex = new Map();
    edges.forEach((edge) => {
      const source = nodeMap.value.get(edge.source);
      const target = nodeMap.value.get(edge.target);
      if (!source || !target) {
        return;
      }
      const sourceKey = edge.source;
      const targetKey = edge.target;
      if (!outgoingIndex.has(sourceKey)) {
        outgoingIndex.set(sourceKey, []);
      }
      if (!incomingIndex.has(targetKey)) {
        incomingIndex.set(targetKey, []);
      }
      outgoingIndex.get(sourceKey).push(edge.id);
      incomingIndex.get(targetKey).push(edge.id);
    });
    outgoingIndex.forEach((ids) => {
      ids.sort((leftId, rightId) => {
        const leftEdge = edgeById.get(leftId);
        const rightEdge = edgeById.get(rightId);
        const leftTarget = leftEdge ? nodeMap.value.get(leftEdge.target) : null;
        const rightTarget = rightEdge ? nodeMap.value.get(rightEdge.target) : null;
        return (leftTarget?.y || 0) - (rightTarget?.y || 0);
      });
    });
    incomingIndex.forEach((ids) => {
      ids.sort((leftId, rightId) => {
        const leftEdge = edgeById.get(leftId);
        const rightEdge = edgeById.get(rightId);
        const leftSource = leftEdge ? nodeMap.value.get(leftEdge.source) : null;
        const rightSource = rightEdge ? nodeMap.value.get(rightEdge.source) : null;
        return (leftSource?.y || 0) - (rightSource?.y || 0);
      });
    });

    return edges.map((edge) => {
      const source = nodeMap.value.get(edge.source);
      const target = nodeMap.value.get(edge.target);
      if (!source || !target) {
        return null;
      }
      const sourceIds = outgoingIndex.get(edge.source) || [edge.id];
      const targetIds = incomingIndex.get(edge.target) || [edge.id];
      const sourcePort = portOffset(sourceIds.indexOf(edge.id), sourceIds.length);
      const targetPort = portOffset(targetIds.indexOf(edge.id), targetIds.length);
      const startX = source.x + NODE_WIDTH;
      const startY = source.y + NODE_HEIGHT / 2 + sourcePort;
      const endX = target.x;
      const endY = target.y + NODE_HEIGHT / 2 + targetPort;
      const sourceLane = Math.max(0, sourceIds.indexOf(edge.id));
      const targetLane = Math.max(0, targetIds.indexOf(edge.id));
      const forwardGap = Math.max(90, endX - startX);
      const segmentLane = segmentLanes.get(edge.id) || { index: 0, count: Math.max(sourceIds.length, targetIds.length, 1) };
      const laneStart = startX + Math.min(82, Math.max(42, forwardGap * 0.18));
      const laneEnd = endX - Math.min(78, Math.max(42, forwardGap * 0.18));
      const laneWidth = Math.max(1, laneEnd - laneStart);
      const laneRatio = segmentLane.count <= 1 ? 0.5 : (segmentLane.index + 0.5) / segmentLane.count;
      const laneX = laneStart + laneWidth * laneRatio;
      const exitX = Math.min(laneX - 8, startX + 26 + sourceLane * 4);
      const entryX = Math.max(laneX + 8, endX - 28 - targetLane * 4);
      const tone = target.tone === "danger" || source.tone === "danger"
        ? "danger"
        : target.tone === "warning" || source.tone === "warning"
          ? "warning"
          : "normal";
      const path = [
        `M ${startX} ${startY}`,
        `L ${exitX} ${startY}`,
        `L ${laneX} ${startY}`,
        `L ${laneX} ${endY}`,
        `L ${entryX} ${endY}`,
        `L ${endX} ${endY}`
      ].join(" ");
      return {
        ...edge,
        tone,
        startX,
        startY,
        endX,
        endY,
        laneX,
        path,
        labelX: Math.min(laneX + 8, endX - 78),
        labelY: startY + (endY >= startY ? 16 : -10)
      };
    })
      .filter(Boolean);
  }
);

const layoutEdgesWithLabel = computed(() => layoutEdges.value.filter((edge) => edge.label));

const stageBands = computed(() => {
  const stages = new Map();
  layoutNodes.value.forEach((node) => {
    if (!stages.has(node.level)) {
      stages.set(node.level, {
        level: node.level,
        label: node.stage,
        left: Math.max(0, node.x - 18),
        width: NODE_WIDTH + 36
      });
    }
  });
  return Array.from(stages.values()).sort((left, right) => left.level - right.level);
});

const canvasTransformStyle = computed(() => ({
  transform: `translate(${pan.value.x}px, ${pan.value.y}px) scale(${scale.value})`
}));

useBodyScrollLock(computed(() => lifecycleOpen.value));

function isEdgeConnectedToNode(edge, nodeId) {
  return Boolean(edge && nodeId && (edge.source === nodeId || edge.target === nodeId));
}

function isEdgeControlledByNodes(edge, nodeIds) {
  if (!edge || !nodeIds?.size) {
    return false;
  }
  return Array.from(nodeIds).some((nodeId) => isEdgeConnectedToNode(edge, nodeId));
}

function isEdgeHighlightedByPersistentNode(edge) {
  return isEdgeControlledByNodes(edge, persistentDagNodeIds.value);
}

function isEdgeHighlightedByHoveredNode(edge) {
  return isEdgeConnectedToNode(edge, hoveredDagNodeId.value);
}

function isEdgeMutedByNodeLock(edge) {
  return Boolean(edge && mutedPersistentDagEdgeIds.value.has(edge.id) && isEdgeHighlightedByPersistentNode(edge));
}

function isEdgeHighlighted(edge) {
  if (!edge) {
    return false;
  }
  if (isEdgeMutedByNodeLock(edge)) {
    return false;
  }
  if (persistentDagEdgeIds.value.has(edge.id) || hoveredDagEdgeId.value === edge.id) {
    return true;
  }
  return isEdgeHighlightedByPersistentNode(edge) || isEdgeHighlightedByHoveredNode(edge);
}

function isEdgeDimmed(edge) {
  if (!hasActiveDagHighlight()) {
    return false;
  }
  return !isEdgeHighlighted(edge);
}

function hasActiveDagHighlight() {
  return Boolean(
    hoveredDagEdgeId.value
    || hoveredDagNodeId.value
    || persistentDagEdgeIds.value.size
    || persistentDagNodeIds.value.size
  );
}

function handleDagNodeClick(node) {
  selectNode(node);
  if (!clickHighlightEnabled.value) {
    return;
  }
  togglePersistentDagNode(node.id);
}

function handleDagEdgeClick(edge) {
  if (!clickHighlightEnabled.value) {
    return;
  }
  const highlightedByNodeLock = isEdgeHighlightedByPersistentNode(edge);
  if (persistentDagEdgeIds.value.has(edge.id)) {
    removePersistentDagEdge(edge.id);
    if (highlightedByNodeLock) {
      addMutedPersistentDagEdge(edge.id);
    }
    return;
  }
  if (highlightedByNodeLock) {
    toggleMutedPersistentDagEdge(edge.id);
    return;
  }
  removeMutedPersistentDagEdge(edge.id);
  addPersistentDagEdge(edge.id);
}

function togglePersistentDagNode(nodeId) {
  const nextNodeIds = new Set(persistentDagNodeIds.value);
  const isLocking = !nextNodeIds.has(nodeId);
  if (nextNodeIds.has(nodeId)) {
    nextNodeIds.delete(nodeId);
  } else {
    nextNodeIds.add(nodeId);
  }
  persistentDagNodeIds.value = nextNodeIds;
  if (isLocking) {
    removeMutedEdgesConnectedToNode(nodeId);
  } else {
    pruneMutedPersistentEdges(nextNodeIds);
  }
}

function addPersistentDagEdge(edgeId) {
  const nextEdgeIds = new Set(persistentDagEdgeIds.value);
  nextEdgeIds.add(edgeId);
  persistentDagEdgeIds.value = nextEdgeIds;
}

function removePersistentDagEdge(edgeId) {
  const nextEdgeIds = new Set(persistentDagEdgeIds.value);
  nextEdgeIds.delete(edgeId);
  persistentDagEdgeIds.value = nextEdgeIds;
}

function addMutedPersistentDagEdge(edgeId) {
  const nextMutedEdgeIds = new Set(mutedPersistentDagEdgeIds.value);
  nextMutedEdgeIds.add(edgeId);
  mutedPersistentDagEdgeIds.value = nextMutedEdgeIds;
}

function removeMutedPersistentDagEdge(edgeId) {
  const nextMutedEdgeIds = new Set(mutedPersistentDagEdgeIds.value);
  nextMutedEdgeIds.delete(edgeId);
  mutedPersistentDagEdgeIds.value = nextMutedEdgeIds;
}

function toggleMutedPersistentDagEdge(edgeId) {
  const nextMutedEdgeIds = new Set(mutedPersistentDagEdgeIds.value);
  if (nextMutedEdgeIds.has(edgeId)) {
    nextMutedEdgeIds.delete(edgeId);
  } else {
    nextMutedEdgeIds.add(edgeId);
  }
  mutedPersistentDagEdgeIds.value = nextMutedEdgeIds;
}

function removeMutedEdgesConnectedToNode(nodeId) {
  const nextMutedEdgeIds = new Set(mutedPersistentDagEdgeIds.value);
  layoutEdges.value.forEach((edge) => {
    if (isEdgeConnectedToNode(edge, nodeId)) {
      nextMutedEdgeIds.delete(edge.id);
    }
  });
  mutedPersistentDagEdgeIds.value = nextMutedEdgeIds;
}

function pruneMutedPersistentEdges(nodeIds = persistentDagNodeIds.value) {
  const nextMutedEdgeIds = new Set();
  layoutEdges.value.forEach((edge) => {
    if (mutedPersistentDagEdgeIds.value.has(edge.id) && isEdgeControlledByNodes(edge, nodeIds)) {
      nextMutedEdgeIds.add(edge.id);
    }
  });
  mutedPersistentDagEdgeIds.value = nextMutedEdgeIds;
}

function handleClickHighlightModeChange() {
  if (!clickHighlightEnabled.value) {
    clearPersistentDagHighlights();
  }
}

function clearPersistentDagHighlights() {
  persistentDagNodeIds.value = new Set();
  persistentDagEdgeIds.value = new Set();
  mutedPersistentDagEdgeIds.value = new Set();
}

function buildAdaptiveLevelX(nodes, edges) {
  const levels = Array.from(new Set(nodes.map((node) => Number(node.level ?? 0))))
    .filter(Number.isFinite)
    .sort((left, right) => left - right);
  if (!levels.length) {
    return new Map();
  }

  const nodeById = new Map(nodes.map((node) => [node.id, node]));
  const pressureBySegment = new Map();
  levels.slice(0, -1).forEach((level, index) => {
    pressureBySegment.set(segmentKey(level, levels[index + 1]), 0);
  });

  edges.forEach((edge) => {
    const source = nodeById.get(edge.source);
    const target = nodeById.get(edge.target);
    if (!source || !target) {
      return;
    }
    const sourceLevel = Number(source.level ?? 0);
    const targetLevel = Number(target.level ?? 0);
    if (!Number.isFinite(sourceLevel) || !Number.isFinite(targetLevel) || sourceLevel === targetLevel) {
      return;
    }

    const leftLevel = Math.min(sourceLevel, targetLevel);
    const rightLevel = Math.max(sourceLevel, targetLevel);
    levels.slice(0, -1).forEach((level, index) => {
      const nextLevel = levels[index + 1];
      if (level >= leftLevel && nextLevel <= rightLevel) {
        const key = segmentKey(level, nextLevel);
        pressureBySegment.set(key, (pressureBySegment.get(key) || 0) + 1);
      }
    });
  });

  const xByLevel = new Map();
  let cursorX = LEFT_OFFSET;
  levels.forEach((level, index) => {
    xByLevel.set(level, cursorX);
    if (index >= levels.length - 1) {
      return;
    }
    const pressure = pressureBySegment.get(segmentKey(level, levels[index + 1])) || 0;
    const adaptiveGap = Math.min(MAX_LEVEL_GAP, BASE_LEVEL_GAP + Math.max(0, pressure - 1) * EDGE_GAP_PER_LINE);
    cursorX += adaptiveGap;
  });

  return xByLevel;
}

function buildSegmentLanes(edges, positionedNodeMap) {
  const lanesBySegment = new Map();
  edges.forEach((edge) => {
    const source = positionedNodeMap.get(edge.source);
    const target = positionedNodeMap.get(edge.target);
    if (!source || !target) {
      return;
    }
    const sourceLevel = Number(source.level ?? 0);
    const targetLevel = Number(target.level ?? 0);
    const key = segmentKey(Math.min(sourceLevel, targetLevel), Math.max(sourceLevel, targetLevel));
    if (!lanesBySegment.has(key)) {
      lanesBySegment.set(key, []);
    }
    lanesBySegment.get(key).push({ edge, source, target });
  });

  const laneByEdge = new Map();
  lanesBySegment.forEach((items) => {
    const sortedItems = items.slice().sort((left, right) => {
      const sourceDelta = (left.source.y || 0) - (right.source.y || 0);
      if (sourceDelta) {
        return sourceDelta;
      }
      const targetDelta = (left.target.y || 0) - (right.target.y || 0);
      if (targetDelta) {
        return targetDelta;
      }
      return String(left.edge.id).localeCompare(String(right.edge.id), "zh-Hans-CN");
    });
    sortedItems.forEach((item, index) => {
      laneByEdge.set(item.edge.id, {
        index,
        count: sortedItems.length
      });
    });
  });

  return laneByEdge;
}

function segmentKey(leftLevel, rightLevel) {
  return `${leftLevel}->${rightLevel}`;
}

function compareDagNode(left, right) {
  if (left.level !== right.level) {
    return left.level - right.level;
  }
  const leftSeq = Number(String(left.title || "").match(/OP(\d+)/i)?.[1] || 0);
  const rightSeq = Number(String(right.title || "").match(/OP(\d+)/i)?.[1] || 0);
  if (leftSeq || rightSeq) {
    return leftSeq - rightSeq;
  }
  return String(left.title || left.id).localeCompare(String(right.title || right.id), "zh-Hans-CN");
}

function distributeLevelRows(rows, branchHeight) {
  if (rows.length <= 1) {
    return rows.map((node) => ({
      node,
      y: TOP_OFFSET + Math.max(0, (branchHeight - NODE_HEIGHT) / 2)
    }));
  }
  const usableHeight = Math.max(branchHeight - NODE_HEIGHT, (rows.length - 1) * ROW_GAP);
  const gap = usableHeight / Math.max(1, rows.length - 1);
  return rows.map((node, index) => ({
    node,
    y: TOP_OFFSET + index * gap
  }));
}

function resolveLevelCollisions(nodes, yById) {
  const levels = new Map();
  nodes.forEach((node) => {
    if (!levels.has(node.level)) {
      levels.set(node.level, []);
    }
    levels.get(node.level).push(node);
  });

  levels.forEach((rows) => {
    rows
      .slice()
      .sort((left, right) => (yById.get(left.id) || 0) - (yById.get(right.id) || 0))
      .reduce((previousY, node) => {
        const currentY = Number(yById.get(node.id) || TOP_OFFSET);
        const nextY = Math.max(currentY, previousY + ROW_GAP);
        yById.set(node.id, nextY);
        return nextY;
      }, TOP_OFFSET - ROW_GAP);
  });
}

function portOffset(index, total) {
  if (total <= 1 || index < 0) {
    return 0;
  }
  const safeTotal = Math.min(total, 6);
  const step = Math.min(20, NODE_HEIGHT / (safeTotal + 1));
  const centeredIndex = index - (total - 1) / 2;
  return Math.max(-42, Math.min(42, centeredIndex * step));
}

function statusToneClass(status) {
  const value = String(status || "").toUpperCase();
  if (["CLOSED", "COMPLETED", "DONE", "POSTED", "RECEIVED"].includes(value)) {
    return "optional-chip";
  }
  if (["REJECTED", "SCRAP", "SCRAPPED"].includes(value)) {
    return "required-chip";
  }
  return "table-chip";
}

function displayStatus(status) {
  if (status === "ABNORMAL") {
    return "异常";
  }
  if (status === "NORMAL") {
    return "正常";
  }
  return formatStatusLabel(status);
}

function selectNode(node) {
  selectedNode.value = node;
}

function resetCanvas() {
  pan.value = { x: 24, y: 38 };
  scale.value = 0.82;
}

function startPan(event) {
  dragState.value = {
    active: true,
    startX: event.clientX,
    startY: event.clientY,
    originX: pan.value.x,
    originY: pan.value.y
  };
}

function movePan(event) {
  if (!dragState.value.active) {
    return;
  }
  pan.value = {
    x: dragState.value.originX + event.clientX - dragState.value.startX,
    y: dragState.value.originY + event.clientY - dragState.value.startY
  };
}

function stopPan() {
  dragState.value.active = false;
}

function clearDagHover() {
  hoveredDagNodeId.value = null;
  hoveredDagEdgeId.value = null;
}

function handleDagViewportMouseLeave() {
  stopPan();
  clearDagHover();
}

function zoomCanvasBy(delta) {
  const nextScale = Math.min(1.55, Math.max(0.45, scale.value + delta));
  scale.value = Number(nextScale.toFixed(2));
}

async function loadMaterialLots() {
  loadingMaterialLots.value = true;
  try {
    materialLots.value = await requestJson("/dashboard/material-lots?limit=300");
  } catch (error) {
    notifyError(error.message || "库存批次号列表加载失败");
  } finally {
    loadingMaterialLots.value = false;
  }
}

async function openLifecycle(lot) {
  activeMaterialLot.value = lot;
  lifecycleOpen.value = true;
  loadingLifecycle.value = true;
  lifecycle.value = null;
  selectedNode.value = null;
  clearDagHover();
  clearPersistentDagHighlights();
  resetCanvas();
  try {
    lifecycle.value = await requestJson(`/dashboard/material-lots/${lot.lot_id}/lifecycle`);
    selectedNode.value = lifecycle.value.nodes?.[0] || null;
  } catch (error) {
    notifyError(error.message || "库存批次号生命周期加载失败");
  } finally {
    loadingLifecycle.value = false;
  }
}

function closeLifecycle() {
  lifecycleOpen.value = false;
  activeMaterialLot.value = null;
  lifecycle.value = null;
  selectedNode.value = null;
  clearDagHover();
  clearPersistentDagHighlights();
}

onMounted(loadMaterialLots);
</script>
