import dayjs from "dayjs";
import { ItemLotTraceResponse } from "@/app/api/itemTracing";
import { COLUMN_WIDTH_MIN } from "./traceFlowConstants";
import {
  buildTraceGraphNodes,
  TraceGraphDetailLabels,
  TraceGraphNode,
} from "./buildTraceGraphNodes";
import { buildJoPreludeGraphNodes, JoPreludeGraphLabels } from "./buildJoPreludeGraphNodes";
import {
  buildProductionGraphNodes,
  ProductionGraphLabels,
} from "./buildProductionGraphNodes";
import {
  buildExtendedTraceGraphNodes,
  ExtendedTraceGraphLabels,
} from "./buildExtendedTraceGraphNodes";
import { buildAllLocationBlockGraphNodes } from "./buildLocationBlockGraphNodes";
import { applyFlowNodeGrouping, isDoGroupChild, regroupLayoutCells } from "./traceDoGroupLayout";
import { cellWidthRequirement } from "./traceFlowLayout";
import {
  materialInputsHaveProduction,
  phaseFromKind,
  sortNodesInPhase,
  sortPhasesByEarliestEvent,
} from "./traceGraphSemantics";

export {
  buildTraceFlowEdgePairs,
  materialInputsHaveProduction,
  phaseFromKind,
  sortNodesInPhase,
  sortPhasesByEarliestEvent,
} from "./traceGraphSemantics";

export type PreProductionPhase = "MATERIAL_PICK";

export type ProductionPhase = "PRODUCTION";

export type FgTraceGraphPhase =
  | "PURCHASE"
  | "INBOUND"
  | "QC"
  | "PUTAWAY"
  | "WAREHOUSE"
  | "OUTBOUND"
  | "STOCK_TAKE";

/** @deprecated MATERIAL_QC merged into QC lane when prelude is shown */
export type LegacyMaterialQcPhase = "MATERIAL_QC";

export type TraceGraphPhase = PreProductionPhase | ProductionPhase | FgTraceGraphPhase;

export const PRE_PRODUCTION_PHASES: PreProductionPhase[] = ["MATERIAL_PICK"];

export const FG_PHASE_ORDER: FgTraceGraphPhase[] = [
  "PURCHASE",
  "INBOUND",
  "QC",
  "PUTAWAY",
  "WAREHOUSE",
  "STOCK_TAKE",
  "OUTBOUND",
];

/** Prelude rows use one shared QC lane (原料 + 成品品檢). */
export const PRELUDE_PHASE_ORDER = (
  hasProduction: boolean,
): TraceGraphPhase[] => {
  const phases: TraceGraphPhase[] = [
    "PURCHASE",
    "INBOUND",
    "QC",
    "PUTAWAY",
    "MATERIAL_PICK",
  ];
  if (hasProduction) phases.push("PRODUCTION");
  phases.push("WAREHOUSE", "STOCK_TAKE", "OUTBOUND");
  return phases;
};

export const getPhaseOrder = (hasPrelude: boolean, hasProduction: boolean): TraceGraphPhase[] => {
  if (hasPrelude) return PRELUDE_PHASE_ORDER(hasProduction);
  const phases: TraceGraphPhase[] = [...FG_PHASE_ORDER];
  if (hasProduction) phases.splice(1, 0, "PRODUCTION");
  return phases;
};

/** @deprecated use layout.phaseOrder */
export const PHASE_ORDER: TraceGraphPhase[] = FG_PHASE_ORDER;

/** @deprecated use layout.laneCount */
export const LANE_COUNT = FG_PHASE_ORDER.length;

export interface TraceGraphLayoutNode extends TraceGraphNode {
  phase: TraceGraphPhase;
  column: number;
  dayKey: string;
  laneIndex: number;
  sequenceIndex: number;
  branchIndex: number;
  branchSize: number;
  /** Slot index within the day column (one dynamic phase slot per phase on that day). */
  dayPhaseStaggerIndex: number;
}

