// /home/mdares/mis-control-tower/lib/contracts/v1.ts
import { z } from "zod";

/**
 * Phase 0: freeze schema version string now and never change it without bumping.
 * If you later create v2, make a new file or new constant.
 */
export const SCHEMA_VERSION = "1.0";

// KPI scale is frozen as 0..100 (you confirmed)
const KPI_0_100 = z.number().min(0).max(100);

function asRecord(value: unknown): Record<string, unknown> | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  return value as Record<string, unknown>;
}

function unwrapCanonicalEnvelope(raw: unknown) {
  if (!raw || typeof raw !== "object") return raw;
  const obj = asRecord(raw);
  if (!obj) return raw;
  const payload = asRecord(obj.payload);
  if (!payload) return raw;

  const hasMeta =
    obj.schemaVersion !== undefined ||
    obj.machineId !== undefined ||
    obj.tsMs !== undefined ||
    obj.tsDevice !== undefined ||
    obj.seq !== undefined ||
    obj.type !== undefined;
  if (!hasMeta) return raw;

  const tsDevice =
    typeof obj.tsDevice === "number"
      ? obj.tsDevice
      : typeof obj.tsMs === "number"
        ? obj.tsMs
        : typeof payload.tsDevice === "number"
          ? payload.tsDevice
          : typeof payload.tsMs === "number"
            ? payload.tsMs
            : undefined;

  return {
    ...payload,
    schemaVersion: obj.schemaVersion ?? payload.schemaVersion,
    machineId: obj.machineId ?? payload.machineId,
    tsDevice: tsDevice ?? payload.tsDevice,
    seq: obj.seq ?? payload.seq,
  };
}

function normalizeTsDevice(raw: unknown) {
  if (!raw || typeof raw !== "object") return raw;
  const obj = asRecord(raw);
  if (!obj) return raw;
  if (typeof obj.tsDevice === "number") return obj;
  if (typeof obj.tsMs === "number") return { ...obj, tsDevice: obj.tsMs };
  return obj;
}

function preprocessPayload(raw: unknown) {
  return normalizeTsDevice(unwrapCanonicalEnvelope(raw));
}

export const SnapshotV1 = z
  .object({
    schemaVersion: z.literal(SCHEMA_VERSION),
    machineId: z.string().uuid(),
    tsDevice: z.number().int().nonnegative(), // epoch ms
    // IMPORTANT: seq should be sent as string if it can ever exceed JS safe int
    seq: z.union([z.number().int().nonnegative(), z.string().regex(/^\d+$/)]),

    // current shape (keep it flat so Node-RED changes are minimal)
    activeWorkOrder: z
    .object({
      id: z.string(),
      sku: z.string().optional(),
      target: z.number().optional(),
      good: z.number().optional(),
      scrap: z.number().optional(),

      // add the ones you actually rely on
      cycleTime: z.number().optional(),
      cavities: z.number().optional(),
      progressPercent: z.number().optional(),
      status: z.string().optional(),
      lastUpdateIso: z.string().optional(),
      cycle_count: z.number().optional(),
      good_parts: z.number().optional(),
      scrap_parts: z.number().optional(),
    })
    .partial()
    .passthrough()
    .optional(),

    cycle_count: z.number().int().nonnegative().optional(),
    good_parts: z.number().int().nonnegative().optional(),
    scrap_parts: z.number().int().nonnegative().optional(),
    cavities: z.number().int().positive().optional(),

    cycleTime: z.number().nonnegative().optional(), // theoretical/target cycle time
    actualCycleTime: z.number().nonnegative().optional(), // optional

    trackingEnabled: z.boolean().optional(),
    productionStarted: z.boolean().optional(),

    kpis: z.object({
      oee: KPI_0_100,
      availability: KPI_0_100,
      performance: KPI_0_100,
      quality: KPI_0_100,
    }),
  })
  .passthrough();

/**
 * TEMPORARY: Accept your current legacy payload while Node-RED is not sending
 * schemaVersion/tsDevice/seq yet. Remove this once edge is upgraded.
 */
const SnapshotLegacy = z
  .object({
    machineId: z.any(),
    kpis: z.any(),
  })
  .passthrough();

export type SnapshotV1Type = z.infer<typeof SnapshotV1>;

