import dayjs from "dayjs";
import { ItemLotTraceJoContext, ItemLotTraceOrigin, ItemLotTraceQcResult, ItemLotTraceResponse, ItemLotTraceStockTakeRecordDetail } from "@/app/api/itemTracing";
import { TraceLabelTranslator, groupQcResultsBySession, resolveDoOutboundChipFlags } from "./traceLabelUtils";
import { formatQty, formatSignedQty } from "./traceQtyUtils";
import { docLinkFromRefType, detailsOf, field, fieldIf, parseExpirySortKey, parseSortKey, pickStatusDetailFields, pickStatusNodeLabels } from "./traceNodeFactory";
import {
  isPendingPutawayOriginStatus,
  isTransferInboundPutaway,
  matchTransferForInboundPutaway,
  normRefType,
  resolvePutawayPresentation,
  shouldEmitDepletedForScope,
  shouldEmitTransferForScope,
} from "./tracePutawayUtils";
import { resolveDoOutboundDocLink, resolveJoPickDocLink, buildPickOrderTargetDateMap, resolvePickOrderTargetDate } from "./traceDocLinkUtils";
import {
  formatStockTakeRoundLabel,
  resolveStockTakeAcceptedQty,
  resolveStockTakeBookQty,
  stockTakeEventSubtitle,
} from "./traceStockTakeUtils";

export type TraceGraphNodeKind =
  | "MATERIAL_IN"
  | "JO_CREATED"
  | "MATERIAL_QC"
  | "MATERIAL_PICK"
  | "PRODUCTION_STEP"
  | "BYPRODUCT"
  | "SCRAP"
  | "DEFECT"
  | "OPEN"
  | "FAIL"
  | "DO_OUT"
  | "REPLENISHMENT_CREATED"
  | "JO_OUT"
  | "PO_OUT"
  | "DO_GROUP"
  | "PICK_GROUP"
  | "RETURN"
  | "REPACK"
  | "PURCHASE"
  | "RECEIPT"
  | "PUTAWAY"
  | "IN"
  | "OUT"
  | "QC"
  | "STOCK_TAKE"
  | "ADJUSTMENT"
  | "TRANSFER"
  | "EXPIRED"
  | "DEPLETED";

export type TraceGraphDocLinkKind = "jo" | "po" | "pick" | "workbench" | "jodetail";

export interface TraceGraphDetailField {
  label: string;
  value: string;
  linkKind?: TraceGraphDocLinkKind;
  linkCode?: string;
  linkId?: number | null;
  consoCode?: string;
  linkTicketNo?: string;
  linkTargetDate?: string;
  variant?: "default" | "qcCriteriaList";
  qcCriteriaItems?: QcCriteriaItem[];
  /** MUI palette key for status value text (success / error / warning / info). */
  valueColor?: "success" | "error" | "warning" | "info" | "default";
}

export interface QcCriteriaItem {
  name: string;
  description?: string;
  passed: boolean;
  failQty?: number;
}

export interface TraceGraphNodeLabels {
  nodeReceipt: string;
  nodePurchase: string;
  nodePutaway: string;
  nodePutawayTransfer: string;
  putawayTransferDetail: string;
  nodeJoCreated: string;
  nodeQcPass: string;
  nodeQcFail: string;
  nodeStockTake: string;
  nodeAdjustment: string;
  nodeTransfer: string;
  nodeDoOut: string;
  nodeJoOut: string;
  nodePoOut: string;
  directionIn: string;
  directionOut: string;
  formatQcSubtitle: (failQty: number, acceptedQty: number) => string;
}

export interface TraceGraphNode {
  id: string;
  kind: TraceGraphNodeKind;
  timestamp: string | null;
  sortKey: number;
  title: string;
  subtitle: string;
  qty?: number;
  uom?: string;
  meta?: string;
  refType?: string;
  refCode?: string;
  refId?: number | null;
  docLinkKind?: TraceGraphDocLinkKind;
  /** Workbench deep-link ticket (TI-*). */
  docLinkTicketNo?: string;
  docLinkTargetDate?: string;
  traceLotNo?: string;
  traceItemCode?: string;
  consoCode?: string;
  categoryLabel?: string;
  /** Translated QC type for chip label (IQC / EPQC / …). */
  qcTypeLabel?: string;
  /** 待上架 / 已上架 for PUTAWAY nodes (chip). */
  putawayStatusLabel?: string;
  /** 加單 / 補貨 flags for DO_OUT title chips (detail uses outboundKindLabel). */
  doOutboundIsExtra?: boolean;
  doOutboundIsReplenish?: boolean;
  /** 改數 — pick line qty ≠ sum of stock-out lines for that pick line. */
  doOutboundQtyChanged?: boolean;
  outboundKindLabel?: string;
  /** Longer status explanation in detail panel (e.g. transfer inbound). */
  putawayStatusDetail?: string;
  /** Warehouse where stock was shelved (PUTAWAY) or transfer endpoints (TRANSFER). */
  warehouseCode?: string;
  transferFromWarehouse?: string;
  transferToWarehouse?: string;
  /** BOM process step this node belongs to (material pick / production step mapping). */
  bomProcessId?: number | null;
  bomProcessSeqNo?: number | null;
  assignedStepName?: string;
  /** Item codes consumed by this production step (from BOM process materials). */
  stepMaterialItemCodes?: string[];
  /** Semi-finished lot whose nested production this pick feeds (nested JO only). */
  feedsProductionScopeLotNo?: string;
  /** Job order that owns this material pick / pick group. */
  jobOrderCode?: string;
  details: TraceGraphDetailField[];
  /** When set, this node is rendered inside a DO group box. */
  doGroupId?: string;
  /** DO_GROUP container size (layout only). */
  groupBoxWidth?: number;
  groupBoxHeight?: number;
  groupMemberCount?: number;
  /** Summed outbound qty for DO_GROUP header. */
  groupTotalQty?: number;
  groupUom?: string;
  /** Links REPLENISHMENT_CREATED → matching DO_OUT (stockOutLineId). */
  replenishmentStockOutLineId?: number;
  /** IN / OUT for ADJUSTMENT nodes (signed variance display). */
  adjustmentDirection?: string;
  /** Stocktake lifecycle record detail (for expandable STOCK_TAKE nodes). */
  stockTakeRecordDetail?: ItemLotTraceStockTakeRecordDetail;
  /** Stocktake variance (accepted − book); null when round has no accepted qty yet. */
  stockTakeVarianceQty?: number | null;
  /** Owning inventory_lot for multi-location graph scopes. */
  inventoryLotId?: number;
  /** Translated pick-line statuses for JO pick cards. */
  processingStatusLabel?: string;
  matchStatusLabel?: string;
  /** Raw status codes (for coloring). */
  processingStatus?: string;
  matchStatus?: string;
}

