// SPEC: PJ2026-0103 HarnessRL draft-2026-06-25-p0-web-caserun-e2e.
// Responsibility: derive CaseRun validation plans and normalize authoritative HWPOD operation observations.

import { clean, firstNumberOption, firstTextOption, parseJsonMaybe, text } from "./hwlab-caserun-evidence.ts";

export type CaseValidationStepKind = "build" | "download" | "uart" | "ioProbe" | "boardComm";

export type CaseValidationStep = {
  actionId: string;
  stage: "post-agent-validation";
  kind: CaseValidationStepKind;
  expectedOp: string;
  args: Record<string, unknown>;
};

export type CaseValidationPlan = {
  contractVersion: "hwpod-case-validation-plan-v1";
  mode: "compile-only" | "download-uart" | "io-probe" | "board-comm" | "custom-runtime";
  compileOnly: boolean;
  source: "case-definition.validation" | "case-definition.compileOnly" | "default-compile-only";
  steps: CaseValidationStep[];
};

export function caseValidationPlanFromDefinition(definition: Record<string, unknown>): CaseValidationPlan {
  const validation = record(definition.validation);
  const explicitSteps = Array.isArray(validation.steps) ? validation.steps.map(record).filter((item) => Object.keys(item).length > 0) : [];
  const rawMode = text(validation.mode).toLowerCase();
  const compileOnlyValue = booleanValue(validation.compileOnly ?? definition.compileOnly ?? definition.compile_only);
  const mode = validationMode(rawMode, compileOnlyValue, explicitSteps);
  const source = Object.keys(validation).length > 0
    ? "case-definition.validation"
    : compileOnlyValue !== undefined
      ? "case-definition.compileOnly"
      : "default-compile-only";
  const declaredSteps = explicitSteps.length > 0 ? explicitSteps : defaultSteps(mode, validation);
  const steps = declaredSteps.map((step, index) => normalizeStep(step, index));
  if (!steps.some((step) => step.kind === "build")) steps.unshift(normalizeStep({ kind: "build" }, -1));
  return {
    contractVersion: "hwpod-case-validation-plan-v1",
    mode,
    compileOnly: steps.every((step) => step.kind === "build"),
    source,
    steps
  };
}

export function hwpodCommandForValidationStep(step: CaseValidationStep, input: { executable: string; cliPath: string; specPath: string; reason: string }) {
  const common = [input.executable, input.cliPath];
  if (step.kind === "build") return [...common, "build", "--spec", input.specPath, "--reason", input.reason];
  if (step.kind === "download") return [...common, "download", "--spec", input.specPath, "--reason", input.reason];
  if (step.kind === "uart") return [...common, "uart", "read", "--spec", input.specPath, ...optionArgs(step.args, ["port", "maxBytes", "limit", "since", "timeoutMs"])];
  if (step.kind === "ioProbe") return [...common, "io-probe", "read", text(step.args.probeId), "--spec", input.specPath, ...optionArgs(step.args, ["count", "intervalMs", "settleMs"])].filter(Boolean);
  return [...common, "board-comm", "jrpctcp", text(step.args.method) || "get", text(step.args.path) || "api", "--spec", input.specPath, ...repeatArgs(step.args.params)].filter(Boolean);
}

export function normalizeValidationStepResult(input: { step: CaseValidationStep; payload: any; document: any; exitCode: number; stderr?: string }) {
  const response = record(input.payload?.body?.body ?? input.payload?.body ?? input.payload);
  const results = Array.isArray(response.results) ? response.results : [];
  const result = results.find((item: any) => text(item?.opId) === input.step.actionId || text(item?.op) === input.step.expectedOp) ?? results[0] ?? {};
  const output = record(result?.output);
  const outputJson = firstObject(result?.observation, output.observation);
  const expected = {
    hwpodId: text(input.document?.metadata?.name ?? input.document?.metadata?.uid),
    nodeId: text(input.document?.spec?.nodeBinding?.nodeId),
    workspacePath: text(input.document?.spec?.workspace?.path),
    op: input.step.expectedOp,
    actionId: input.step.actionId
  };
  const observed = {
    hwpodId: firstTextOption(response.hwpodId, result?.hwpodId, output.hwpodId),
    nodeId: firstTextOption(response.nodeId, result?.nodeId, output.nodeId),
    workspacePath: firstTextOption(result?.workspacePath, output.workspacePath, output.cwd),
    op: text(result?.op),
    actionId: text(result?.opId) || input.step.actionId
  };
  const mismatches = Object.entries(expected).flatMap(([field, value]) => {
    if (!value || field === "actionId") return [];
    const observedValue = observed[field as keyof typeof observed];
    return observedValue === value ? [] : [{ field, expected: value, observed: observedValue || null }];
  });
  const operation = clean({
    contractVersion: "hwpod-operation-result-v0.3",
    authority: "hwpod-service",
    actionId: input.step.actionId,
    stage: input.step.stage,
    kind: input.step.kind,
    op: observed.op || input.step.expectedOp,
    opId: text(result?.opId),
    status: text(result?.status ?? response.status ?? input.payload?.status) || "unknown",
    ok: input.exitCode === 0 && result?.ok === true && mismatches.length === 0,
    expected,
    observed,
    topology: mismatches.length === 0 ? { ok: true } : { ok: false, mismatches },
    rawArtifactRef: artifactRef(result, outputJson),
    blocker: result?.blocker ?? input.payload?.diagnostic ?? null
  });
  const observation = normalizeObservation(input.step, result, outputJson, operation);
  const blocker = validationBlocker(input.step, operation, observation, input.exitCode, input.stderr);
  return clean({ operation, observation, blocker });
}