export function normalizeSnapshotV1(raw: unknown): { ok: true; value: SnapshotV1Type } | { ok: false; error: string } {
  const candidate = preprocessPayload(raw);
  const strict = SnapshotV1.safeParse(candidate);
  if (strict.success) return { ok: true, value: strict.data };

  // Legacy fallback (temporary)
  const legacy = SnapshotLegacy.safeParse(candidate);
  if (!legacy.success) {
    return { ok: false, error: strict.error.message };
  }
/*
  const b: any = legacy.data;

  // Build a "best effort" SnapshotV1 so ingest works during transition.
  // seq is intentionally set to "0" if missing (so you can still store);
  // once Node-RED emits real seq, dedupe and ordering become reliable.
  const migrated: any = {
    schemaVersion: SCHEMA_VERSION,
    machineId: String(b.machineId),
    tsDevice: typeof b.tsDevice === "number" ? b.tsDevice : Date.now(),
    seq: typeof b.seq === "number" || typeof b.seq === "string" ? b.seq : "0",
    ...b,
  };

  const recheck = SnapshotV1.safeParse(migrated);
  if (!recheck.success) return { ok: false, error: recheck.error.message };
  return { ok: true, value: recheck.data };
  */
  const b = asRecord(legacy.data) ?? {};
  const activeWorkOrder = asRecord(b.activeWorkOrder);
  const kpis = asRecord(b.kpis);
  const kpiSnapshot = asRecord(b.kpi_snapshot);

  const legacyCycleTime =
    b.cycleTime ??
    b.cycle_time ??
    b.theoretical_cycle_time ??
    b.theoreticalCycleTime ??
    b.standard_cycle_time ??
    kpiSnapshot?.cycleTime ??
    kpiSnapshot?.cycle_time ??
    undefined;

  const legacyActualCycleTime =
    b.actualCycleTime ??
    b.actual_cycle_time ??
    b.actualCycleSeconds ??
    kpiSnapshot?.actualCycleTime ??
    kpiSnapshot?.actual_cycle_time ??
    undefined;

  const legacyWorkOrderId =
    activeWorkOrder?.id ??
    b.work_order_id ??
    b.workOrderId ??
    kpis?.workOrderId ??
    kpiSnapshot?.work_order_id ??
    undefined;

  const legacySku =
    activeWorkOrder?.sku ??
    b.sku ??
    kpis?.sku ??
    kpiSnapshot?.sku ??
    undefined;

  const legacyTarget =
    activeWorkOrder?.target ??
    b.target ??
    kpis?.target ??
    kpiSnapshot?.target ??
    undefined;

  const legacyGood =
    activeWorkOrder?.good ??
    b.good_parts ??
    b.good ??
    kpis?.good ??
    kpiSnapshot?.good_parts ??
    undefined;

  const legacyScrap =
    activeWorkOrder?.scrap ??
    b.scrap_parts ??
    b.scrap ??
    kpis?.scrap ??
    kpiSnapshot?.scrap_parts ??
    undefined;

  const migrated: Record<string, unknown> = {
    schemaVersion: SCHEMA_VERSION,
    machineId: String(b.machineId),
    tsDevice: typeof b.tsDevice === "number" ? b.tsDevice : Date.now(),
    seq: typeof b.seq === "number" || typeof b.seq === "string" ? b.seq : "0",

    // canonical fields (force them)
    cycleTime: legacyCycleTime != null ? Number(legacyCycleTime) : undefined,
    actualCycleTime: legacyActualCycleTime != null ? Number(legacyActualCycleTime) : undefined,

    activeWorkOrder: legacyWorkOrderId
      ? {
          id: String(legacyWorkOrderId),
          sku: legacySku != null ? String(legacySku) : undefined,
          target: legacyTarget != null ? Number(legacyTarget) : undefined,
          good: legacyGood != null ? Number(legacyGood) : undefined,
          scrap: legacyScrap != null ? Number(legacyScrap) : undefined,
        }
      : activeWorkOrder,

    // keep everything else
    ...b,
  };
  const recheck = SnapshotV1.safeParse(migrated);
  if (!recheck.success) return { ok: false, error: recheck.error.message };
  return { ok: true, value: recheck.data };

}

const HeartbeatV1 = z.object({
  schemaVersion: z.literal(SCHEMA_VERSION),
  machineId: z.string().uuid(),
  tsDevice: z.number().int().nonnegative(),
  seq: z.union([z.number().int().nonnegative(), z.string().regex(/^\d+$/)]),

  // legacy shape you currently send: status/message/ip/fwVersion
  status: z.string().optional(),
  message: z.string().optional(),
  ip: z.string().optional(),
  fwVersion: z.string().optional(),

  // new canonical boolean
  online: z.boolean().optional(),
}).passthrough();