export interface TraceGraphDetailLabels extends TraceGraphNodeLabels {
  tr: TraceLabelTranslator;
  detailQcCriteria: string;
  detailQcType: string;
  detailQcUnknownItem: string;
  nodeExpired: string;
  nodeDepleted: string;
  detailQty: string;
  detailTime: string;
  detailHandler: string;
  detailStockTaker: string;
  detailApprover: string;
  detailWarehouse: string;
  detailRemarks: string;
  detailFailCategory: string;
  detailProcessDescription: string;
  detailType: string;
  detailRef: string;
  detailStockTakeCode: string;
  detailAdjustmentRef: string;
  detailReplenishmentCode: string;
  detailReason: string;
  detailReturnRef: string;
  detailSourceDoc: string;
  detailPurchaseOrderNo: string;
  detailInboundRef: string;
  detailInboundSiNo: string;
  detailPutawayBin: string;
  detailDirection: string;
  detailStatus: string;
  detailSupplier: string;
  detailMaterial: string;
  detailItemCode: string;
  detailItemName: string;
  detailLot: string;
  detailAcceptedQty: string;
  detailFailQty: string;
  detailFrom: string;
  detailTo: string;
  detailVariance: string;
  detailBefore: string;
  detailAfter: string;
  detailStockTakeFirstCount: string;
  detailStockTakeSecondCount: string;
  detailStockTakeApproverCount: string;
  detailScrapQty: string;
  detailDefectQty: string;
  detailEquipment: string;
  detailProcessStep: string;
  detailStepMaterials: string;
  detailAssignedStep: string;
  detailPickTargetDate: string;
  detailProductLotNo: string;
  detailOrderQty: string;
  detailPutAwayQty: string;
  detailPurchaseUnit: string;
  detailSupplyTo: string;
  detailStockTakeRound: string;
  detailStockTakeSection: string;
  detailLocation: string;
  deliveryOrder: string;
  deliveryNoteCode: string;
  ticketNo: string;
  pickOrder: string;
  jobOrder: string;
  expiryDate: string;
  totalAvailable: string;
  itemCode: string;
  categoryPurchase: string;
  categoryProduction: string;
  categoryInbound: string;
  categoryReceipt: string;
  categoryPutaway: string;
  categoryTransfer: string;
  categoryStockTake: string;
  categoryOpen: string;
  categoryAdjustment: string;
  categoryPick: string;
  categoryQc: string;
  categoryOutbound: string;
  categoryTerminal: string;
  processingStatus: string;
  matchStatus: string;
}

const categoryForKind = (kind: TraceGraphNodeKind, labels: TraceGraphDetailLabels): string => {
  switch (kind) {
    case "MATERIAL_IN":
      return labels.categoryPurchase;
    case "JO_CREATED":
      return labels.categoryProduction;
    case "MATERIAL_PICK":
      return labels.categoryPick;
    case "PRODUCTION_STEP":
    case "BYPRODUCT":
    case "SCRAP":
    case "DEFECT":
      return labels.categoryProduction;
    case "OPEN":
      return labels.categoryOpen;
    case "FAIL":
      return labels.categoryQc;
    case "DO_OUT":
    case "JO_OUT":
    case "PO_OUT":
    case "RETURN":
      return labels.categoryOutbound;
    case "REPACK":
      return labels.categoryTransfer;
    case "PURCHASE":
      return labels.categoryPurchase;
    case "RECEIPT":
      return labels.categoryReceipt;
    case "PUTAWAY":
      return labels.categoryPutaway;
    case "MATERIAL_QC":
    case "QC":
      return labels.categoryQc;
    case "IN":
      return labels.categoryInbound;
    case "TRANSFER":
      return labels.categoryTransfer;
    case "STOCK_TAKE":
      return labels.categoryStockTake;
    case "ADJUSTMENT":
      return labels.categoryAdjustment;
    case "OUT":
      return labels.categoryOutbound;
    case "EXPIRED":
    case "DEPLETED":
      return labels.categoryTerminal;
    default:
      return labels.detailType;
  }
};

/** Movement refTypes covered by dedicated trace arrays (avoid duplicate graph nodes). */
const MOVEMENT_REF_TYPES_IN_DEDICATED_ARRAYS = new Set([
  "TRANSFER",
  "ADJ",
  "OPEN",
  "TKE",
]);

/** Stock-take ledger movements are represented by stockTakeEvents nodes. */
const isStockTakeLedgerMovement = (movementType: string) => {
  const mt = movementType.trim().toUpperCase();
  return mt === "TKE" || mt === "STOCKTAKE";
};

const norm = (v: string | null | undefined) => (v ?? "").trim().toUpperCase();

const doMovementDedupKey = (
  pickOrderCode: string,
  timestamp: string | null | undefined,
  qty: number,
) => `${pickOrderCode.trim()}|${timestamp ?? ""}|${qty}`;

const buildCoveredDoMovementKeys = (data: ItemLotTraceResponse): Set<string> =>
  new Set(
    (data.doDeliveries ?? []).map((d) =>
      doMovementDedupKey(d.pickOrderCode, d.timestamp, d.qty),
    ),
  );

export interface TraceGraphBuildScope {
  /** e.g. `loc-42-` for alternate inventory_lot nodes (aligned with backend traceGraph keys). */
  idPrefix?: string;
  locationBlockKeys?: boolean;
  inventoryLotId?: number;
  defaultWarehouseCode?: string;
  /** When true, TRANSFER cards are omitted; destination shows 轉倉入庫 putaway only. */
  mergedMultiLocation?: boolean;
}