export function validationReplayRelationship(plan: CaseValidationPlan, results: any[]) {
  const observations = results.map((item) => item?.observation).filter(Boolean);
  return clean({
    contractVersion: "hwpod-case-validation-replay-v1",
    source: "archived-hwpod-operation-observations",
    planMode: plan.mode,
    actionIds: plan.steps.map((step) => step.actionId),
    operationIds: results.map((item) => text(item?.operation?.opId)).filter(Boolean),
    observationIds: observations.map((item) => text(item.observationId)).filter(Boolean),
    relationships: observations.map((item) => clean({
      observationId: item.observationId,
      actionId: item.actionId,
      operationId: item.operationId,
      stage: item.stage,
      kind: item.kind,
      probeId: item.probeId,
      quantity: item.quantity,
      unit: item.unit,
      rawArtifactRef: item.rawArtifactRef,
      evidenceLevel: item.evidenceLevel
    }))
  });
}

function normalizeObservation(step: CaseValidationStep, result: any, body: any, operation: any) {
  if (step.kind !== "ioProbe" && step.kind !== "boardComm" && step.kind !== "uart") return undefined;
  if (Object.keys(record(body)).length === 0) return undefined;
  const samples = numberArray(body.samples ?? body.values ?? body.rawSamples?.map?.((item: any) => item?.value));
  const stats = record(body.stats ?? body.statistics);
  const sampleCount = firstNumberOption(body.sampleCount, stats.count, samples.length) ?? 0;
  const mean = firstNumberOption(stats.mean, body.mean, body.value, average(samples));
  const minimum = firstNumberOption(stats.min, body.min, samples.length > 0 ? Math.min(...samples) : undefined);
  const maximum = firstNumberOption(stats.max, body.max, samples.length > 0 ? Math.max(...samples) : undefined);
  const rawArtifactRef = artifactRef(result, body) || operation.rawArtifactRef;
  const operationId = text(operation.opId) || step.actionId;
  return clean({
    contractVersion: "hwpod-observation-result-v1",
    authority: "hwpod-service",
    observationId: text(body.observationId ?? body.id) || `${step.actionId}:${operationId}`,
    actionId: step.actionId,
    operationId,
    stage: step.stage,
    kind: step.kind,
    status: text(body.status ?? operation.status),
    probeId: firstTextOption(body.probeId, body.probe_id, step.args.probeId),
    quantity: firstTextOption(body.quantity, step.args.quantity),
    unit: firstTextOption(body.unit, step.args.unit),
    sampleCount,
    mean,
    min: minimum,
    max: maximum,
    response: step.kind === "boardComm" ? firstObject(body.response, body.result, body.body, body) : undefined,
    rawArtifactRef,
    evidenceLevel: evidenceLevel(step.kind),
    ok: operation.ok === true && (step.kind === "boardComm" ? Object.keys(firstObject(body.response, body.result, body.body, body)).length > 0 : step.kind === "uart" ? Boolean(rawArtifactRef || text(body.stdout ?? body.text ?? body.data)) : sampleCount > 0 && mean !== undefined)
  });
}

function validationBlocker(step: CaseValidationStep, operation: any, observation: any, exitCode: number, stderr?: string) {
  if (operation.topology?.ok === false) return { code: "hwpod_validation_topology_mismatch", summary: "HWPOD validation operation result does not match the case-selected topology", details: operation.topology.mismatches };
  if (operation.blocker) return operation.blocker;
  if (exitCode !== 0 || operation.ok !== true) return { code: "hwpod_validation_operation_failed", summary: `HWPOD ${step.kind} validation operation failed`, details: clean({ actionId: step.actionId, op: step.expectedOp, exitCode, status: operation.status, stderr: text(stderr) }) };
  if ((step.kind === "ioProbe" || step.kind === "boardComm" || step.kind === "uart") && !observation) return { code: "hwpod_validation_observation_missing", summary: `HWPOD ${step.kind} operation returned no observation result`, details: { actionId: step.actionId, operationId: operation.opId || null } };
  if (observation && observation.ok !== true) return { code: "hwpod_validation_observation_inconsistent", summary: `HWPOD ${step.kind} observation is missing required authoritative fields`, details: clean({ actionId: step.actionId, operationId: operation.opId, probeId: observation.probeId, sampleCount: observation.sampleCount, rawArtifactRef: observation.rawArtifactRef }) };
  return null;
}

