// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first.
// Responsibility: Workbench typed error, diagnostic normalization, projection blockers and readable server errors.
// Mechanical source references:
// - OpenCode utils/server-errors.ts:1-17 named error shapes.
// - OpenCode utils/server-errors.ts:28-35 formatServerError.
// - OpenCode utils/server-errors.ts:38-55 unwrap/type guards.
// - OpenCode utils/server-errors.ts:57-88 readable config/provider errors.
// - OpenCode stream.transport.ts:250-271 formatUnknownError.
// - OpenCode session-data.ts:163-183 formatError boundary.

import type { ApiError, ChatMessage, ErrorDiagnostic, ProjectionBlocker, ProjectionDiagnostic } from "@/types";

export type ConfigInvalidError = {
  name: "ConfigInvalidError";
  data: { path?: string; message?: string; issues?: Array<{ message: string; path: string[] }> };
};

export type ProviderModelNotFoundError = {
  name: "ProviderModelNotFoundError";
  data: { providerID: string; modelID: string; suggestions?: string[] };
};

type Translator = (key: string, vars?: Record<string, string | number>) => string;

export function formatServerError(error: unknown, translate?: Translator, fallback?: string): string {
  const unwrapped = unwrapNamedError(error);
  if (isConfigInvalidErrorLike(unwrapped)) return parseReadableConfigInvalidError(unwrapped, translate);
  if (isProviderModelNotFoundErrorLike(unwrapped)) return parseReadableProviderModelNotFoundError(unwrapped, translate);
  if (error instanceof Error && error.message) return error.message;
  if (typeof error === "string" && error) return error;
  if (fallback) return fallback;
  return tr(translate, "error.chain.unknown", "Unknown error");
}

export function normalizeErrorDiagnostic(...values: unknown[]): ErrorDiagnostic | null {
  for (const value of values) {
    const record = recordValue(value);
    if (!record) continue;
    const httpStatus = firstFiniteNumber(record.httpStatus, record.http_status);
    return {
      ...record,
      contractVersion: firstNonEmptyString(record.contractVersion, record.contract_version, "hwlab-error-diagnostic-v1"),
      traceId: firstNonEmptyString(record.traceId, record.trace_id, record.otelTraceId, record.otel_trace_id),
      requestId: firstNonEmptyString(record.requestId, record.request_id),
      serviceId: firstNonEmptyString(record.serviceId, record.service_id),
      route: firstNonEmptyString(record.route),
      layer: firstNonEmptyString(record.layer),
      category: firstNonEmptyString(record.category),
      code: firstStringOrNumber(record.code),
      httpStatus: httpStatus ?? null,
      source: firstNonEmptyString(record.source),
      observedAt: firstNonEmptyString(record.observedAt, record.observed_at),
      retryable: firstBoolean(record.retryable),
      valuesPrinted: record.valuesPrinted === true
    };
  }
  return null;
}

export function normalizeApiErrorRecord(value: unknown, fallback: string | null = null, diagnostic: ErrorDiagnostic | null = null): ApiError | null {
  const record = recordValue(value);
  if (!record) return null;
  const normalizedDiagnostic = normalizeErrorDiagnostic(record.diagnostic, diagnostic);
  const message = firstNonEmptyString(record.message, record.userMessage, record.reason, fallback);
  return {
    ...record,
    message: message ?? fallback ?? "Workbench 请求失败",
    userMessage: firstNonEmptyString(record.userMessage, record.message, fallback),
    code: firstStringOrNumber(record.code, normalizedDiagnostic?.code),
    reason: firstNonEmptyString(record.reason),
    retryable: firstBoolean(record.retryable, normalizedDiagnostic?.retryable),
    layer: firstNonEmptyString(record.layer, normalizedDiagnostic?.layer),
    category: firstNonEmptyString(record.category, normalizedDiagnostic?.category),
    route: firstNonEmptyString(record.route, normalizedDiagnostic?.route),
    traceId: firstNonEmptyString(record.traceId, normalizedDiagnostic?.traceId),
    requestId: firstNonEmptyString(record.requestId, normalizedDiagnostic?.requestId),
    source: firstNonEmptyString(record.source, normalizedDiagnostic?.source),
    diagnostic: normalizedDiagnostic,
    valuesPrinted: record.valuesPrinted === true
  } as ApiError;
}