export interface DayColumnLayout {
  columnStarts: number[];
  columnWidths: number[];
  /** Width of each phase slot within a calendar day (same order as phases on that day). */
  phaseSlotWidths: number[][];
  /** Start X offset of each phase slot within a calendar day. */
  phaseSlotStarts: number[][];
  timelineWidth: number;
}

export interface TraceGraphLayout {
  nodes: TraceGraphLayoutNode[];
  columnCount: number;
  columnDates: string[];
  dayColumns: DayColumnLayout;
  cellGroups: TraceGraphLayoutNode[][];
  phaseOrder: TraceGraphPhase[];
  laneCount: number;
  hasPrelude: boolean;
  hasProduction: boolean;
}

export const laneIndexFromPhase = (phase: TraceGraphPhase, phaseOrder: TraceGraphPhase[]): number =>
  phaseOrder.indexOf(phase);

const cellKey = (column: number, laneIndex: number) => `${column}:${laneIndex}`;

export const dayKeyFromTimestamp = (timestamp: string | null): string => {
  if (!timestamp?.trim()) return "—";
  const d = dayjs(timestamp);
  if (!d.isValid()) return timestamp.slice(0, 10) || "—";
  return d.format("YYYY-MM-DD");
};

export const phasesOnDay = (
  nodes: TraceGraphLayoutNode[],
  dayKey: string,
  phaseOrder: TraceGraphPhase[],
): TraceGraphPhase[] => {
  const present = new Set<TraceGraphPhase>();
  nodes.forEach((n) => {
    if (n.dayKey === dayKey) present.add(n.phase);
  });
  return sortPhasesByEarliestEvent(nodes, Array.from(present), phaseOrder, dayKey);
};

/** Widen each calendar day by chronological time slots (not phase-packed slots). */
export const computeDayColumnLayout = (
  columnDates: string[],
  nodes: TraceGraphLayoutNode[],
  _phaseOrder: TraceGraphPhase[],
): DayColumnLayout => {
  const visible = nodes.filter((n) => !isDoGroupChild(n));
  const phaseSlotWidths: number[][] = [];
  const phaseSlotStarts: number[][] = [];
  const columnWidths: number[] = [];

  columnDates.forEach((dayKey, colIndex) => {
    const dayNodes = visible.filter((n) => n.column === colIndex && n.dayKey === dayKey);
    const maxSlot = dayNodes.reduce((m, n) => Math.max(m, n.dayPhaseStaggerIndex), 0);
    const slotCount = dayNodes.length === 0 ? 1 : maxSlot + 1;

    const slotWidths: number[] = [];
    for (let slot = 0; slot < slotCount; slot++) {
      const members = dayNodes.filter((n) => n.dayPhaseStaggerIndex === slot);
      slotWidths.push(cellWidthRequirement(members));
    }

    const starts: number[] = [];
    let dayWidth = 0;
    slotWidths.forEach((w) => {
      starts.push(dayWidth);
      dayWidth += w;
    });

    phaseSlotWidths.push(slotWidths);
    phaseSlotStarts.push(starts);
    columnWidths.push(Math.max(COLUMN_WIDTH_MIN, dayWidth));
  });

  const columnStarts: number[] = [];
  let x = 0;
  for (const w of columnWidths) {
    columnStarts.push(x);
    x += w;
  }
  return { columnStarts, columnWidths, phaseSlotWidths, phaseSlotStarts, timelineWidth: x };
};

/**
 * Within each calendar day, assign left→right slots by event time (sortKey).
 * Avoids packing an entire phase to the left of another phase when later events
 * in the early phase happen after earlier events in a later phase.
 */