export const scopePrefix = (scope?: TraceGraphBuildScope) => scope?.idPrefix ?? "";

export const withNodeScope = (node: TraceGraphNode, scope?: TraceGraphBuildScope): TraceGraphNode => {
  if (!scope?.inventoryLotId && !scope?.defaultWarehouseCode) return node;
  return {
    ...node,
    inventoryLotId: scope.inventoryLotId ?? node.inventoryLotId,
    warehouseCode: node.warehouseCode?.trim() || scope.defaultWarehouseCode || node.warehouseCode,
  };
};

const buildQcNodes = (
  qcResults: ItemLotTraceQcResult[],
  labels: TraceGraphDetailLabels,
  stockUom: string,
  seqStart: number,
  scope?: TraceGraphBuildScope,
): { nodes: TraceGraphNode[]; nextSeq: number } => {
  const idPfx = scopePrefix(scope);
  const lb = scope?.locationBlockKeys ?? false;
  const nodes: TraceGraphNode[] = [];
  let seq = seqStart;
  groupQcResultsBySession(qcResults).forEach((group, i) => {
    const first = group[0];
    if (!first) return;
    const allPassed = group.every((q) => q.qcPassed);
    const totalFail = group.reduce((s, q) => s + (q.failQty ?? 0), 0);
    const metaParts = [
      first.handledBy,
      ...group.map((q) => q.remarks).filter(Boolean),
    ].filter(Boolean);
    const qcTypeRaw = group.map((q) => q.qcType).find((t) => t?.trim()) ?? "";
    nodes.push({
      id: lb
        ? `${idPfx}qc-${i}-${first.stockInLineId}`
        : `${idPfx}qc-${i}-${first.stockInLineId}-${first.created}`,
      kind: "QC",
      timestamp: first.created,
      sortKey: parseSortKey(first.created, seq++),
      title: allPassed ? labels.nodeQcPass : labels.nodeQcFail,
      subtitle: "",
      qty: totalFail,
      uom: stockUom,
      meta: metaParts.length ? Array.from(new Set(metaParts)).join(" · ") : undefined,
      categoryLabel: labels.categoryQc,
      qcTypeLabel: labels.tr.qcType(qcTypeRaw),
      details: detailsOf(
        field(labels.detailStatus, labels.tr.qcPassed(allPassed)),
        field(labels.detailQcType, labels.tr.qcType(qcTypeRaw)),
        field(labels.detailQcCriteria, "", {
          variant: "qcCriteriaList",
          qcCriteriaItems: group.map((q) => ({
            name: q.qcItemName?.trim() || q.qcItemCode?.trim() || labels.detailQcUnknownItem,
            description: q.qcItemDescription?.trim() || undefined,
            passed: q.qcPassed,
            failQty: q.failQty > 0 ? q.failQty : undefined,
          })),
        }),
        field(labels.detailAcceptedQty, formatQty(first.acceptedQty, stockUom)),
        field(labels.detailFailQty, formatQty(totalFail, stockUom)),
        field(labels.detailHandler, first.handledBy),
        field(labels.detailTime, first.created),
        fieldIf(labels.detailRemarks, group.map((q) => q.remarks).filter(Boolean).join("；")),
      ),
    });
  });
  return { nodes, nextSeq: seq };
};

const resolveJoCreatedTimestamp = (
  data: ItemLotTraceResponse,
  refCode: string | null | undefined,
  fallback: string | null | undefined,
): string | null => {
  const code = refCode?.trim();
  if (!code) return fallback ?? null;

  const candidates: Array<ItemLotTraceJoContext | undefined> = [
    data.joPrelude?.jobOrder,
    ...(data.joPrelude?.materialInputs ?? []).map((m) => m.nestedJoPrelude?.jobOrder),
  ];

  for (const jo of candidates) {
    if (jo?.jobOrderCode?.trim() === code && jo.createdAt?.trim()) {
      return jo.createdAt;
    }
  }
  return fallback ?? null;
};

const poInboundKey = (
  refCode: string | null | undefined,
  refId: number | null | undefined,
  qty: number,
): string => `${(refCode ?? "").trim()}|${refId ?? 0}|${qty}`;

