// SPEC: PJ2026-0103 HarnessRL draft-2026-06-25-p0-web-caserun-e2e.
// Responsibility: normalize AgentRun provider, terminal, and tool-call evidence for CaseRun.

import { createHash } from "node:crypto";
import path from "node:path";

export type AgentToolCallSummaryItem = {
  source: string;
  index: number;
  kind?: string;
  toolName?: string;
  status?: string;
  exitCode?: number;
  command?: string;
  normalizedCommand?: string;
  detail?: string;
};

export type AgentToolCallSummary = {
  source: string;
  count: number;
  statusCounts: Record<string, number>;
  exitCodeCounts: Record<string, number>;
  terminalStatus?: string;
  commandStatus?: string;
  agentRunStatus?: string;
  items: AgentToolCallSummaryItem[];
  hwpodRawSteps: AgentToolCallSummaryItem[];
};

export function agentWithResultEvidence(agent: any, resultBody: any, agentTask: any = {}) {
  const root = resultBody?.body && typeof resultBody.body === "object" ? resultBody.body : resultBody;
  return clean({ ...agent, ...agentTerminalEvidence(root), ...agentProviderEvidence({ ...agent, result: root }, agentTask), toolCallSummary: agentToolCallSummaryFromResult(root), result: compactObject(resultBody) });
}

export function agentTerminalEvidence(value: any) {
  const root = value?.body && typeof value.body === "object" ? value.body : value;
  const agentRun = firstObject(root?.agentRun, root?.traceSummary?.agentRun, root?.terminalEvidence?.agentRun, root?.terminalEvidence?.traceSummary?.agentRun);
  const terminalStatus = firstTextOption(root?.terminalStatus, root?.traceSummary?.terminalStatus, root?.terminalEvidence?.terminalStatus, agentRun?.terminalStatus);
  const commandStatus = firstTextOption(root?.commandStatus, root?.traceSummary?.commandStatus, root?.terminalEvidence?.commandStatus, agentRun?.commandStatus, agentRun?.commandState);
  const agentRunStatus = firstTextOption(root?.agentRunStatus, root?.traceSummary?.agentRunStatus, root?.terminalEvidence?.agentRunStatus, agentRun?.runStatus, agentRun?.status);
  const terminal = [terminalStatus, commandStatus, agentRunStatus].some((status) => /^(?:completed|failed|cancelled|canceled|timed_out|timeout|natural_end)$/iu.test(status));
  return clean({ terminalStatus, commandStatus, agentRunStatus, terminal });
}

export function agentToolCallSummaryFromResult(value: any): AgentToolCallSummary | null {
  const root = value?.body && typeof value.body === "object" ? value.body : value;
  const summary = firstObject(root?.toolCallSummary, root?.traceSummary?.toolCallSummary, root?.terminalEvidence?.toolCallSummary, root?.terminalEvidence?.traceSummary?.toolCallSummary, root?.agentRun?.toolCallSummary, value?.toolCallSummary);
  const items = firstArray(summary?.items, summary?.toolCalls, summary?.calls, summary?.commands).map((item, index) => toolCallItem(item, index)).filter((item) => item.command || item.status || item.exitCode !== undefined || item.toolName);
  const statusCounts = countedRecord(summary?.statusCounts ?? summary?.status_counts, items.map((item) => item.status));
  const exitCodeCounts = countedRecord(summary?.exitCodeCounts ?? summary?.exit_code_counts, items.map((item) => item.exitCode === undefined ? "" : String(item.exitCode)));
  const count = firstNumberOption(summary?.count, summary?.total, summary?.totalCount, items.length || undefined, sumRecordValues(statusCounts), sumRecordValues(exitCodeCounts)) ?? 0;
  if (Object.keys(summary).length === 0 && count === 0 && items.length === 0) return null;
  const terminal = agentTerminalEvidence(root);
  return clean({ source: text(summary.source) || "agentrun-result.toolCallSummary", count, statusCounts, exitCodeCounts, terminalStatus: terminal.terminalStatus, commandStatus: terminal.commandStatus, agentRunStatus: terminal.agentRunStatus, items, hwpodRawSteps: items.filter((item) => ["build", "download", "uart-read"].includes(text(item.kind))) });
}

export function commandsFromToolCallSummary(summary: AgentToolCallSummary | null) {
  return (summary?.items ?? []).flatMap((item) => item.command ? [clean({ source: item.source, seq: item.index + 1, toolName: item.toolName, status: item.status, command: item.command, normalizedCommand: item.normalizedCommand, bodyPreview: item.detail, exitCode: item.exitCode })] : []);
}

