// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection; PJ2026-010403 API契约 draft-2026-06-18-r1; PJ2026-010401 Web工作台 draft-2026-06-18-r1.
// Responsibility: Workbench read-model HTTP client for session, message, turn and trace projection APIs.

import { fetchJson, type ApiRequestOptions } from "./client";
import type { AgentChatResultResponse, ApiResult, ChatMessage, ProjectionDiagnostic, WorkbenchSessionRecord, WorkbenchTurnTimingProjection } from "@/types";

export interface WorkbenchSessionListResponse {
  sessions?: WorkbenchSessionRecord[];
  count?: number;
  total?: number;
  cursor?: string | null;
  hasMore?: boolean;
  nextCursor?: string | null;
}

export interface WorkbenchSessionDetailResponse {
  session?: WorkbenchSessionRecord;
}

export interface WorkbenchMessagePageResponse {
  sessionId?: string | null;
  messages?: ChatMessage[];
  count?: number;
  total?: number;
  hasMore?: boolean;
}

export interface SessionListOptions {
  includeSessionId?: string | null;
  limit?: number | null;
  cursor?: string | null;
  timeoutMs?: number | null;
}

export interface SessionMessageOptions {
  limit?: number | null;
  cursor?: string | null;
  timeoutMs?: number | null;
}

export interface WorkbenchTraceRequestOptions {
  afterProjectedSeq?: number | null;
  limit?: number | null;
}

const SESSION_LIST_TIMEOUT_MS = 30_000;
const SESSION_DETAIL_TIMEOUT_MS = 45_000;

export const workbenchAPI = {
  sessions: (options: SessionListOptions = {}): Promise<ApiResult<WorkbenchSessionListResponse>> => fetchJson(sessionListPath(options), { timeoutMs: requestTimeoutMs(options.timeoutMs, SESSION_LIST_TIMEOUT_MS), timeoutName: "workbench sessions" }),
  session: (sessionId: string, timeoutMs: number | null = null): Promise<ApiResult<WorkbenchSessionDetailResponse>> => fetchJson(`/v1/workbench/sessions/${encodeURIComponent(sessionId)}`, { timeoutMs: requestTimeoutMs(timeoutMs, SESSION_DETAIL_TIMEOUT_MS), timeoutName: "workbench session" }),
  sessionMessages: (sessionId: string, options: SessionMessageOptions = {}): Promise<ApiResult<WorkbenchMessagePageResponse>> => fetchJson(sessionMessagesPath(sessionId, options), { timeoutMs: requestTimeoutMs(options.timeoutMs, SESSION_DETAIL_TIMEOUT_MS), timeoutName: "workbench session messages" }),
  turn: async (traceId: string, timeoutMs = 8000, activityRef?: ApiRequestOptions["activityRef"]): Promise<ApiResult<AgentChatResultResponse>> => normalizeWorkbenchTurnResult(await fetchJson<Record<string, unknown>>(`/v1/workbench/turns/${encodeURIComponent(traceId)}`, { timeoutMs, timeoutName: "workbench turn", activityRef }), traceId),
  traceEvents: async (traceId: string, timeoutMs = 8000, activityRef?: ApiRequestOptions["activityRef"], options?: WorkbenchTraceRequestOptions): Promise<ApiResult<AgentChatResultResponse>> => normalizeWorkbenchTraceResult(await fetchJson<Record<string, unknown>>(workbenchTracePath(traceId, options), { timeoutMs, timeoutName: "workbench trace", activityRef }), traceId)
};

function sessionListPath(options: SessionListOptions): string {
  const params = new URLSearchParams();
  const includeSessionId = typeof options.includeSessionId === "string" ? options.includeSessionId.trim() : "";
  if (includeSessionId) params.set("includeSessionId", includeSessionId);
  if (Number.isFinite(options.limit)) params.set("limit", String(Math.max(1, Math.trunc(Number(options.limit)))));
  const cursor = typeof options.cursor === "string" ? options.cursor.trim() : "";
  if (cursor) params.set("cursor", cursor);
  const query = params.toString();
  return `/v1/workbench/sessions${query ? `?${query}` : ""}`;
}

function sessionMessagesPath(sessionId: string, options: SessionMessageOptions): string {
  const params = new URLSearchParams();
  if (Number.isFinite(options.limit)) params.set("limit", String(Math.max(1, Math.trunc(Number(options.limit)))));
  const cursor = typeof options.cursor === "string" ? options.cursor.trim() : "";
  if (cursor) params.set("cursor", cursor);
  const query = params.toString();
  return `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages${query ? `?${query}` : ""}`;
}