export const assignChronologicalDaySlots = (nodes: TraceGraphLayoutNode[]): void => {
  const byDay = new Map<string, TraceGraphLayoutNode[]>();
  nodes.forEach((n) => {
    if (isDoGroupChild(n)) return;
    const list = byDay.get(n.dayKey) ?? [];
    list.push(n);
    byDay.set(n.dayKey, list);
  });

  byDay.forEach((dayNodes) => {
    dayNodes.sort((a, b) => {
      if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
      // Same timestamp: putaway before material ADJUSTMENT only (上架 → 庫存調整).
      if (
        (a.kind === "PUTAWAY" || a.kind === "ADJUSTMENT") &&
        (b.kind === "PUTAWAY" || b.kind === "ADJUSTMENT") &&
        a.kind !== b.kind
      ) {
        return a.kind === "PUTAWAY" ? -1 : 1;
      }
      // 建立工單 before 工單提料 group / lines in the same pick phase.
      const pickLaneOrder = (n: TraceGraphLayoutNode) =>
        n.kind === "JO_CREATED" ? 0 : n.kind === "PICK_GROUP" ? 1 : n.kind === "MATERIAL_PICK" ? 2 : 3;
      const pickA = pickLaneOrder(a);
      const pickB = pickLaneOrder(b);
      if (pickA !== pickB) return pickA - pickB;
      // Then canonical phase / lane order (e.g. QC before putaway).
      if (a.laneIndex !== b.laneIndex) return a.laneIndex - b.laneIndex;
      if (a.sequenceIndex !== b.sequenceIndex) return a.sequenceIndex - b.sequenceIndex;
      return a.id.localeCompare(b.id);
    });
    dayNodes.forEach((n, i) => {
      n.dayPhaseStaggerIndex = i;
      n.branchIndex = 0;
      n.branchSize = 1;
    });
  });

  // Group children share the container's horizontal time slot.
  const parentById = new Map(nodes.map((n) => [n.id, n]));
  nodes.forEach((n) => {
    if (!n.doGroupId) return;
    const parent = parentById.get(n.doGroupId);
    if (parent) n.dayPhaseStaggerIndex = parent.dayPhaseStaggerIndex;
  });
};

const assignColumns = (
  sorted: TraceGraphNode[],
  phaseOrder: TraceGraphPhase[],
): { columnDates: string[]; nodes: TraceGraphLayoutNode[]; cellGroups: TraceGraphLayoutNode[][] } => {
  const dayOrder: string[] = [];
  const columnByDayKey = new Map<string, number>();

  const withMeta: TraceGraphLayoutNode[] = sorted.map((node, sequenceIndex) => {
    const dayKey = dayKeyFromTimestamp(node.timestamp);
    let column = columnByDayKey.get(dayKey);
    if (column === undefined) {
      column = dayOrder.length;
      dayOrder.push(dayKey);
      columnByDayKey.set(dayKey, column);
    }
    const phase = phaseFromKind(node.kind, phaseOrder, node.traceLotNo, node.refType);
    return {
      ...node,
      phase,
      dayKey,
      column,
      laneIndex: laneIndexFromPhase(phase, phaseOrder),
      sequenceIndex,
      branchIndex: 0,
      branchSize: 1,
      dayPhaseStaggerIndex: 0,
    };
  });

  // Provisional phase-based stagger (overwritten after grouping by chronological slots).
  const staggerByDay = new Map<string, TraceGraphPhase[]>();
  withMeta.forEach((node) => {
    if (!staggerByDay.has(node.dayKey)) {
      staggerByDay.set(node.dayKey, phasesOnDay(withMeta, node.dayKey, phaseOrder));
    }
    const ordered = staggerByDay.get(node.dayKey)!;
    node.dayPhaseStaggerIndex = Math.max(0, ordered.indexOf(node.phase));
  });

  const groups = new Map<string, TraceGraphLayoutNode[]>();
  withMeta.forEach((node) => {
    const key = cellKey(node.column, node.laneIndex);
    const list = groups.get(key) ?? [];
    list.push(node);
    groups.set(key, list);
  });

  groups.forEach((list) => {
    list.sort(sortNodesInPhase);
    const size = list.length;
    list.forEach((node, idx) => {
      node.branchIndex = idx;
      node.branchSize = size;
    });
  });

  const cellGroups = Array.from(groups.values()).sort((a, b) => {
    const na = a[0];
    const nb = b[0];
    if (na.column !== nb.column) return na.column - nb.column;
    return na.laneIndex - nb.laneIndex;
  });

  return { columnDates: dayOrder, nodes: withMeta, cellGroups };
};