export function normalizeHeartbeatV1(raw: unknown) {
  const candidate = preprocessPayload(raw);
  const strict = HeartbeatV1.safeParse(candidate);
  if (strict.success) return { ok: true as const, value: strict.data };

  // legacy fallback: allow missing meta
  const legacy = z.object({ machineId: z.any() }).passthrough().safeParse(candidate);
  if (!legacy.success) return { ok: false as const, error: strict.error.message };

  const b = asRecord(legacy.data) ?? {};
  const migrated: Record<string, unknown> = {
    schemaVersion: SCHEMA_VERSION,
    machineId: String(b.machineId),
    tsDevice: typeof b.tsDevice === "number" ? b.tsDevice : Date.now(),
    seq: typeof b.seq === "number" || typeof b.seq === "string" ? b.seq : "0",
    ...b,
  };

  const recheck = HeartbeatV1.safeParse(migrated);
  if (!recheck.success) return { ok: false as const, error: recheck.error.message };
  return { ok: true as const, value: recheck.data };
}

const CycleV1 = z.object({
  schemaVersion: z.literal(SCHEMA_VERSION),
  machineId: z.string().uuid(),
  tsDevice: z.number().int().nonnegative(),
  seq: z.union([z.number().int().nonnegative(), z.string().regex(/^\d+$/)]),

  cycle: z.object({
    timestamp: z.number().int().positive(),
    cycle_count: z.number().int().nonnegative(),
    actual_cycle_time: z.number(),
    theoretical_cycle_time: z.number().optional(),
    work_order_id: z.string(),
    sku: z.string().optional(),
    cavities: z.number().optional(),
    good_delta: z.number().optional(),
    scrap_total: z.number().optional(),
  }).passthrough(),
}).passthrough();

export function normalizeCycleV1(raw: unknown) {
  const candidate = preprocessPayload(raw);
  const strict = CycleV1.safeParse(candidate);
  if (strict.success) return { ok: true as const, value: strict.data };

  // legacy fallback: { machineId, cycle }
  const legacy = z.object({ machineId: z.any(), cycle: z.any() }).passthrough().safeParse(candidate);
  if (!legacy.success) return { ok: false as const, error: strict.error.message };

  const b = asRecord(legacy.data) ?? {};
  const cycle = asRecord(b.cycle);
  const tsDevice =
    typeof b.tsDevice === "number" ? b.tsDevice : (cycle?.timestamp as number | undefined) ?? Date.now();
  const seq =
    typeof b.seq === "number" || typeof b.seq === "string"
      ? b.seq
      : (cycle?.cycle_count as number | string | undefined) ?? "0";

  const migrated: Record<string, unknown> = {
    schemaVersion: SCHEMA_VERSION,
    machineId: String(b.machineId),
    tsDevice,
    seq,
    ...b,
  };
  const recheck = CycleV1.safeParse(migrated);
  if (!recheck.success) return { ok: false as const, error: recheck.error.message };
  return { ok: true as const, value: recheck.data };
}

const EventV1 = z.object({
  schemaVersion: z.literal(SCHEMA_VERSION),
  machineId: z.string().uuid(),
  tsDevice: z.number().int().nonnegative(),
  seq: z.union([z.number().int().nonnegative(), z.string().regex(/^\d+$/)]),

  // IMPORTANT: event must be an object, not an array
  event: z.object({
    anomaly_type: z.string(),
    severity: z.string(),
    title: z.string(),
    description: z.string().optional(),
    timestamp: z.number().int().positive(),
    work_order_id: z.string(),
    cycle_count: z.number().optional(),
    data: z.any().optional(),
    kpi_snapshot: z.any().optional(),
  }).passthrough(),
}).passthrough();

export function normalizeEventV1(raw: unknown) {
  const candidate = preprocessPayload(raw);
  const strict = EventV1.safeParse(candidate);
  if (strict.success) return { ok: true as const, value: strict.data };

  // legacy fallback: allow missing meta, but STILL reject arrays later
  const legacy = z.object({ machineId: z.any(), event: z.any() }).passthrough().safeParse(candidate);
  if (!legacy.success) return { ok: false as const, error: strict.error.message };

  const b = asRecord(legacy.data) ?? {};
  const event = asRecord(b.event);
  const tsDevice =
    typeof b.tsDevice === "number" ? b.tsDevice : (event?.timestamp as number | undefined) ?? Date.now();
  const migrated: Record<string, unknown> = {
    schemaVersion: SCHEMA_VERSION,
    machineId: String(b.machineId),
    tsDevice,
    seq: typeof b.seq === "number" || typeof b.seq === "string" ? b.seq : "0",
    ...b,
  };

  const recheck = EventV1.safeParse(migrated);
  if (!recheck.success) return { ok: false as const, error: recheck.error.message };
  return { ok: true as const, value: recheck.data };
}