function requestTimeoutMs(value: number | null | undefined, fallback: number): number {
  const timeout = Number(value);
  return Number.isFinite(timeout) && timeout > 0 ? Math.trunc(timeout) : fallback;
}

function workbenchTracePath(traceId: string, options: WorkbenchTraceRequestOptions = {}): string {
  const params = new URLSearchParams();
  if (Number.isFinite(options.afterProjectedSeq)) params.set("afterProjectedSeq", String(Math.max(0, Math.trunc(Number(options.afterProjectedSeq)))));
  if (Number.isFinite(options.limit)) params.set("limit", String(Math.max(1, Math.trunc(Number(options.limit)))));
  const query = params.toString();
  return `/v1/workbench/traces/${encodeURIComponent(traceId)}/events${query ? `?${query}` : ""}`;
}

function normalizeWorkbenchTurnResult(result: ApiResult<Record<string, unknown>>, traceId: string): ApiResult<AgentChatResultResponse> {
  if (!result.ok || !result.data) return result as ApiResult<AgentChatResultResponse>;
  const turn = recordValue(result.data.turn) ?? result.data;
  const trace = recordValue(turn.trace);
  const status = normalizeStatus(textValue(turn.status)) ?? "unknown";
  const projection = normalizeProjectionDiagnostic(result.data.projection, turn.projection, result.data);
  const timing = normalizeTimingProjection(turn);
  return {
    ...result,
    data: {
      ...turn,
      projection,
      timing,
      startedAt: timing?.startedAt ?? undefined,
      lastEventAt: timing?.lastEventAt ?? undefined,
      finishedAt: timing?.finishedAt ?? undefined,
      durationMs: timing?.durationMs ?? undefined,
      projectionStatus: projection?.projectionStatus ?? textValue(result.data.projectionStatus) ?? undefined,
      projectionHealth: projection?.projectionHealth ?? textValue(result.data.projectionHealth) ?? undefined,
      lastProjectedSeq: Number.isFinite(Number(result.data.lastProjectedSeq)) ? Number(result.data.lastProjectedSeq) : undefined,
      sourceRunId: projection?.sourceRunId ?? textValue(result.data.sourceRunId) ?? undefined,
      sourceCommandId: projection?.sourceCommandId ?? textValue(result.data.sourceCommandId) ?? undefined,
      staleMs: projection?.staleMs ?? numberValue(result.data.staleMs) ?? undefined,
      blocker: projection?.blocker ?? recordValue(result.data.blocker) ?? undefined,
      traceId: textValue(turn.traceId) ?? traceId,
      status,
      running: typeof turn.running === "boolean" ? turn.running : undefined,
      terminal: typeof turn.terminal === "boolean" ? turn.terminal : undefined,
      sessionId: textValue(turn.sessionId) ?? undefined,
      threadId: textValue(turn.threadId) ?? undefined,
      agentRun: recordValue(turn.agentRun) ?? undefined,
      updatedAt: textValue(turn.updatedAt) ?? textValue(result.data.observedAt) ?? undefined
    } as AgentChatResultResponse
  };
}

function normalizeWorkbenchTraceResult(result: ApiResult<Record<string, unknown>>, traceId: string): ApiResult<AgentChatResultResponse> {
  if (!result.ok || !result.data) return result as ApiResult<AgentChatResultResponse>;
  const events = Array.isArray(result.data.events) ? result.data.events : [];
  const nextProjectedSeq = Number(result.data.nextProjectedSeq);
  const traceStatus = textValue(result.data.traceStatus) ?? undefined;
  const eventCount = Number.isFinite(Number(result.data.eventCount)) ? Number(result.data.eventCount) : events.length;
  const normalizedNextProjectedSeq = Number.isFinite(nextProjectedSeq) ? Math.trunc(nextProjectedSeq) : null;
  const hasMore = result.data.hasMore === true;
  const fullTraceLoaded = typeof result.data.fullTraceLoaded === "boolean" ? result.data.fullTraceLoaded : undefined;
  const projection = normalizeProjectionDiagnostic(result.data.projection, result.data);
  const timing = normalizeTimingProjection(result.data);
  return {
    ...result,
    data: {
      ...result.data,
      traceId: textValue(result.data.traceId) ?? traceId,
      status: undefined,
      traceStatus,
      timing,
      startedAt: timing?.startedAt ?? undefined,
      lastEventAt: timing?.lastEventAt ?? undefined,
      finishedAt: timing?.finishedAt ?? undefined,
      durationMs: timing?.durationMs ?? undefined,
      events,
      traceEvents: events,
      eventCount,
      hasMore,
      fullTraceLoaded,
      projection,
      projectionStatus: projection?.projectionStatus ?? textValue(result.data.projectionStatus) ?? undefined,
      projectionHealth: projection?.projectionHealth ?? textValue(result.data.projectionHealth) ?? undefined,
      staleMs: projection?.staleMs ?? numberValue(result.data.staleMs) ?? undefined,
      blocker: projection?.blocker ?? recordValue(result.data.blocker) ?? undefined,
      nextProjectedSeq: normalizedNextProjectedSeq,
      range: recordValue(result.data.range) ?? undefined,
      runnerTrace: {
        traceId: textValue(result.data.traceId) ?? traceId,
        status: undefined,
        timing,
        startedAt: timing?.startedAt ?? undefined,
        lastEventAt: timing?.lastEventAt ?? undefined,
        finishedAt: timing?.finishedAt ?? undefined,
        durationMs: timing?.durationMs ?? undefined,
        events,
        eventCount,
        hasMore,
        fullTraceLoaded,
        nextProjectedSeq: normalizedNextProjectedSeq,
        range: recordValue(result.data.range) ?? undefined,
        projection,
        projectionStatus: projection?.projectionStatus ?? textValue(result.data.projectionStatus) ?? undefined,
        projectionHealth: projection?.projectionHealth ?? textValue(result.data.projectionHealth) ?? undefined,
        staleMs: projection?.staleMs ?? numberValue(result.data.staleMs) ?? undefined,
        blocker: projection?.blocker ?? recordValue(result.data.blocker) ?? undefined,
        eventSource: "trace-api"
      }
    } as AgentChatResultResponse
  };
}