export function normalizeProjectionDiagnostic(value: unknown): ProjectionDiagnostic | null {
  const record = recordValue(value);
  if (!record) return null;
  const nested = recordValue(record.projection);
  const source = nested ?? record;
  const rawBlocker = recordValue(source.blocker ?? record.blocker);
  const apiError = normalizeApiErrorRecord(source.apiError ?? record.apiError, null);
  const diagnostic = normalizeErrorDiagnostic(source.diagnostic, record.diagnostic, rawBlocker?.diagnostic, apiError?.diagnostic);
  const blocker = normalizeProjectionBlocker(rawBlocker, { diagnostic, apiError });
  const projectionStatus = firstNonEmptyString(source.projectionStatus, record.projectionStatus);
  const projectionHealth = firstNonEmptyString(source.projectionHealth, record.projectionHealth);
  const lastProjectedSeq = firstFiniteNumber(source.lastProjectedSeq, record.lastProjectedSeq);
  const staleMs = firstFiniteNumber(source.staleMs, record.staleMs);
  const sourceRunId = firstNonEmptyString(source.sourceRunId, record.sourceRunId);
  const sourceCommandId = firstNonEmptyString(source.sourceCommandId, record.sourceCommandId);
  if (!projectionStatus && !projectionHealth && !blocker && lastProjectedSeq == null && staleMs == null && !sourceRunId && !sourceCommandId) return null;
  return { ...source, projectionStatus, projectionHealth, lastProjectedSeq: lastProjectedSeq ?? null, staleMs: staleMs ?? null, sourceRunId: sourceRunId ?? null, sourceCommandId: sourceCommandId ?? null, blocker, diagnostic, apiError, valuesRedacted: source.valuesRedacted !== false } as ProjectionDiagnostic;
}

export function projectionDiagnosticFromApiFailure(result: { status: number; error?: string | null; apiError?: ApiError | null; diagnostic?: ErrorDiagnostic | null }, input: { code: string; message: string; health: string }): ProjectionDiagnostic {
  return projectionDiagnosticFromFailure({ ...input, status: result.status, apiError: result.apiError, diagnostic: result.diagnostic });
}

export function projectionDiagnosticFromFailure(input: { code: string; message: string; health: string; status?: number | null; apiError?: ApiError | null; diagnostic?: ErrorDiagnostic | null; route?: string | null; source?: string | null }): ProjectionDiagnostic {
  const apiError = input.apiError ?? null;
  const diagnostic = normalizeErrorDiagnostic(input.diagnostic, apiError?.diagnostic, {
    contractVersion: "hwlab-error-diagnostic-v1",
    traceId: apiError?.traceId ?? null,
    requestId: apiError?.requestId ?? null,
    route: firstNonEmptyString(apiError?.route, input.route),
    layer: apiError?.layer ?? "workbench",
    category: apiError?.category ?? "workbench-projection",
    code: firstStringOrNumber(apiError?.code, input.code),
    httpStatus: input.status ?? null,
    source: firstNonEmptyString(apiError?.source, input.source, "browser"),
    retryable: apiError?.retryable ?? true,
    valuesPrinted: false
  });
  const message = firstNonEmptyString(apiError?.userMessage, apiError?.message, input.message) ?? input.message;
  const blocker = normalizeProjectionBlocker({
    code: firstStringOrNumber(apiError?.code, diagnostic?.code, input.code),
    message,
    userMessage: firstNonEmptyString(apiError?.userMessage, message),
    layer: firstNonEmptyString(apiError?.layer, diagnostic?.layer, "workbench"),
    category: firstNonEmptyString(apiError?.category, diagnostic?.category, "workbench-projection"),
    retryable: firstBoolean(apiError?.retryable, diagnostic?.retryable, true),
    route: firstNonEmptyString(apiError?.route, diagnostic?.route, input.route),
    traceId: firstNonEmptyString(apiError?.traceId, diagnostic?.traceId),
    requestId: firstNonEmptyString(apiError?.requestId, diagnostic?.requestId),
    source: firstNonEmptyString(apiError?.source, diagnostic?.source, input.source),
    httpStatus: firstFiniteNumber(diagnostic?.httpStatus, input.status),
    diagnostic,
    valuesPrinted: false
  }, { diagnostic, apiError, fallbackCode: input.code, fallbackMessage: message });
  return { projectionStatus: "blocked", projectionHealth: input.health, blocker, diagnostic, apiError, valuesRedacted: true };
}