export function mentionsHwpod(command: { command?: string; normalizedCommand?: string }) {
  const raw = normalizeCommand(command.normalizedCommand || command.command);
  return /^(?:hwpod|hwpod-ctl|hwpod-cli)(?:\s|$)/iu.test(raw) || /^(?:bun\s+)?tools\/hwpod-cli\.ts(?:\s|$)/iu.test(raw);
}

export function hwpodCommandKind(command: { command?: string; normalizedCommand?: string }) {
  return commandKind(normalizeCommand(command.normalizedCommand || command.command));
}

export function applyCaseRunEvidenceRelationships(evidence: any, run: any) {
  const resultBody = run.agent?.result?.body && typeof run.agent.result.body === "object" ? run.agent.result.body : run.agent?.result;
  const terminal = agentTerminalEvidence(resultBody);
  const finalResponse = firstObject(resultBody?.finalResponse, resultBody?.reply);
  const finalText = firstTextOption(finalResponse?.text, finalResponse?.content, typeof resultBody?.finalResponse === "string" ? resultBody.finalResponse : "", typeof resultBody?.reply === "string" ? resultBody.reply : "");
  const agentWarningCount = firstNumberOption(resultBody?.warningCount, resultBody?.buildWarningCount, resultBody?.build?.warningCount, resultBody?.agentBuild?.warningCount);
  const runnerWarningCount = firstNumberOption(evidence.keilJob?.warningCount, evidence.keilJob?.warning_count, evidence.keilJob?.summary?.warningCount, evidence.hwpod?.stdoutJson?.warningCount);
  evidence.agentFinal = clean({ source: "agentrun-result", present: Boolean(finalText || finalResponse?.present === true), text: finalText, stageStatus: run.agent?.stageStatus, timedOut: run.agent?.timedOut === true, terminalStatus: terminal.terminalStatus, commandStatus: terminal.commandStatus, agentRunStatus: terminal.agentRunStatus, naturalEnd: terminal.terminal === true && run.agent?.timedOut !== true, missingReason: finalText ? undefined : terminal.terminal ? "agentrun-terminal-without-final-response" : run.agent?.timedOut ? "caserun-poll-timeout-before-agentrun-terminal" : "final-response-not-reported" });
  evidence.postValidation = clean({ source: evidence.hwpod?.source, jobId: evidence.keilJob?.jobId, status: evidence.keilJob?.status, returnCode: evidence.keilJob?.returnCode ?? evidence.hwpod?.exitCode, warningCount: runnerWarningCount, boundary: "runner-post-agent-validation" });
  evidence.warnings = clean({ agentReportedBuildWarningCount: agentWarningCount, runnerPostValidationWarningCount: runnerWarningCount });
}

function agentProviderEvidence(agent: any, agentTask: any = {}) {
  const root = agent?.result?.body && typeof agent.result.body === "object" ? agent.result.body : agent?.result;
  const agentRun = firstObject(root?.agentRun, root?.traceSummary?.agentRun, root?.terminalEvidence?.agentRun);
  const providerTrace = firstObject(root?.providerTrace, root?.traceSummary?.providerTrace, root?.terminalEvidence?.providerTrace, agentRun?.providerTrace);
  const requestedProviderProfile = firstTextOption(agent?.requestedProviderProfile, root?.requestedProviderProfile, root?.providerProfile, agent?.providerProfile, agentTask?.providerProfile);
  const resolvedBackendProfile = firstTextOption(agent?.resolvedBackendProfile, root?.resolvedBackendProfile, root?.backendProfile, agentRun?.backendProfile, providerTrace?.backendProfile);
  const backend = firstTextOption(agent?.backend, root?.backend, agentRun?.backend, resolvedBackendProfile);
  return clean({ requestedProviderProfile, resolvedBackendProfile, provider: firstTextOption(agent?.provider, root?.provider, providerTrace?.provider), model: firstTextOption(agent?.model, root?.model, agentRun?.model, providerTrace?.model), backend, infrastructureBackend: firstTextOption(agent?.infrastructureBackend, root?.infrastructureBackend, agentRun?.infrastructureBackend, backend), providerTrace: Object.keys(providerTrace).length > 0 ? compactObject(providerTrace) : null });
}