const buildInboundOriginNodes = (
  data: ItemLotTraceResponse,
  labels: TraceGraphDetailLabels,
  stockUom: string,
  seqStart: number,
  scope: TraceGraphBuildScope | undefined,
  existingPurchaseCodes: Set<string>,
  coveredPoInbound: Set<string>,
  coveredJoInbound: Set<string>,
): { nodes: TraceGraphNode[]; nextSeq: number } => {
  const idPfx = scopePrefix(scope);
  const lb = scope?.locationBlockKeys ?? false;
  const defaultWh = scope?.defaultWarehouseCode;
  const nodes: TraceGraphNode[] = [];
  let seq = seqStart;

  (data.origins ?? []).forEach((origin, i) => {
    const originType = norm(origin.type);
    const linkKind = docLinkFromRefType(origin.type);
    const supplierLabel = [origin.supplierCode, origin.supplierName].filter(Boolean).join(" ");

    if (originType === "PO") {
      const refCode = origin.refCode?.trim();
      if (!refCode) return;
      coveredPoInbound.add(poInboundKey(origin.refCode, origin.refId, origin.acceptedQty));

      if (!existingPurchaseCodes.has(refCode)) {
        existingPurchaseCodes.add(refCode);
        nodes.push({
          id: lb
            ? `${idPfx}purchase-origin-${origin.refId ?? i}-${origin.stockInLineId}`
            : `${idPfx}purchase-origin-${origin.refId ?? i}-${origin.stockInLineId}`,
          kind: "PURCHASE",
          timestamp: origin.receiptDate,
          sortKey: parseSortKey(origin.receiptDate, seq++),
          title: labels.nodePurchase,
          subtitle: [supplierLabel, data.lot.itemCode].filter(Boolean).join(" · ") || "—",
          qty: origin.acceptedQty,
          uom: stockUom,
          refType: "PO",
          refCode,
          refId: origin.refId,
          docLinkKind: "po",
          categoryLabel: labels.categoryPurchase,
          details: [
            field(labels.detailSupplier, supplierLabel || "—"),
            field(labels.detailOrderQty, formatQty(origin.acceptedQty, stockUom)),
            field(labels.detailPurchaseOrderNo, refCode, {
              linkKind: "po",
              linkCode: refCode,
              linkId: origin.refId,
            }),
            field(labels.detailTime, origin.receiptDate),
          ],
        });
      }

      nodes.push({
        id: `${idPfx}origin-${i}-${origin.stockInLineId}`,
        kind: "RECEIPT",
        timestamp: origin.receiptDate,
        sortKey: parseSortKey(origin.receiptDate, seq++),
        title: labels.nodeReceipt,
        subtitle: [refCode, supplierLabel].filter(Boolean).join(" · ") || "—",
        qty: origin.acceptedQty,
        uom: stockUom,
        refType: origin.type,
        refCode,
        refId: origin.refId,
        docLinkKind: linkKind,
        warehouseCode: defaultWh,
        categoryLabel: categoryForKind("RECEIPT", labels),
        details: detailsOf(
          field(labels.detailPurchaseOrderNo, refCode, {
            linkKind,
            linkCode: refCode,
            linkId: origin.refId,
          }),
          field(labels.detailQty, formatQty(origin.acceptedQty, stockUom)),
          field(labels.detailTime, origin.receiptDate),
        ),
      });
      return;
    }

    if (originType === "JO") {
      const refCode = origin.refCode?.trim();
      if (!refCode) return;
      const joKey = `${refCode}|${origin.refId ?? 0}`;
      coveredJoInbound.add(joKey);
      const eventTimestamp = resolveJoCreatedTimestamp(data, refCode, origin.receiptDate);
      nodes.push({
        id: `${idPfx}origin-${i}-${origin.stockInLineId}`,
        kind: "JO_CREATED",
        timestamp: eventTimestamp,
        sortKey: parseSortKey(eventTimestamp, seq++),
        title: labels.nodeJoCreated,
        subtitle: refCode,
        qty: origin.acceptedQty,
        uom: stockUom,
        refType: origin.type,
        refCode,
        refId: origin.refId,
        docLinkKind: linkKind,
        warehouseCode: defaultWh,
        categoryLabel: categoryForKind("JO_CREATED", labels),
        details: detailsOf(
          field(labels.detailType, labels.nodeJoCreated),
          field(labels.jobOrder, refCode, {
            linkKind,
            linkCode: refCode,
            linkId: origin.refId,
          }),
          field(labels.detailQty, formatQty(origin.acceptedQty, stockUom)),
          field(labels.detailTime, eventTimestamp),
        ),
      });
    }
  });

  return { nodes, nextSeq: seq };
};