export function normalizeProjectionBlocker(value: unknown, context: { diagnostic?: ErrorDiagnostic | null; apiError?: ApiError | null; fallbackCode?: string | null; fallbackMessage?: string | null } = {}): ProjectionBlocker | null {
  const record = recordValue(value);
  if (!record && !context.diagnostic && !context.apiError && !context.fallbackMessage && !context.fallbackCode) return null;
  const source = record ?? {};
  const diagnostic = context.diagnostic ?? normalizeErrorDiagnostic(source.diagnostic, context.apiError?.diagnostic);
  const code = firstStringOrNumber(source.code, context.apiError?.code, diagnostic?.code, context.fallbackCode);
  const message = firstNonEmptyString(source.message, source.userMessage, source.summary, context.apiError?.userMessage, context.apiError?.message, context.fallbackMessage);
  const retryable = firstBoolean(source.retryable, context.apiError?.retryable, diagnostic?.retryable);
  return {
    ...source,
    code: code === null ? undefined : String(code),
    message: message ?? null,
    userMessage: firstNonEmptyString(source.userMessage, context.apiError?.userMessage, message),
    summary: firstNonEmptyString(source.summary),
    layer: firstNonEmptyString(source.layer, context.apiError?.layer, diagnostic?.layer),
    category: firstNonEmptyString(source.category, context.apiError?.category, diagnostic?.category, "workbench-projection"),
    retryable: retryable ?? true,
    route: firstNonEmptyString(source.route, context.apiError?.route, diagnostic?.route),
    traceId: firstNonEmptyString(source.traceId, context.apiError?.traceId, diagnostic?.traceId),
    requestId: firstNonEmptyString(source.requestId, context.apiError?.requestId, diagnostic?.requestId),
    source: firstNonEmptyString(source.source, context.apiError?.source, diagnostic?.source),
    httpStatus: firstFiniteNumber(source.httpStatus, source.http_status, diagnostic?.httpStatus) ?? null,
    diagnostic,
    valuesPrinted: source.valuesPrinted === true
  } as ProjectionBlocker;
}

export function agentErrorFromProjection(projection: ProjectionDiagnostic): ChatMessage["error"] | null {
  const blocker = projection.blocker ?? null;
  const diagnostic = projection.diagnostic ?? blocker?.diagnostic ?? projection.apiError?.diagnostic ?? null;
  const message = firstNonEmptyString(blocker?.userMessage, blocker?.message, blocker?.summary, projection.apiError?.userMessage, projection.apiError?.message, diagnostic?.code ? String(diagnostic.code) : null);
  if (!blocker && !projection.apiError && !diagnostic && !message) return null;
  return normalizeApiErrorRecord({ ...(projection.apiError ?? {}), ...(blocker ?? {}), message, code: firstStringOrNumber(blocker?.code, projection.apiError?.code, diagnostic?.code), diagnostic, traceId: firstNonEmptyString(blocker?.traceId, projection.apiError?.traceId, diagnostic?.traceId), requestId: firstNonEmptyString(blocker?.requestId, projection.apiError?.requestId, diagnostic?.requestId) }, message) as ChatMessage["error"];
}

export interface WorkbenchMessageDiagnosticView {
  visible: boolean;
  showMessage: boolean;
  text: string | null;
  apiError: ApiError | null;
  diagnostic: ErrorDiagnostic | null;
}

export function messageDiagnosticView(message: ChatMessage): WorkbenchMessageDiagnosticView {
  if (hasSealedCompletedText(message)) return { visible: false, showMessage: false, text: null, apiError: null, diagnostic: null };
  const text = messageDiagnosticText(message);
  const diagnostic = messageErrorDiagnostic(message, text);
  const apiError = messageApiError(message, text, diagnostic);
  return { visible: Boolean(text || apiError || diagnostic), showMessage: !messageOwnsPrimaryFailurePresentation(message), text, apiError, diagnostic };
}