function toolCallItem(value: Record<string, unknown>, index: number): AgentToolCallSummaryItem {
  const input = firstObject(value.input, value.args, value.arguments);
  const output = firstObject(value.output, value.result);
  const command = firstTextOption(value.command, input.command, input.cmd, Array.isArray(input.argv) ? input.argv.join(" ") : input.argv, output.command);
  const normalizedCommand = normalizeCommand(command);
  return clean({ source: "agentrun-result.toolCallSummary", index, kind: commandKind(normalizedCommand), toolName: firstTextOption(value.toolName, value.name, value.tool), status: firstTextOption(value.status, output.status), exitCode: firstNumberOption(value.exitCode, value.exit_code, output.exitCode, output.exit_code), command: clipText(command, 1000), normalizedCommand: clipText(normalizedCommand, 1000), detail: clipText(firstTextOption(value.detail, value.summary, output.stderr, output.stdout), 1000) });
}

function commandKind(raw: string) {
  if (/^(?:hwpod|hwpod-cli)\s+build(?:\s|$)/iu.test(raw) || /^(?:bun\s+)?tools\/hwpod-cli\.ts\s+build(?:\s|$)/iu.test(raw)) return "build";
  if (/^(?:hwpod|hwpod-cli)\s+download(?:\s|$)/iu.test(raw) || /^(?:bun\s+)?tools\/hwpod-cli\.ts\s+download(?:\s|$)/iu.test(raw)) return "download";
  if (/^(?:hwpod|hwpod-cli)\s+(?:uart\s+read|io\s+uart\s+read)(?:\s|$)/iu.test(raw) || /^(?:bun\s+)?tools\/hwpod-cli\.ts\s+(?:uart\s+read|io\s+uart\s+read)(?:\s|$)/iu.test(raw)) return "uart-read";
  if (/^(?:hwpod-ctl|hwpod-cli)\s+spec\s+validate(?:\s|$)/iu.test(raw)) return "spec-validate";
  if (/^hwpod\s+inspect(?:\s|$)/iu.test(raw)) return "inspect";
  if (/^hwpod\s+workspace\s+(?:insert-after|apply-patch|write|replace|append)(?:\s|$)/iu.test(raw)) return "workspace-edit";
  return "";
}

function normalizeCommand(value: unknown) { return text(value).replace(/^\/bin\/sh\s+-lc\s+/u, "").replace(/^['"]|['"]$/gu, "").replace(/\s+/gu, " ").trim(); }
export function firstObject(...values: any[]): Record<string, any> { return values.find((value) => value && typeof value === "object" && !Array.isArray(value)) ?? {}; }
function firstArray(...values: any[]): Record<string, unknown>[] { return (values.find(Array.isArray) ?? []).filter((value: any) => value && typeof value === "object" && !Array.isArray(value)); }
export function firstTextOption(...values: any[]) { return values.map(text).find(Boolean) ?? ""; }
export function firstNumberOption(...values: any[]) { for (const value of values) { const number = numberOption(value); if (number !== undefined) return number; } return undefined; }
function countedRecord(value: any, fallbacks: Array<string | undefined>) { if (value && typeof value === "object" && !Array.isArray(value)) return Object.fromEntries(Object.entries(value).map(([key, count]) => [key, Number(count) || 0])); return fallbacks.filter(Boolean).reduce<Record<string, number>>((result, key) => ({ ...result, [key as string]: (result[key as string] ?? 0) + 1 }), {}); }
function sumRecordValues(value: Record<string, number>) { return Object.values(value).reduce((sum, count) => sum + count, 0); }
export function compactObject(value: any) {
  const json = JSON.stringify(value ?? null);
  return json.length <= 4000
    ? value
    : { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) };
}

export function clipText(value: unknown, maxBytes = 2000) {
  const raw = String(value ?? "");
  const buffer = Buffer.from(raw, "utf8");
  return buffer.length <= maxBytes
    ? raw
    : `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`;
}

export function numberOption(value: unknown) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  return Number.isFinite(parsed) ? parsed : undefined;
}

export function parseJsonMaybe(value: unknown) {
  if (value && typeof value === "object") return value as any;
  try {
    return JSON.parse(String(value ?? "").trim());
  } catch {
    return null;
  }
}

export function text(value: unknown) {
  return String(value ?? "").trim();
}

export function sha256(value: string | Uint8Array) {
  return createHash("sha256").update(value).digest("hex");
}

export function commandVisibility(command: string[]) {
  return command.map((item) => item.includes("/") || item.includes("\\") ? path.basename(item) || item : item);
}

export function cliError(code: string, message: string, details: Record<string, unknown> = {}) {
  return Object.assign(new Error(message), { code, details });
}

export function clean<T extends Record<string, unknown>>(value: T): T {
  return Object.fromEntries(
    Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null),
  ) as T;
}