/** FP-MTMS Version Checklist | Functions Ref. No. 23 | v1.0.0 | 2026-07-20 */
export const buildTraceGraphNodes = (
  data: ItemLotTraceResponse,
  labels: TraceGraphDetailLabels,
  scope?: TraceGraphBuildScope,
): TraceGraphNode[] => {
  const nodes: TraceGraphNode[] = [];
  let seq = 0;
  const stockUom = data.lot.uom;
  const idPfx = scopePrefix(scope);
  const lb = scope?.locationBlockKeys ?? false;
  const primaryWh =
    data.warehouseLines.map((w) => w.warehouseCode).filter(Boolean).join(" / ") || undefined;
  const defaultScope: TraceGraphBuildScope | undefined = scope
    ? scope
    : primaryWh
      ? { defaultWarehouseCode: primaryWh, inventoryLotId: data.lot.inventoryLotId }
      : { inventoryLotId: data.lot.inventoryLotId };
  const mergedMultiLocation = defaultScope?.mergedMultiLocation ?? false;

  const existingPurchaseCodes = new Set<string>();
  const coveredPoInbound = new Set<string>();
  const coveredJoInbound = new Set<string>();

  (data.purchaseEvents ?? []).forEach((purchase, i) => {
    const purchaseUnit = purchase.purchaseUnit;
    const supplierLabel = [purchase.supplierCode, purchase.supplierName].filter(Boolean).join(" ");
    const poCode = purchase.purchaseOrderCode?.trim();
    if (poCode) existingPurchaseCodes.add(poCode);
    const subtitle =
      [supplierLabel, purchase.itemCode, purchase.itemName].filter(Boolean).join(" · ") || "—";
    const meta = [supplierLabel].filter(Boolean).join(" · ");
    nodes.push({
      id: `${idPfx}purchase-${i}-${purchase.purchaseOrderLineId}`,
      kind: "PURCHASE",
      timestamp: purchase.orderDate,
      sortKey: parseSortKey(purchase.orderDate, seq++),
      title: labels.nodePurchase,
      subtitle,
      qty: purchase.orderQty,
      uom: purchaseUnit,
      meta: meta || undefined,
      refType: "PO",
      refCode: purchase.purchaseOrderCode,
      refId: purchase.purchaseOrderId,
      docLinkKind: "po",
      categoryLabel: labels.categoryPurchase,
      details: [
        field(labels.detailItemCode, purchase.itemCode),
        field(labels.detailItemName, purchase.itemName),
        field(labels.detailSupplier, supplierLabel),
        field(labels.detailOrderQty, formatQty(purchase.orderQty, purchaseUnit)),
        field(labels.detailPurchaseUnit, purchase.purchaseUnit),
        field(labels.detailPurchaseOrderNo, purchase.purchaseOrderCode, {
          linkKind: "po",
          linkCode: purchase.purchaseOrderCode,
          linkId: purchase.purchaseOrderId,
        }),
        field(labels.detailTime, purchase.orderDate),
      ],
    });
  });

  const originBuilt = buildInboundOriginNodes(
    data,
    labels,
    stockUom,
    seq,
    defaultScope,
    existingPurchaseCodes,
    coveredPoInbound,
    coveredJoInbound,
  );
  nodes.push(...originBuilt.nodes);
  seq = originBuilt.nextSeq;

  const coveredDoMovementKeys = buildCoveredDoMovementKeys(data);
  const pickOrderTargetDateMap = buildPickOrderTargetDateMap(data);
  const doMetaByPickCode = new Map<
    string,
    {
      isExtra: boolean;
      isReplenish: boolean;
      qtyChanged: boolean;
      consoCode?: string;
    }
  >();
  (data.doDeliveries ?? []).forEach((d) => {
    const code = d.pickOrderCode?.trim();
    if (!code) return;
    const chipFlags = resolveDoOutboundChipFlags({
      isExtra: d.isExtra,
      isReplenish: d.isReplenish,
      ticketNo: d.ticketNo,
      consoCode: d.consoCode,
      releaseType: d.releaseType,
      deliveryOrderPickOrderId: d.deliveryOrderPickOrderId,
      relationshipId: d.relationshipId,
    });
    doMetaByPickCode.set(code, {
      isExtra: chipFlags.isExtra,
      isReplenish: chipFlags.isReplenish,
      qtyChanged: d.qtyChanged === true,
      consoCode: d.consoCode?.trim() || code,
    });
  });

  data.movements.forEach((m, i) => {
    const refType = norm(m.refType);
    if (MOVEMENT_REF_TYPES_IN_DEDICATED_ARRAYS.has(refType)) return;
    if (isStockTakeLedgerMovement(m.movementType)) return;
    if (
      refType === "DO" &&
      coveredDoMovementKeys.has(doMovementDedupKey(m.refCode, m.timestamp, m.qty))
    ) {
      return;
    }
    if (refType === "RETURN" && (data.returnEvents?.length ?? 0) > 0) return;

    if (m.direction === "IN" && refType === "PO") {
      if (coveredPoInbound.has(poInboundKey(m.refCode, m.refId, m.qty))) return;
      coveredPoInbound.add(poInboundKey(m.refCode, m.refId, m.qty));
    }
    if (m.direction === "IN" && refType === "JO") {
      const joKey = `${(m.refCode ?? "").trim()}|${m.refId ?? 0}`;
      if (coveredJoInbound.has(joKey)) return;
      coveredJoInbound.add(joKey);
    }

    const isDoOut = refType === "DO" && m.direction !== "IN";
    const isJoOut = refType === "JO_PICK" && m.direction !== "IN";
    const isPoOut = refType === "PO_PICK" && m.direction !== "IN";
    const kind: TraceGraphNodeKind =
      m.direction === "IN"
        ? refType === "PO"
          ? "RECEIPT"
          : refType === "JO"
            ? "JO_CREATED"
            : "IN"
        : isDoOut
          ? "DO_OUT"
          : isJoOut
            ? "JO_OUT"
            : isPoOut
              ? "PO_OUT"
            : "OUT";
    const eventTimestamp =
      kind === "JO_CREATED"
        ? resolveJoCreatedTimestamp(data, m.refCode, m.timestamp)
        : m.timestamp;
    const title =
      kind === "RECEIPT"
        ? labels.nodeReceipt
        : kind === "JO_CREATED"
          ? labels.nodeJoCreated
          : kind === "DO_OUT"
            ? `${labels.nodeDoOut} · ${m.refCode || "—"}`
            : kind === "JO_OUT"
              ? `${labels.nodeJoOut} · ${m.refCode || "—"}`
              : kind === "PO_OUT"
                ? `${labels.nodePoOut} · ${m.refCode || "—"}`
              : `${labels.tr.refType(m.refType)} · ${labels.tr.movementType(m.movementType)}`;
    const subtitle =
      kind === "DO_OUT" || kind === "JO_OUT" || kind === "PO_OUT"
        ? ""
        : [m.refCode, m.warehouseCode].filter(Boolean).join(" · ") || "—";
    const meta = [m.handledBy, m.remarks].filter(Boolean).join(" · ");
    const linkKind = isDoOut || isJoOut || isPoOut ? "pick" : docLinkFromRefType(m.refType);
    const receiptDetails =
      kind === "RECEIPT"
        ? detailsOf(
            field(labels.detailPurchaseOrderNo, m.refCode, {
              linkKind,
              linkCode: m.refCode,
              linkId: m.refId,
            }),
            field(labels.detailQty, formatQty(m.qty, stockUom)),
            field(labels.detailHandler, m.handledBy),
            field(labels.detailTime, m.timestamp),
            fieldIf(labels.detailRemarks, m.remarks),
          )
        : kind === "DO_OUT"
          ? detailsOf(
              field(labels.detailType, labels.nodeDoOut),
              field(labels.pickOrder, m.refCode, {
                linkKind: "pick",
                linkCode: m.refCode,
                linkId: m.refId,
                consoCode: m.refCode,
              }),
              field(labels.detailWarehouse, m.warehouseCode),
              field(labels.detailQty, formatQty(m.qty, stockUom)),
              field(labels.detailHandler, m.handledBy),
              field(labels.detailTime, m.timestamp),
              fieldIf(labels.detailRemarks, m.remarks),
            )
        : kind === "JO_OUT"
          ? detailsOf(
              field(labels.detailType, labels.nodeJoOut),
              field(labels.pickOrder, m.refCode, {
                linkKind: "jodetail",
                linkCode: m.refCode,
                linkId: m.refId,
                consoCode: m.refCode,
                linkTargetDate: resolvePickOrderTargetDate(
                  pickOrderTargetDateMap,
                  m.refCode,
                  m.timestamp,
                ),
              }),
              field(
                labels.detailPickTargetDate,
                resolvePickOrderTargetDate(pickOrderTargetDateMap, m.refCode, m.timestamp),
              ),
              field(labels.detailWarehouse, m.warehouseCode),
              field(labels.detailQty, formatQty(m.qty, stockUom)),
              ...pickStatusDetailFields(labels, m.processingStatus, m.matchStatus, {
                always: true,
              }),
              field(labels.detailHandler, m.handledBy),
              field(labels.detailTime, m.timestamp),
              fieldIf(labels.detailRemarks, m.remarks),
            )
        : kind === "PO_OUT"
          ? detailsOf(
              field(labels.detailType, labels.nodePoOut),
              field(labels.pickOrder, m.refCode, {
                linkKind: "pick",
                linkCode: m.refCode,
                linkId: m.refId,
                consoCode: m.refCode,
              }),
              field(labels.detailWarehouse, m.warehouseCode),
              field(labels.detailQty, formatQty(m.qty, stockUom)),
              field(labels.detailHandler, m.handledBy),
              field(labels.detailTime, m.timestamp),
              fieldIf(labels.detailRemarks, m.remarks),
            )
        : detailsOf(
            field(
              labels.detailType,
              kind === "JO_CREATED" ? labels.nodeJoCreated : labels.tr.movementType(m.movementType),
            ),
            field(labels.detailDirection, labels.tr.direction(m.direction)),
            field(labels.detailSourceDoc, m.refCode, {
              linkKind,
              linkCode: m.refCode,
              linkId: m.refId,
            }),
            field(labels.detailWarehouse, m.warehouseCode),
            field(labels.detailQty, formatQty(m.qty, stockUom)),
            field(labels.detailHandler, m.handledBy),
            field(labels.detailTime, eventTimestamp),
            fieldIf(labels.detailRemarks, m.remarks),
          );
    const pickMeta = isDoOut ? doMetaByPickCode.get(m.refCode?.trim() ?? "") : undefined;
    const delivery = isDoOut
      ? (data.doDeliveries ?? []).find((d) => d.pickOrderCode?.trim() === m.refCode?.trim())
      : undefined;
    const docLink = isDoOut
      ? resolveDoOutboundDocLink({
          pickOrderCode: delivery?.pickOrderCode || m.refCode,
          pickOrderId: delivery?.pickOrderId ?? m.refId,
          deliveryOrderCode: delivery?.deliveryOrderCode,
          ticketNo: delivery?.ticketNo,
          outboundTicketNo: m.outboundTicketNo,
          consoCode: delivery?.consoCode || m.pickConsoCode,
          timestamp: m.timestamp,
          deliveryOrderPickOrderId:
            delivery?.deliveryOrderPickOrderId ?? m.deliveryOrderPickOrderId,
        })
      : null;
    const joPickTargetDate = isJoOut
      ? resolvePickOrderTargetDate(pickOrderTargetDateMap, m.refCode, m.timestamp)
      : undefined;
    const joDocLink = isJoOut
      ? resolveJoPickDocLink({
          pickOrderCode: m.refCode,
          pickOrderId: m.refId,
          timestamp: m.timestamp,
          targetDate: joPickTargetDate,
        })
      : null;
    const movementChip = isDoOut
      ? resolveDoOutboundChipFlags({
          isExtra: pickMeta ? undefined : m.doOutboundIsExtra,
          isReplenish: pickMeta ? undefined : m.doOutboundIsReplenish,
          ticketNo: pickMeta ? undefined : m.outboundTicketNo,
          consoCode: pickMeta ? undefined : (m.pickConsoCode || m.refCode),
          deliveryOrderPickOrderId: pickMeta
            ? undefined
            : m.deliveryOrderPickOrderId,
          relationshipId: pickMeta ? undefined : m.relationshipId,
        })
      : null;
    const chipFlags = pickMeta
      ? resolveDoOutboundChipFlags({
          isExtra: pickMeta.isExtra,
          isReplenish: pickMeta.isReplenish,
          consoCode: pickMeta.consoCode,
        })
      : movementChip;
    nodes.push({
      id: lb
        ? `${idPfx}mov-${i}-${m.refCode}`
        : `${idPfx}mov-${i}-${m.refCode}-${eventTimestamp}`,
      kind,
      timestamp: eventTimestamp,
      sortKey: parseSortKey(eventTimestamp, seq++),
      title,
      subtitle,
      qty: m.qty,
      uom: stockUom,
      meta: meta || undefined,
      refType: m.refType,
      refCode: isDoOut ? (docLink?.displayCode ?? m.refCode) : m.refCode,
      refId: m.refId,
      docLinkKind: isDoOut ? docLink?.kind : isJoOut ? joDocLink?.kind : linkKind,
      docLinkTicketNo: docLink?.ticketNo,
      docLinkTargetDate: isDoOut ? docLink?.targetDate : joDocLink?.targetDate,
      consoCode: isDoOut
        ? (docLink?.consoCode ?? pickMeta?.consoCode ?? (m.pickConsoCode?.trim() || m.refCode))
        : isJoOut || isPoOut
          ? (m.pickConsoCode?.trim() || m.refCode?.trim() || undefined)
          : undefined,
      doOutboundIsExtra: chipFlags?.isExtra,
      doOutboundIsReplenish: chipFlags?.isReplenish,
      doOutboundQtyChanged: isDoOut
        ? (pickMeta?.qtyChanged ?? m.doOutboundQtyChanged === true)
        : undefined,
      warehouseCode: m.warehouseCode?.trim() || undefined,
      categoryLabel: categoryForKind(kind, labels),
      ...(isJoOut
        ? pickStatusNodeLabels(labels, m.processingStatus, m.matchStatus, { always: true })
        : {}),
      details: receiptDetails,
    });
  });

  const qcBuilt = buildQcNodes(data.qcResults, labels, stockUom, seq, scope);
  nodes.push(...qcBuilt.nodes);
  seq = qcBuilt.nextSeq;

  const coveredPutawaySilIds = new Set<number>();
  const putawayLabels = {
    nodePutaway: labels.nodePutaway,
    nodePutawayTransfer: labels.nodePutawayTransfer,
    putawayTransferDetail: labels.putawayTransferDetail,
  };
  (data.putawayEvents ?? []).forEach((putaway, i) => {
    const matchedTransfer = isTransferInboundPutaway(putaway.refType)
      ? matchTransferForInboundPutaway(putaway, data.transfers ?? [])
      : undefined;
    if (isTransferInboundPutaway(putaway.refType)) {
      // Represented by TRANSFER cards (one per TR-*); skip destination putaway cards.
      if (putaway.stockInLineId != null) {
        coveredPutawaySilIds.add(putaway.stockInLineId);
      }
      return;
    }
    const linkKind = docLinkFromRefType(putaway.refType);
    const pres = resolvePutawayPresentation(labels.tr, putawayLabels, putaway.status, putaway.refType);
    const meta = [pres.chipLabel, putaway.handledBy, putaway.warehouseCode].filter(Boolean).join(" · ");
    nodes.push({
      id: lb
        ? `${idPfx}putaway-${i}-${putaway.inventoryLotLineId}`
        : `${idPfx}putaway-${i}-${putaway.inventoryLotLineId}-${putaway.timestamp}`,
      kind: "PUTAWAY",
      timestamp: putaway.timestamp,
      sortKey: parseSortKey(putaway.timestamp, seq++),
      title: pres.title,
      subtitle: [pres.chipLabel, putaway.refCode, putaway.warehouseCode].filter(Boolean).join(" · ") || "—",
      qty: putaway.qty,
      uom: stockUom,
      meta: meta || undefined,
      refType: putaway.refType,
      refCode: putaway.refCode,
      refId: putaway.refId,
      warehouseCode: putaway.warehouseCode,
      transferFromWarehouse: matchedTransfer?.fromWarehouse ?? undefined,
      transferToWarehouse: matchedTransfer?.toWarehouse ?? undefined,
      docLinkKind: linkKind,
      putawayStatusLabel: pres.chipLabel,
      putawayStatusDetail: pres.statusDetail,
      categoryLabel: labels.categoryPutaway,
      details: [
        field(labels.detailStatus, pres.statusDetail),
        field(
          isTransferInboundPutaway(putaway.refType)
            ? labels.detailInboundSiNo
            : normRefType(putaway.refType) === "PO"
              ? labels.detailPurchaseOrderNo
              : normRefType(putaway.refType) === "JO"
                ? labels.jobOrder
                : labels.detailSourceDoc,
          putaway.refCode,
          {
          linkKind,
          linkCode: putaway.refCode,
          linkId: putaway.refId,
        }),
        ...(matchedTransfer
          ? [
              field(labels.detailFrom, matchedTransfer.fromWarehouse),
              field(labels.detailTo, matchedTransfer.toWarehouse),
            ]
          : []),
        field(labels.detailPutawayBin, putaway.warehouseCode),
        field(labels.detailQty, formatQty(putaway.qty, stockUom)),
        field(labels.detailHandler, putaway.handledBy),
        field(labels.detailTime, putaway.timestamp),
      ],
    });
    if (putaway.stockInLineId != null) {
      coveredPutawaySilIds.add(putaway.stockInLineId);
    }
  });

  (data.origins ?? []).forEach((origin, i) => {
    if (coveredPutawaySilIds.has(origin.stockInLineId)) return;
    if (!isPendingPutawayOriginStatus(origin.status)) return;
    const originType = norm(origin.type);
    if (!["PO", "JO", "TKE", "STOCK_IN"].includes(originType)) return;
    if (isTransferInboundPutaway(origin.type)) return;
    const pres = resolvePutawayPresentation(labels.tr, putawayLabels, origin.status, origin.type);
    const linkKind = docLinkFromRefType(origin.type);
    nodes.push({
      id: `putaway-pending-${origin.stockInLineId}-${i}`,
      kind: "PUTAWAY",
      timestamp: origin.receiptDate,
      sortKey: parseSortKey(origin.receiptDate, seq++),
      title: pres.title,
      subtitle: [pres.chipLabel, origin.refCode].filter(Boolean).join(" · ") || "—",
      qty: origin.acceptedQty,
      uom: stockUom,
      refType: origin.type,
      refCode: origin.refCode,
      refId: origin.refId,
      docLinkKind: linkKind,
      putawayStatusLabel: pres.chipLabel,
      putawayStatusDetail: pres.statusDetail,
      categoryLabel: labels.categoryPutaway,
      details: [
        field(labels.detailStatus, pres.statusDetail),
        field(
          originType === "PO"
            ? labels.detailPurchaseOrderNo
            : originType === "JO"
              ? labels.jobOrder
              : labels.detailSourceDoc,
          origin.refCode,
          {
          linkKind,
          linkCode: origin.refCode,
          linkId: origin.refId,
        }),
        field(labels.detailQty, formatQty(origin.acceptedQty, stockUom)),
        field(labels.detailTime, origin.receiptDate),
      ],
    });
  });

  data.stockTakeEvents.forEach((e, i) => {
    const roundLabel = formatStockTakeRoundLabel(e);
    const detail = e.recordDetail ?? undefined;
    const bookQty = resolveStockTakeBookQty(detail, e.beforeQty);
    const acceptedQty = resolveStockTakeAcceptedQty(detail, e.afterQty);
    const varianceQty =
      detail?.varianceQty != null
        ? Number(detail.varianceQty)
        : acceptedQty != null && e.varianceQty != null
          ? Number(e.varianceQty)
          : null;
    const meta = [e.approver, roundLabel, e.stockTakeSection].filter(Boolean).join(" · ");
    nodes.push({
      id: lb
        ? `${idPfx}st-${i}-${e.stockTakeCode}`
        : `${idPfx}st-${i}-${e.stockTakeCode}-${e.timestamp}`,
      kind: "STOCK_TAKE",
      timestamp: e.timestamp,
      sortKey: parseSortKey(e.timestamp, seq++),
      title: labels.nodeStockTake,
      subtitle: stockTakeEventSubtitle(e),
      qty: acceptedQty ?? undefined,
      uom: stockUom,
      meta: meta || undefined,
      refCode: e.stockTakeCode,
      warehouseCode: e.warehouseCode?.trim() || undefined,
      traceLotNo: e.lotNo?.trim() || data.lot.lotNo?.trim() || undefined,
      categoryLabel: labels.categoryStockTake,
      stockTakeRecordDetail: detail,
      stockTakeVarianceQty: varianceQty,
      details: [
        field(labels.detailStockTakeCode, e.stockTakeCode),
        fieldIf(labels.detailStockTakeRound, roundLabel || undefined),
        fieldIf(labels.detailStockTakeSection, e.stockTakeSection),
        fieldIf(labels.detailLocation, e.warehouseCode),
        fieldIf(labels.detailLot, e.lotNo || data.lot.lotNo),
        fieldIf(labels.detailItemCode, e.itemCode),
        fieldIf(labels.detailItemName, e.itemName),
        field(labels.detailBefore, formatQty(bookQty, stockUom)),
        fieldIf(
          labels.detailStockTakeFirstCount,
          detail?.pickerFirstQty != null
            ? formatQty(detail.pickerFirstQty, stockUom)
            : undefined,
        ),
        fieldIf(
          labels.detailStockTakeSecondCount,
          detail?.pickerSecondQty != null
            ? formatQty(detail.pickerSecondQty, stockUom)
            : undefined,
        ),
        fieldIf(
          labels.detailStockTakeApproverCount,
          detail?.lastSelect === 3 && detail.approverQty != null
            ? formatQty(detail.approverQty, stockUom)
            : undefined,
        ),
        field(labels.detailAfter, formatQty(acceptedQty, stockUom)),
        field(labels.detailVariance, formatQty(varianceQty, stockUom)),
        fieldIf(labels.detailStockTaker, detail?.stockTakerName),
        fieldIf(labels.detailApprover, detail?.approverName),
        // Fallback when record detail is absent: keep legacy single handler field.
        !detail?.stockTakerName && !detail?.approverName
          ? field(labels.detailHandler, e.approver)
          : null,
        field(labels.detailTime, e.timestamp),
      ].filter((row): row is TraceGraphDetailField => Boolean(row)),
    });
  });

  data.adjustments.forEach((a, i) => {
    if (norm(a.adjustmentType) === "TKE" && (data.stockTakeEvents?.length ?? 0) > 0) return;
    const meta = [a.handledBy, a.reason].filter(Boolean).join(" · ");
    const adjustmentWh =
      a.warehouseCode?.trim() || defaultScope?.defaultWarehouseCode;
    nodes.push({
      id: lb
        ? `${idPfx}adj-${i}-${a.refCode}-${a.timestamp}`
        : `${idPfx}adj-${i}-${a.refCode}-${a.timestamp}-${a.direction}`,
      kind: "ADJUSTMENT",
      timestamp: a.timestamp,
      sortKey: parseSortKey(a.timestamp, seq++),
      title: `${labels.nodeAdjustment} (${labels.tr.direction(a.direction)})`,
      subtitle: [labels.tr.adjustmentType(a.adjustmentType), a.refCode].filter(Boolean).join(" · ") || "—",
      qty: a.qty,
      uom: stockUom,
      adjustmentDirection: a.direction,
      meta: meta || undefined,
      refCode: a.refCode,
      warehouseCode: adjustmentWh,
      categoryLabel: labels.categoryAdjustment,
      details: detailsOf(
        field(labels.detailVariance, formatSignedQty(a.qty, a.direction, stockUom)),
        field(labels.detailAdjustmentRef, a.refCode),
        fieldIf(labels.detailRemarks, a.reason),
        ...(adjustmentWh ? [field(labels.detailWarehouse, adjustmentWh)] : []),
        field(labels.detailHandler, a.handledBy),
        field(labels.detailTime, a.timestamp),
      ),
    });
  });

  const normWh = (w?: string | null) => (w ?? "").trim().toUpperCase();
  data.transfers.forEach((tr, i) => {
    if (!shouldEmitTransferForScope(defaultScope?.locationBlockKeys)) {
      return;
    }
    const inboundSi = (data.putawayEvents ?? []).find(
      (p) =>
        isTransferInboundPutaway(p.refType) &&
        normWh(p.warehouseCode) === normWh(tr.toWarehouse),
    );
    nodes.push({
      id: lb
        ? `${idPfx}tr-${i}-${tr.transferCode}`
        : `${idPfx}tr-${i}-${tr.transferCode}-${tr.timestamp}`,
      kind: "TRANSFER",
      timestamp: tr.timestamp,
      sortKey: parseSortKey(tr.timestamp, seq++),
      title: labels.nodeTransfer,
      subtitle: "",
      qty: tr.qty,
      uom: stockUom,
      refCode: tr.transferCode,
      transferFromWarehouse: tr.fromWarehouse,
      transferToWarehouse: tr.toWarehouse,
      categoryLabel: labels.categoryTransfer,
      details: [
        field(labels.detailFrom, tr.fromWarehouse),
        field(labels.detailTo, tr.toWarehouse),
        field(labels.detailQty, formatQty(tr.qty, stockUom)),
        ...(inboundSi?.refCode
          ? [field(labels.detailInboundSiNo, inboundSi.refCode)]
          : []),
        field(labels.detailTime, tr.timestamp),
      ],
    });
  });

  const totalAvailable = data.warehouseLines.reduce((s, w) => s + (w.availableQty ?? 0), 0);
  const expiry = data.lot.expiryDate;
  if (expiry && dayjs(expiry).isValid() && dayjs(expiry).isBefore(dayjs(), "day")) {
    nodes.push({
      id: `${idPfx}terminal-expired-${data.lot.lotNo}`,
      kind: "EXPIRED",
      timestamp: expiry,
      sortKey: parseExpirySortKey(expiry, seq + 1_000_000),
      title: labels.nodeExpired,
      subtitle: expiry,
      categoryLabel: labels.categoryTerminal,
      details: [
        field(labels.expiryDate, expiry),
        field(labels.totalAvailable, formatQty(totalAvailable, stockUom)),
        field(labels.detailStatus, labels.nodeExpired),
      ],
    });
  }
  if (
    data.warehouseLines.length > 0 &&
    shouldEmitDepletedForScope(
      totalAvailable,
      defaultScope?.defaultWarehouseCode,
      data.transfers ?? [],
      mergedMultiLocation,
    )
  ) {
    const lastTs =
      data.movements[0]?.timestamp ??
      data.outboundUsage[0]?.timestamp ??
      data.lot.stockInDate;
    nodes.push({
      id: `${idPfx}terminal-depleted-${data.lot.lotNo}`,
      kind: "DEPLETED",
      timestamp: lastTs,
      sortKey: parseSortKey(lastTs, seq + 2_000_000),
      title: labels.nodeDepleted,
      subtitle: `${labels.totalAvailable}: ${formatQty(0, stockUom)}`,
      categoryLabel: labels.categoryTerminal,
      details: [
        field(labels.totalAvailable, formatQty(0, stockUom)),
        field(labels.detailTime, lastTs),
        field(labels.detailStatus, labels.nodeDepleted),
      ],
    });
  }

  return nodes
    .map((n) => withNodeScope(n, defaultScope))
    .sort((a, b) => {
    if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
    return a.id.localeCompare(b.id);
  });
};

export { categoryForKind };