export function messageOwnsPrimaryFailurePresentation(message: ChatMessage): boolean {
  if (message.role !== "agent" || !isTerminalAgentMessage(message) || !visibleMessageText(message)) return false;
  const error = recordValue(message.error);
  const blocker = recordValue(message.projection?.blocker ?? message.blocker ?? message.runnerTrace?.blocker);
  const primaryPresentation = firstNonEmptyString(error?.primaryPresentation, blocker?.primaryPresentation);
  const failureIdentity = firstNonEmptyString(error?.failureIdentity, blocker?.failureIdentity, (message as Record<string, unknown>).failureIdentity);
  return primaryPresentation === "assistant-message" && Boolean(failureIdentity);
}

function messageDiagnosticText(message: ChatMessage): string | null {
  if (message.role === "agent") return projectionDiagnosticText(message);
  if (message.role !== "user" || String(message.text ?? "").trim()) return null;
  return "消息内容缺失：Workbench read model 未返回 user text。";
}

function projectionDiagnosticText(message: ChatMessage): string | null {
  if (hasSealedCompletedText(message)) return null;
  const projection = messageProjection(message);
  const health = projection?.projectionHealth ?? message.projectionHealth ?? message.runnerTrace?.projectionHealth;
  if (!projection || health === "caught-up") return null;
  return firstNonEmptyString(projection.blocker?.userMessage, projection.blocker?.message, projection.blocker?.summary, projection.blocker?.code ? `状态更新异常：${projection.blocker.code}` : null);
}

function messageProjection(message: ChatMessage): ChatMessage["projection"] {
  return message.projection ?? message.runnerTrace?.projection ?? null;
}

function messageApiError(message: ChatMessage, diagnosticText: string | null, diagnostic: ErrorDiagnostic | null): ApiError | null {
  const projection = messageProjection(message);
  const blocker = recordValue(projection?.blocker ?? message.blocker ?? message.runnerTrace?.blocker);
  const error = recordValue(message.error);
  const apiError = recordValue(projection?.apiError ?? error?.apiError);
  if (!diagnosticText && !blocker && !error && !apiError && !diagnostic) return null;
  const source = apiError ?? error ?? blocker ?? {};
  const messageText = firstNonEmptyString(source.userMessage, source.message, blocker?.userMessage, blocker?.message, blocker?.summary, diagnostic?.code ? String(diagnostic.code) : null, diagnosticText);
  return {
    ...source,
    message: messageText ?? "Workbench 诊断",
    userMessage: firstNonEmptyString(source.userMessage, blocker?.userMessage, messageText),
    code: firstStringOrNumber(source.code, blocker?.code, diagnostic?.code),
    retryable: firstBoolean(source.retryable, blocker?.retryable, diagnostic?.retryable),
    layer: firstNonEmptyString(source.layer, blocker?.layer, diagnostic?.layer),
    category: firstNonEmptyString(source.category, blocker?.category, diagnostic?.category),
    route: firstNonEmptyString(source.route, blocker?.route, diagnostic?.route),
    traceId: firstNonEmptyString(source.traceId, blocker?.traceId, diagnostic?.traceId, message.traceId, message.runnerTrace?.traceId),
    requestId: firstNonEmptyString(source.requestId, blocker?.requestId, diagnostic?.requestId),
    source: firstNonEmptyString(source.source, blocker?.source, diagnostic?.source),
    diagnostic,
    valuesPrinted: source.valuesPrinted === true || blocker?.valuesPrinted === true
  } as ApiError;
}

