import { z } from "zod";

const nullableStr = z.string().nullable().optional();
const nullableNum = z.number().nullable().optional();

const ItemLotTraceOriginSchema = z
  .object({
    stockInLineId: z.number(),
    type: z.string(),
    refCode: z.string(),
    refId: nullableNum,
    supplierCode: z.string(),
    supplierName: z.string(),
    dnNo: z.string(),
    receiptDate: nullableStr,
    acceptedQty: z.number(),
    status: z.string(),
  })
  .passthrough();

const ItemLotTraceQcResultSchema = z
  .object({
    stockInLineId: z.number(),
    qcPassed: z.boolean(),
    failQty: z.number(),
    acceptedQty: z.number(),
    remarks: z.string(),
    handledBy: z.string(),
    created: nullableStr,
  })
  .passthrough();

const ItemLotTracePutawayEventSchema = z
  .object({
    inventoryLotLineId: z.number(),
    stockInLineId: nullableNum,
    warehouseCode: z.string(),
    qty: z.number(),
    handledBy: z.string(),
    timestamp: nullableStr,
    refType: z.string(),
    refCode: z.string(),
    refId: nullableNum,
    status: z.string().optional(),
  })
  .passthrough();

const ItemLotTraceProductionStepSchema = z
  .object({
    processLineId: z.number(),
    stepName: z.string(),
    description: z.string(),
    equipmentCode: z.string(),
    equipmentName: z.string(),
    operatorName: z.string(),
    startTime: nullableStr,
    endTime: nullableStr,
    outputQty: z.number(),
    scrapQty: z.number(),
    defectQty: z.number(),
    status: z.string(),
    seqNo: nullableNum,
    bomProcessId: nullableNum,
  })
  .passthrough();

const ItemLotTraceMaterialInputSchema: z.ZodType<Record<string, unknown>> = z.lazy(() =>
  z
    .object({
      jobOrderCode: z.string(),
      jobOrderId: nullableNum,
      materialItemCode: z.string(),
      materialItemName: z.string(),
      materialLotNo: z.string(),
      materialStockInLineId: nullableNum,
      materialInventoryLotId: nullableNum,
      materialQty: z.number(),
      materialUom: z.string().optional(),
      bomQtyPerUnit: nullableNum,
      bomProcessId: nullableNum,
      bomProcessSeqNo: nullableNum,
      assignedStepName: z.string().optional(),
      pickOrderCode: z.string(),
      pickOrderId: nullableNum,
      consoCode: z.string(),
      pickedAt: nullableStr,
      stockInOrigin: ItemLotTraceOriginSchema.nullable().optional(),
      purchaseEvents: z.array(z.record(z.unknown())).optional(),
      qcResults: z.array(ItemLotTraceQcResultSchema),
      putawayEvents: z.array(ItemLotTracePutawayEventSchema).optional(),
      nestedJoPrelude: ItemLotTraceJoPreludeSchema.nullable().optional(),
      productionSteps: z.array(ItemLotTraceProductionStepSchema).optional(),
    })
    .passthrough(),
);

const ItemLotTraceJoPreludeSchema: z.ZodType<Record<string, unknown>> = z.lazy(() =>
  z
    .object({
      jobOrder: z
        .object({
          jobOrderId: z.number(),
          jobOrderCode: z.string(),
          planStart: nullableStr,
          createdAt: nullableStr.optional(),
          reqQty: z.number(),
          status: z.string(),
        })
        .passthrough(),
      pickOrders: z.array(z.record(z.unknown())),
      materialInputs: z.array(ItemLotTraceMaterialInputSchema),
    })
    .passthrough(),
);

const ItemLotTraceGraphEdgeSchema = z
  .object({
    fromKey: z.string(),
    toKey: z.string(),
    kind: z.string(),
  })
  .passthrough();

const ItemLotTraceGraphNodeRefSchema = z
  .object({
    key: z.string(),
    kind: z.string(),
    scopeKey: nullableStr,
    refCode: nullableStr,
    traceLotNo: nullableStr,
    traceItemCode: nullableStr,
    warehouseCode: nullableStr,
  })
  .passthrough();

const ItemLotTraceGraphSchema = z
  .object({
    nodes: z.array(ItemLotTraceGraphNodeRefSchema),
    edges: z.array(ItemLotTraceGraphEdgeSchema),
  })
  .passthrough()
  .nullable()
  .optional();

export const ItemLotTraceResponseSchema = z
  .object({
    lot: z
      .object({
        inventoryLotId: z.number(),
        lotNo: z.string(),
        itemId: z.number(),
        itemCode: z.string(),
        itemName: z.string(),
        expiryDate: nullableStr,
        productionDate: nullableStr,
        stockInDate: nullableStr,
        uom: z.string(),
        primaryStockInLineId: nullableNum,
      })
      .passthrough(),
    warehouseLines: z.array(z.record(z.unknown())),
    origins: z.array(ItemLotTraceOriginSchema),
    purchaseEvents: z.array(z.record(z.unknown())).optional(),
    qcResults: z.array(ItemLotTraceQcResultSchema),
    putawayEvents: z.array(ItemLotTracePutawayEventSchema),
    movements: z.array(z.record(z.unknown())),
    outboundUsage: z.array(z.record(z.unknown())),
    stockTakeEvents: z.array(z.record(z.unknown())),
    adjustments: z.array(z.record(z.unknown())),
    transfers: z.array(z.record(z.unknown())),
    bomTrace: z.record(z.unknown()),
    joPrelude: ItemLotTraceJoPreludeSchema.nullable(),
    productionSteps: z.array(ItemLotTraceProductionStepSchema),
    byproductLots: z.array(z.record(z.unknown())),
    openMovements: z.array(z.record(z.unknown())),
    failEvents: z.array(z.record(z.unknown())),
    doDeliveries: z.array(z.record(z.unknown())),
    replenishmentEvents: z.array(z.record(z.unknown())).optional(),
    returnEvents: z.array(z.record(z.unknown())),
    lotRelations: z.array(z.record(z.unknown())),
    alternateLocations: z.array(z.record(z.unknown())),
    locationBlocks: z.array(z.record(z.unknown())).optional(),
    traceGraph: ItemLotTraceGraphSchema,
  })
  .passthrough();

export type ItemLotTraceResponseParsed = z.infer<typeof ItemLotTraceResponseSchema>;

export const parseItemLotTraceResponse = (data: unknown): ItemLotTraceResponseParsed =>
  ItemLotTraceResponseSchema.parse(data);

/** Collect dot-path keys for contract snapshot tests. */
export const collectObjectPaths = (
  obj: unknown,
  prefix = "",
  depth = 0,
  maxDepth = 6,
): string[] => {
  if (obj == null || depth > maxDepth) return prefix ? [prefix] : [];
  if (Array.isArray(obj)) {
    if (obj.length === 0) return prefix ? [prefix] : [];
    return collectObjectPaths(obj[0], prefix ? `${prefix}[]` : "[]", depth + 1, maxDepth);
  }
  if (typeof obj !== "object") return prefix ? [prefix] : [];
  const paths: string[] = prefix ? [prefix] : [];
  Object.keys(obj as Record<string, unknown>).sort().forEach((key) => {
    const next = prefix ? `${prefix}.${key}` : key;
    paths.push(...collectObjectPaths((obj as Record<string, unknown>)[key], next, depth + 1, maxDepth));
  });
  return paths;
};