function validationMode(rawMode: string, compileOnly: boolean | undefined, steps: Record<string, unknown>[]) {
  if (steps.length > 0) return steps.every((step) => text(step.kind) === "build") ? "compile-only" : "custom-runtime";
  if (["compile-only", "compile", "build-only"].includes(rawMode)) return "compile-only";
  if (["download-uart", "uart", "runtime-uart"].includes(rawMode)) return "download-uart";
  if (["io-probe", "ioprobe"].includes(rawMode)) return "io-probe";
  if (["board-comm", "boardcomm"].includes(rawMode)) return "board-comm";
  return compileOnly === false ? "custom-runtime" : "compile-only";
}

function defaultSteps(mode: CaseValidationPlan["mode"], validation: Record<string, unknown>) {
  const build = { kind: "build" };
  if (mode === "compile-only") return [build];
  if (mode === "download-uart") return [build, { kind: "download" }, { kind: "uart", ...record(validation.uart) }];
  if (mode === "io-probe") return [build, { kind: "download" }, { kind: "ioProbe", ...record(validation.ioProbe) }];
  if (mode === "board-comm") return [build, { kind: "download" }, { kind: "boardComm", ...record(validation.boardComm) }];
  return [build, ...[validation.download === false ? null : { kind: "download" }, validation.uart ? { kind: "uart", ...record(validation.uart) } : null, validation.ioProbe ? { kind: "ioProbe", ...record(validation.ioProbe) } : null, validation.boardComm ? { kind: "boardComm", ...record(validation.boardComm) } : null].filter(Boolean) as Record<string, unknown>[]];
}

function normalizeStep(step: Record<string, unknown>, index: number): CaseValidationStep {
  const rawKind = text(step.kind).toLowerCase().replace(/[-_]/gu, "");
  const kind: CaseValidationStepKind = rawKind === "download" ? "download" : rawKind === "uart" || rawKind === "uartread" ? "uart" : rawKind === "ioprobe" || rawKind === "ioproberead" ? "ioProbe" : rawKind === "boardcomm" ? "boardComm" : "build";
  const sequence = index < 0 ? 1 : index + 1;
  const expectedOp = kind === "download" ? "debug.download" : kind === "uart" ? "io.uart.read" : kind === "ioProbe" || kind === "boardComm" ? "cmd.run" : "debug.build";
  return { actionId: text(step.actionId) || `validation_${String(sequence).padStart(2, "0")}_${kind}`, stage: "post-agent-validation", kind, expectedOp, args: clean({ ...step, kind: undefined, actionId: undefined }) };
}

function optionArgs(args: Record<string, unknown>, keys: string[]) {
  return keys.flatMap((key) => args[key] === undefined ? [] : [`--${key.replace(/[A-Z]/gu, (value) => `-${value.toLowerCase()}`)}`, String(args[key])]);
}

function repeatArgs(value: unknown) {
  return Array.isArray(value) ? value.flatMap((item) => ["--param", String(item)]) : [];
}

function artifactRef(...values: any[]) {
  for (const value of values) {
    const candidate = firstTextOption(value?.rawArtifactRef, value?.artifactRef, value?.artifact?.ref, value?.artifact?.path, value?.rawEvidence?.artifactRef, value?.rawEvidence?.logPath, value?.log_path, value?.logPath);
    if (candidate) return candidate;
  }
  return "";
}

function evidenceLevel(kind: CaseValidationStepKind) {
  if (kind === "ioProbe") return "external-physical-reading";
  if (kind === "boardComm") return "board-internal-response";
  if (kind === "uart") return "serial-log";
  return "operation-result";
}

function record(value: unknown): Record<string, any> { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {}; }
function firstObject(...values: unknown[]) { return values.map(record).find((value) => Object.keys(value).length > 0) ?? {}; }
function booleanValue(value: unknown) { if (typeof value === "boolean") return value; const normalized = text(value).toLowerCase(); return normalized === "true" ? true : normalized === "false" ? false : undefined; }
function numberArray(value: unknown) { return Array.isArray(value) ? value.map(Number).filter(Number.isFinite) : []; }
function average(values: number[]) { return values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : undefined; }