type AgentTraceCommand = { source: string; seq?: number; rowId?: string; header?: string; toolName?: string; status?: string; command?: string; normalizedCommand?: string; bodyPreview?: string; exitCode?: number };
const MAX_AGENT_TRACE_COMMANDS = 30;

export function summarizeAgentTrace(traceId: string, httpStatus: number, body: any) {
  const traceBody = body?.body ?? body;
  const rows = arrayOfObjects(traceBody?.rows ?? traceBody?.renderedRows ?? traceBody?.traceRows);
  const events = arrayOfObjects(traceBody?.events ?? traceBody?.trace?.events ?? traceBody?.sourceEvents);
  const toolCallSummary = agentToolCallSummaryFromResult(traceBody);
  const traceCommands = [...commandsFromTraceRows(rows), ...commandsFromTraceEvents(events)];
  const summaryCommands = commandsFromToolCallSummary(toolCallSummary);
  const allCommands = summaryCommands.length > 0 ? summaryCommands : traceCommands;
  const commands = selectAgentTraceCommands(allCommands);
  const searchable = [
    text(traceBody?.assistantText ?? body?.assistantText),
    ...[...traceCommands, ...summaryCommands].map((command) => [command.command, command.bodyPreview].map((item) => text(item)).filter(Boolean).join("\n"))
  ].filter(Boolean).join("\n");
  const hwpodCommandCount = allCommands.filter((command) => mentionsHwpod(command)).length;
  const hwpodBuildCommandCount = allCommands.filter((command) => mentionsHwpodBuild(command)).length;
  return clean({
    traceId,
    status: text(traceBody?.status ?? body?.status) || "recorded",
    httpStatus,
    source: text(traceBody?.source ?? body?.source),
    lookupOnly: traceBody?.lookupOnly === true || body?.lookupOnly === true ? true : undefined,
    lookup: compactObject(traceBody?.lookup ?? body?.lookup ?? null),
    sourceEventCount: numberOption(traceBody?.sourceEventCount ?? traceBody?.traceSummary?.sourceEventCount ?? body?.sourceEventCount),
    renderedRowCount: rows.length || numberOption(traceBody?.renderedRowCount),
    commandCount: allCommands.length,
    hwpodCommandCount,
    hwpodBuildCommandCount,
    keilJobCandidates: uniqueStrings(Array.from(searchable.matchAll(/20\d{6}_\d{6}_[0-9a-f]{8}/giu)).map((match) => match[0])),
    terminalStatus: firstTextOption(traceBody?.terminalStatus, traceBody?.traceSummary?.terminalStatus, body?.terminalStatus, toolCallSummary?.terminalStatus),
    agentRun: compactObject(traceBody?.agentRun ?? traceBody?.traceSummary?.agentRun ?? body?.agentRun ?? traceBody?.terminalEvidence?.agentRun ?? null),
    toolCallSummary,
    commands
  });
}

function commandsFromTraceRows(rows: Record<string, unknown>[]) {
  return rows.flatMap((row, index) => {
    const header = text(row.header ?? row.title ?? row.label);
    const body = text(row.body ?? row.content ?? row.text ?? row.message);
    if (!isCommandTraceRow(row, header)) return [];
    const command = text((row as any).command) || commandFromText(`${header}\n${body}`) || commandFromRenderedToolBody(body);
    if (!command) return [];
    return [clean({
      source: "trace-row",
      seq: numberOption(row.seq) ?? index + 1,
      rowId: text(row.id ?? row.rowId),
      header: clipText(header, 500),
      toolName: text(row.toolName ?? row.name),
      status: text(row.status ?? row.level),
      command: clipText(command, 1000),
      normalizedCommand: clipText(normalizedHwpodCommand(command), 1000),
      bodyPreview: clipText(body, 1200),
      exitCode: exitCodeFromText(body)
    })];
  });
}