export const buildTraceGraphLayout = (
  data: ItemLotTraceResponse,
  labels: TraceGraphDetailLabels &
    Partial<JoPreludeGraphLabels> &
    Partial<ProductionGraphLabels> &
    Partial<ExtendedTraceGraphLabels> & {
      flowDoGroupTitle?: (count: number) => string;
      flowPickGroupTitle?: (pickOrderCode: string, count: number) => string;
    },
): TraceGraphLayout => {
  const hasPrelude = data.joPrelude != null;
  const hasScrap = (data.productionSteps ?? []).some(
    (s) => (s.scrapQty ?? 0) + (s.defectQty ?? 0) > 0,
  );
  const hasMaterialProduction =
    data.joPrelude != null && materialInputsHaveProduction(data.joPrelude.materialInputs);
  const hasProduction =
    (data.productionSteps?.length ?? 0) > 0 ||
    (data.byproductLots?.length ?? 0) > 0 ||
    hasScrap ||
    hasMaterialProduction;
  const phaseOrder = getPhaseOrder(hasPrelude, hasProduction);
  const mergedMultiLocation = (data.locationBlocks?.length ?? 0) > 0;
  const primaryWh =
    data.warehouseLines.map((w) => w.warehouseCode).filter(Boolean).join(" / ") || undefined;
  const primaryScope = {
    defaultWarehouseCode: primaryWh,
    inventoryLotId: data.lot.inventoryLotId,
    mergedMultiLocation,
  };
  const fgNodes = buildTraceGraphNodes(data, labels, primaryScope);
  const preludeNodes =
    data.joPrelude && labels.nodeMaterialIn && labels.nodeMaterialPick && labels.nodePurchase
      ? buildJoPreludeGraphNodes(data.joPrelude, labels as JoPreludeGraphLabels)
      : [];
  const productionNodes =
    hasProduction && labels.nodeProductionStep && labels.nodeScrap && labels.nodeDefect
      ? buildProductionGraphNodes(
          data.productionSteps ?? [],
          data.byproductLots ?? [],
          labels as ProductionGraphLabels,
          data.lot.uom,
        )
      : [];
  const extendedNodes =
    labels.nodeOpen && labels.nodeFail && labels.nodeDoOut
      ? buildExtendedTraceGraphNodes(data, labels as ExtendedTraceGraphLabels, primaryScope)
      : [];
  const locationNodes =
    (data.locationBlocks?.length ?? 0) > 0 &&
    labels.nodeOpen &&
    labels.nodeFail &&
    labels.nodeDoOut
      ? buildAllLocationBlockGraphNodes(data.locationBlocks ?? [], labels as ExtendedTraceGraphLabels)
      : [];
  const merged = [...preludeNodes, ...productionNodes, ...extendedNodes, ...fgNodes, ...locationNodes].sort((a, b) => {
    if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
    return a.id.localeCompare(b.id);
  });
  const { columnDates, nodes: assigned, cellGroups: _initialCellGroups } = assignColumns(
    merged,
    phaseOrder,
  );
  const groupTitleDo =
    labels.flowDoGroupTitle ?? ((count: number) => `DO × ${count}`);
  const groupTitlePick =
    labels.flowPickGroupTitle ??
    ((pickOrderCode: string, count: number) => `${pickOrderCode} × ${count}`);
  const nodes = applyFlowNodeGrouping(assigned, {
    flowDoGroupTitle: groupTitleDo,
    flowPickGroupTitle: groupTitlePick,
  });
  assignChronologicalDaySlots(nodes);
  const cellGroups = regroupLayoutCells(nodes);
  const columnCount = Math.max(columnDates.length, 1);
  const dayColumns = computeDayColumnLayout(columnDates, nodes, phaseOrder);

  return {
    nodes,
    columnCount,
    columnDates,
    dayColumns,
    cellGroups,
    phaseOrder,
    laneCount: phaseOrder.length,
    hasPrelude,
    hasProduction,
  };
};