function messageErrorDiagnostic(message: ChatMessage, diagnosticText: string | null): ErrorDiagnostic | null {
  const projection = messageProjection(message);
  const blocker = recordValue(projection?.blocker ?? message.blocker ?? message.runnerTrace?.blocker);
  const error = recordValue(message.error);
  const apiError = recordValue(projection?.apiError ?? error?.apiError);
  if (!diagnosticText && !blocker && !error && !apiError && !projection?.diagnostic) return null;
  const nested = normalizeErrorDiagnostic(projection?.diagnostic, blocker?.diagnostic, error?.diagnostic, apiError?.diagnostic);
  if (nested) return nested;
  const code = firstStringOrNumber(error?.code, apiError?.code, blocker?.code);
  const traceId = firstNonEmptyString(error?.traceId, apiError?.traceId, blocker?.traceId, message.traceId, message.runnerTrace?.traceId);
  const requestId = firstNonEmptyString(error?.requestId, apiError?.requestId, blocker?.requestId);
  const route = firstNonEmptyString(error?.route, apiError?.route, blocker?.route);
  const layer = firstNonEmptyString(error?.layer, apiError?.layer, blocker?.layer);
  const category = firstNonEmptyString(error?.category, apiError?.category, blocker?.category);
  const source = firstNonEmptyString(error?.source, apiError?.source, blocker?.source);
  const httpStatus = firstNumber(error?.httpStatus, apiError?.httpStatus, blocker?.httpStatus, error?.providerStatus);
  if (!code && !traceId && !requestId && !route && !layer && !category && !source && httpStatus === null) return null;
  return { contractVersion: "hwlab-error-diagnostic-v1", traceId, requestId, route, layer, category, code, httpStatus, source, retryable: firstBoolean(error?.retryable, apiError?.retryable, blocker?.retryable), valuesPrinted: false };
}

function hasSealedCompletedText(message: ChatMessage): boolean {
  return isCompletedAgentMessage(message) && Boolean(visibleMessageText(message));
}

function isCompletedAgentMessage(message: ChatMessage): boolean {
  if (message.role !== "agent") return false;
  return String(message.status ?? "").trim().toLowerCase().replace(/_/gu, "-") === "completed";
}

function visibleMessageText(message: ChatMessage): string {
  const text = String(message.text ?? "").trim();
  if (message.role === "agent" && !isTerminalAgentMessage(message)) return "";
  return text;
}

function isTerminalAgentMessage(message: ChatMessage): boolean {
  if (message.role !== "agent") return false;
  return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(message.status ?? "").trim().toLowerCase().replace(/_/gu, "-"));
}

function tr(translator: Translator | undefined, key: string, text: string, vars?: Record<string, string | number>): string {
  if (!translator) return text;
  const out = translator(key, vars);
  if (!out || out === key) return text;
  return out;
}

function unwrapNamedError(error: unknown): unknown {
  if (error instanceof Error && error.cause && typeof error.cause === "object" && "body" in error.cause) return (error.cause as Record<string, unknown>).body;
  return error;
}

function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
  const record = recordValue(error);
  return record?.name === "ConfigInvalidError" && typeof record.data === "object" && record.data !== null;
}

function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError {
  const record = recordValue(error);
  return record?.name === "ProviderModelNotFoundError" && typeof record.data === "object" && record.data !== null;
}

export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator): string {
  const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config";
  const detail = errorInput.data.message?.trim() ?? "";
  const issues = (errorInput.data.issues ?? []).map((issue) => {
    const msg = issue.message.trim();
    if (!issue.path.length) return msg;
    return `${issue.path.join(".")}: ${msg}`;
  }).filter(Boolean);
  const msg = issues.length ? issues.join("\n") : detail;
  if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file });
  return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, { path: file, message: msg });
}

function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator): string {
  const provider = errorInput.data.providerID.trim();
  const model = errorInput.data.modelID.trim();
  const list = (errorInput.data.suggestions ?? []).map((value) => value.trim()).filter(Boolean);
  const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${provider}/${model}`, { provider, model });
  const tail = tr(translator, "error.chain.checkConfig", "Check your config (opencode.json) provider/model names");
  if (list.length) {
    const suggestions = list.slice(0, 5).join(", ");
    return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join("\n");
  }
  return [body, tail].join("\n");
}

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

function firstNonEmptyString(...values: unknown[]): string | null {
  for (const value of values) {
    if (typeof value !== "string") continue;
    const text = value.trim();
    if (text) return text;
  }
  return null;
}

function firstStringOrNumber(...values: unknown[]): string | number | null {
  for (const value of values) {
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim()) return value.trim();
  }
  return null;
}

function firstFiniteNumber(...values: unknown[]): number | null {
  for (const value of values) {
    const number = Number(value);
    if (Number.isFinite(number)) return Math.trunc(number);
  }
  return null;
}

function firstNumber(...values: unknown[]): number | null {
  for (const value of values) {
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
  }
  return null;
}

function firstBoolean(...values: unknown[]): boolean | null {
  for (const value of values) if (typeof value === "boolean") return value;
  return null;
}