function commandsFromTraceEvents(events: Record<string, unknown>[]) {
  return events.flatMap((event, index) => {
    const toolName = text(event.toolName ?? event.name ?? event.label);
    const message = text(event.message ?? event.text ?? event.content ?? event.output ?? event.stdout);
    if (!isCommandTraceEvent(event, toolName)) return [];
    const command = text(event.command ?? event.input ?? event.args) || commandFromExecutionText(message) || commandFromText(message) || commandFromRenderedToolBody(message);
    if (!command) return [];
    return [clean({
      source: "trace-event",
      seq: numberOption(event.seq ?? event.index) ?? index + 1,
      rowId: text(event.id ?? event.eventId),
      toolName,
      status: text(event.status ?? event.level),
      command: clipText(command, 1000),
      normalizedCommand: clipText(normalizedHwpodCommand(command), 1000),
      bodyPreview: clipText(message, 1200),
      exitCode: numberOption(event.exitCode) ?? exitCodeFromText(message)
    })];
  });
}

function isCommandTraceRow(row: Record<string, unknown>, header: string) {
  return /^tool:/u.test(text(row.rowId ?? row.id)) || /commandExecution|tool_call/iu.test(header) || /commandExecution|tool_call/iu.test(text(row.toolName ?? row.name));
}

function isCommandTraceEvent(event: Record<string, unknown>, toolName: string) {
  const type = text(event.type ?? event.kind ?? event.event);
  return /commandExecution/iu.test(toolName) || /commandExecution|tool_call/iu.test(type);
}

function selectAgentTraceCommands(commands: AgentTraceCommand[]) {
  const selected: AgentTraceCommand[] = [];
  for (const command of [
    ...selectRepresentativeHwpodCommands(commands),
    ...commands.slice(0, 10),
    ...commands.slice(-10),
    ...commands.filter((item) => mentionsHwpod(item))
  ]) {
    const key = `${command.source}:${command.seq ?? ""}:${command.rowId ?? ""}:${command.command ?? ""}:${command.status ?? ""}`;
    if (selected.some((item) => `${item.source}:${item.seq ?? ""}:${item.rowId ?? ""}:${item.command ?? ""}:${item.status ?? ""}` === key)) continue;
    selected.push(command);
    if (selected.length >= MAX_AGENT_TRACE_COMMANDS) break;
  }
  return selected;
}

function selectRepresentativeHwpodCommands(commands: AgentTraceCommand[]) {
  return ["spec-validate", "inspect", "workspace-edit", "build", "download", "uart-read"].flatMap((kind) => {
    const candidates = commands.filter((command) => hwpodCommandKind(command) === kind);
    return candidates.find((command) => text(command.status) === "completed" || command.exitCode === 0) ?? candidates[0] ?? [];
  });
}

export function arrayOfObjects(value: unknown): Record<string, unknown>[] {
  return Array.isArray(value) ? value.filter((item) => item && typeof item === "object" && !Array.isArray(item)) as Record<string, unknown>[] : [];
}

function commandFromText(value: string) {
  const match = value.match(/(?:command|cmd)\s*[:=]\s*([^\n]+)/iu);
  return text(match?.[1]);
}

function commandFromRenderedToolBody(value: string) {
  const raw = text(value);
  if (!raw) return "";
  const match = raw.match(/^(.+?)(?:\s+stdout:|\s+stderr:|\s+exitCode=|\s+exit\s*code\b)/isu);
  return text(match?.[1] ?? raw);
}

function commandFromExecutionText(value: string) {
  const raw = text(value);
  const match = raw.match(/commandExecution\s+(?:inProgress|completed|started|failed):\s+(.+?)(?:\s+exit=|\s+durationMs=|\s*$)/isu);
  return text(match?.[1]);
}

function exitCodeFromText(value: string) {
  const match = value.match(/exit\s*code\s*[:=]?\s*(-?\d+)/iu) ?? value.match(/exitCode\s*[:=]?\s*(-?\d+)/u);
  return numberOption(match?.[1]);
}

function mentionsHwpodBuild(command: AgentTraceCommand) {
  const raw = normalizedHwpodCommand(command.normalizedCommand || command.command || "");
  return /^(?:hwpod|hwpod-cli)\s+build(?:\s|$)/iu.test(raw) || /^(?:bun\s+)?tools\/hwpod-cli\.ts\s+build(?:\s|$)/iu.test(raw);
}

function normalizedHwpodCommand(value: string) {
  return unquoteShellCommand(text(value)).replace(/\s+/gu, " ").trim();
}

function unquoteShellCommand(value: string) {
  return value.replace(/^\/bin\/sh\s+-lc\s+/u, "").replace(/^['"]|['"]$/gu, "").trim();
}

export function uniqueStrings(values: string[]) {
  return Array.from(new Set(values.map((value) => text(value)).filter(Boolean)));
}