function normalizeTimingProjection(value: unknown): WorkbenchTurnTimingProjection | undefined {
  const record = recordValue(value);
  const source = recordValue(record?.timing);
  if (!source) return undefined;
  const startedAt = textValue(source.startedAt);
  const lastEventAt = textValue(source.lastEventAt);
  const finishedAt = textValue(source.finishedAt);
  const durationMs = numberValue(source.durationMs);
  // observedAt and lastEventAgeMs are server-side diagnostic snapshots.
  // They must NOT be used as the user-visible relative time authority.
  // Running relative time display must use startedAt/lastEventAt + browser local now().
  const observedAt = textValue(source.observedAt);
  const lastEventAgeMs = numberValue(source.lastEventAgeMs);
  if (!startedAt && !lastEventAt && !finishedAt && durationMs == null && lastEventAgeMs == null) return undefined;
  return {
    ...source,
    startedAt: startedAt ?? null,
    lastEventAt: lastEventAt ?? null,
    finishedAt: finishedAt ?? null,
    durationMs,
    observedAt: observedAt ?? null,
    lastEventAgeMs: lastEventAgeMs ?? null,
    valuesRedacted: source.valuesRedacted !== false
  } as WorkbenchTurnTimingProjection;
}

function normalizeStatus(value: unknown): string | null {
  const text = textValue(value);
  if (!text) return null;
  const normalized = text.toLowerCase().replace(/_/gu, "-");
  return normalized === "cancelled" ? "canceled" : normalized;
}

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

function normalizeProjectionDiagnostic(...values: unknown[]): ProjectionDiagnostic | undefined {
  for (const value of values) {
    const record = recordValue(value);
    if (!record) continue;
    const source = recordValue(record.projection) ?? record;
    const blocker = recordValue(source.blocker ?? record.blocker);
    const projectionStatus = textValue(source.projectionStatus ?? record.projectionStatus);
    const projectionHealth = textValue(source.projectionHealth ?? record.projectionHealth);
    const lastProjectedSeq = numberValue(source.lastProjectedSeq ?? record.lastProjectedSeq);
    const staleMs = numberValue(source.staleMs ?? record.staleMs);
    const sourceRunId = textValue(source.sourceRunId ?? record.sourceRunId);
    const sourceCommandId = textValue(source.sourceCommandId ?? record.sourceCommandId);
    if (!projectionStatus && !projectionHealth && !blocker && lastProjectedSeq == null && staleMs == null && !sourceRunId && !sourceCommandId) continue;
    return {
      ...source,
      projectionStatus: projectionStatus ?? undefined,
      projectionHealth: projectionHealth ?? undefined,
      lastProjectedSeq,
      sourceRunId: sourceRunId ?? undefined,
      sourceCommandId: sourceCommandId ?? undefined,
      staleMs,
      blocker: blocker ?? null,
      valuesRedacted: source.valuesRedacted !== false
    } as ProjectionDiagnostic;
  }
  return undefined;
}

function numberValue(value: unknown): number | null {
  const number = Number(value);
  return Number.isFinite(number) ? number : null;
}

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