import { createHash } from "node:crypto";
import type { JsonValue, LiveOutput, QueueTask, TaskStatus } from "./types";

type JsonRecord = Record<string, JsonValue>;

interface OaEventContext {
  baseUrl: string;
  logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void;
  nowIso: () => string;
}

interface OaEventEnvelope {
  eventId: string;
  type: string;
  createdAt: string;
  sourceKind: string;
  sourceId: string;
  aggregateType: string;
  aggregateId: string;
  correlationId: string;
  causationId?: string | null;
  tags: string[];
  payload: JsonRecord;
}

interface QueuedOaEvent {
  event: OaEventEnvelope;
  attempts: number;
}

export interface OaTraceStats extends JsonRecord {
  scopeId: string;
  source: "oa-event-flow";
}

export interface TraceStatsFallback {
  stepCount?: number | null;
  llmStepCount?: number | null;
  traceLineCount?: number | null;
  outputMaxSeq?: number | null;
  readCount?: number | null;
  editCount?: number | null;
  runCount?: number | null;
  errorCount?: number | null;
  reason?: string;
}

export interface OaTraceStepSummary {
  eventSequence: number;
  seq: number;
  at: string;
  kind: string;
  title: string;
  status: string;
  summaryLines: string[];
  rawSeqs: number[];
  scopeId: string;
  attemptIndex: number | null;
  source: "oa-event-flow";
}

const postTimeoutMs = 10000;
const publishConcurrency = 4;
const maxQueuedEvents = 20_000;
const maxPublishAttempts = 5;
let context: OaEventContext | null = null;
let queuedEvents: QueuedOaEvent[] = [];
let activePublishes = 0;
let totalEnqueued = 0;
let totalSucceeded = 0;
let totalFailed = 0;
let totalRetried = 0;
let totalAbandoned = 0;
let totalDropped = 0;
let totalOverflowDropped = 0;
let lastEnqueuedAt: string | null = null;
let lastSucceededAt: string | null = null;
let lastDroppedAt: string | null = null;
let lastOverflowAt: string | null = null;
let lastError: JsonRecord | null = null;
let nextOverflowLogAtMs = 0;

export function configureOaEvents(runtimeContext: OaEventContext): void {
  const baseUrl = runtimeContext.baseUrl.replace(/\/+$/u, "");
  if (baseUrl.length === 0) throw new Error("OA_EVENT_FLOW_BASE_URL is required");
  context = { ...runtimeContext, baseUrl };
}

function ctx(): OaEventContext {
  if (context === null) throw new Error("oa-events module is not configured");
  return context;
}

function safePreview(value: string, max = 300): string {
  const clean = String(value || "").replace(/\u001b\[[0-9;]*m/gu, "").trim();
  return clean.length > max ? `${clean.slice(0, max)}...` : clean;
}

function hash(value: unknown): string {
  return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 20);
}

function addTag(tags: string[], value: string): void {
  const tag = value.trim();
  if (tag.length > 0 && !tags.includes(tag)) tags.push(tag);
}

function taskTags(task: QueueTask, queueId: string, extra: string[] = [], attemptIndexOverride: number | null = null): string[] {
  const tags = ["service:code-queue", "trace", `task:${task.id}`, `queue:${queueId}`];
  const attempt = Number(attemptIndexOverride ?? (task.currentAttempt || task.attempts.length || 0));
  if (attempt > 0) addTag(tags, `attempt:${attempt}`);
  for (const tag of extra) addTag(tags, tag);
  return tags;
}

export function oaEventPublisherStatus(): JsonRecord {
  return {
    pending: queuedEvents.length,
    active: activePublishes,
    concurrency: publishConcurrency,
    maxQueuedEvents,
    maxPublishAttempts,
    totalEnqueued,
    totalSucceeded,
    totalFailed,
    totalRetried,
    totalAbandoned,
    totalDropped,
    totalOverflowDropped,
    lastEnqueuedAt,
    lastSucceededAt,
    lastDroppedAt,
    lastOverflowAt,
    lastError,
  };
}

async function sendOaEvent(item: QueuedOaEvent): Promise<boolean> {
  const runtime = ctx();
  const event = item.event;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), postTimeoutMs);
  try {
    const response = await fetch(`${runtime.baseUrl}/api/events`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(event),
      signal: controller.signal,
    });
    if (!response.ok) {
      totalFailed += 1;
      lastError = { eventId: event.eventId, type: event.type, attempt: item.attempts, status: response.status, body: safePreview(await response.text(), 1000) };
      runtime.logger("warn", "oa_event_publish_failed", lastError);
      return false;
    }
    totalSucceeded += 1;
    lastSucceededAt = runtime.nowIso();
    return true;
  } catch (error) {
    totalFailed += 1;
    lastError = { eventId: event.eventId, type: event.type, attempt: item.attempts, error: error instanceof Error ? error.message : String(error) };
    runtime.logger("warn", "oa_event_publish_error", lastError);
    return false;
  } finally {
    clearTimeout(timer);
  }
}

function pumpOaEventQueue(): void {
  while (activePublishes < publishConcurrency && queuedEvents.length > 0) {
    const item = queuedEvents.shift();
    if (item === undefined) return;
    activePublishes += 1;
    void sendOaEvent(item).then((ok) => {
      if (!ok) {
        if (item.attempts < maxPublishAttempts) {
          totalRetried += 1;
          queuedEvents.push({ event: item.event, attempts: item.attempts + 1 });
        } else {
          totalAbandoned += 1;
          lastError = { eventId: item.event.eventId, type: item.event.type, attempt: item.attempts, error: "oa event publish attempts exhausted" };
          ctx().logger("error", "oa_event_publish_abandoned", lastError);
        }
      }
    }).finally(() => {
      activePublishes = Math.max(0, activePublishes - 1);
      pumpOaEventQueue();
    });
  }
}

function postOaEvent(event: OaEventEnvelope): void {
  const runtime = ctx();
  totalEnqueued += 1;
  lastEnqueuedAt = runtime.nowIso();
  if (queuedEvents.length >= maxQueuedEvents) {
    totalDropped += 1;
    totalOverflowDropped += 1;
    lastDroppedAt = lastEnqueuedAt;
    lastOverflowAt = lastEnqueuedAt;
    lastError = { eventId: event.eventId, type: event.type, error: "oa event publish queue overflow; incoming observability event dropped" };
    const nowMs = Date.now();
    if (nowMs >= nextOverflowLogAtMs) {
      nextOverflowLogAtMs = nowMs + 30_000;
      runtime.logger("error", "oa_event_publish_queue_overflow", {
        ...lastError,
        pending: queuedEvents.length,
        maxQueuedEvents,
        totalOverflowDropped,
        isolated: true,
      });
    }
    return;
  }
  queuedEvents.push({ event, attempts: 1 });
  pumpOaEventQueue();
}

function normalizeCommandText(text: string): string {
  const trimmed = String(text || "").trim();
  if (!trimmed.startsWith("{")) return trimmed;
  try {
    const parsed = JSON.parse(trimmed) as Record<string, unknown>;
    const command = parsed.command ?? parsed.cmd ?? parsed.text ?? parsed.summary;
    return typeof command === "string" ? command : trimmed;
  } catch {
    return trimmed;
  }
}

function commandKind(command: string): "read" | "edit" | "run" {
  if (/\b(apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text|write|patch|edit|delete|create)\b/iu.test(command)) return "edit";
  if (/\b(rg|grep|find|ls|cat|sed -n|tail|head|git status|git diff|ps|read|glob|search|view|webSearch)\b/iu.test(command)) return "read";
  return "run";
}

function openCodeToolRecord(output: LiveOutput): Record<string, unknown> | null {
  if (!String(output.method || "").startsWith("opencode/")) return null;
  try {
    const parsed = JSON.parse(output.text) as unknown;
    return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
  } catch {
    return null;
  }
}

function recordString(record: Record<string, unknown> | null, keys: string[]): string {
  if (record === null) return "";
  for (const key of keys) {
    const value = record[key];
    if (typeof value === "string") return value;
  }
  return "";
}

function asJsonRecord(value: unknown): JsonRecord {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {};
}

function stringValue(value: unknown, fallback = ""): string {
  return typeof value === "string" ? value : fallback;
}

function positiveInteger(value: unknown): number | null {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? Math.floor(parsed) : null;
}

export function outputTraceKind(output: LiveOutput): "read" | "edit" | "run" | "error" | "message" | "system" {
  if (output.channel === "diff") return "edit";
  if (output.channel === "error") return "error";
  if (output.channel === "assistant" || output.channel === "user" || output.channel === "reasoning") return "message";
  if (output.channel === "tool") {
    const record = openCodeToolRecord(output);
    if (record === null) return commandKind(normalizeCommandText(output.text));
    const part = record?.part && typeof record.part === "object" && !Array.isArray(record.part) ? record.part as Record<string, unknown> : null;
    const state = part?.state && typeof part.state === "object" && !Array.isArray(part.state) ? part.state as Record<string, unknown> : null;
    const input = state?.input && typeof state.input === "object" && !Array.isArray(state.input) ? state.input as Record<string, unknown> : null;
    const tool = recordString(part, ["tool", "title"]) || recordString(record, ["type", "event", "name"]);
    const command = recordString(input, ["command", "cmd", "script", "path", "pattern", "query"]);
    return commandKind(`${tool} ${command}`);
  }
  if (output.channel === "command") return commandKind(normalizeCommandText(output.text));
  return "system";
}

function outputTitle(output: LiveOutput, kind: string): string {
  const method = String(output.method || "");
  if (output.channel === "diff") return "Edited files";
  if (output.channel === "error") return "Error";
  if (output.channel === "assistant") return "Assistant message";
  if (output.channel === "reasoning") return "Reasoning";
  if (output.channel === "user") return method === "enqueue" ? "Submitted prompt" : "User prompt";
  const command = normalizeCommandText(output.text).split(/\r?\n/u).find((line) => line.trim().length > 0)?.trim() ?? "";
  if (command.length > 0) return safePreview(command, 160);
  return kind === "read" ? "Read" : kind === "edit" ? "Edit" : kind === "run" ? "Run" : method || output.channel;
}

function outputSummaryLines(output: LiveOutput): string[] {
  return String(output.text || "")
    .replace(/\u001b\[[0-9;]*m/gu, "")
    .split(/\r?\n/u)
    .map((line) => line.trim())
    .filter(Boolean)
    .slice(0, 4)
    .map((line) => safePreview(line, 220));
}

function outputAttemptIndex(output: LiveOutput): number | null {
  const match = String(output.text || "").match(/\battempt\s+(\d+)\s*\/\s*\d+\b/iu);
  const value = Number(match?.[1] ?? NaN);
  return Number.isInteger(value) && value > 0 ? value : null;
}

function taskOutputAttemptIndex(task: QueueTask, output: LiveOutput, attemptIndexOverride: number | null = null): number | null {
  if (attemptIndexOverride !== null && Number.isInteger(attemptIndexOverride) && attemptIndexOverride > 0) return attemptIndexOverride;
  const fromOutput = outputAttemptIndex(output);
  if (fromOutput !== null) return fromOutput;
  const current = Number(task.currentAttempt || 0);
  return Number.isInteger(current) && current > 0 ? current : null;
}

function taskStatusPayload(task: QueueTask, queueId: string, reason: string, stepCount: number, outputMaxSeq: number): JsonRecord {
  return {
    taskId: task.id,
    queueId,
    reason,
    status: task.status,
    updatedAt: task.updatedAt,
    createdAt: task.createdAt,
    startedAt: task.startedAt,
    finishedAt: task.finishedAt,
    readAt: task.readAt,
    providerId: task.providerId,
    executionMode: task.executionMode,
    model: task.model,
    currentAttempt: task.currentAttempt,
    stepCount,
    outputMaxSeq,
  };
}

export function publishCodeQueueTaskUpdated(task: QueueTask, queueId: string, reason: string, stepCount: number, outputMaxSeq: number): void {
  postOaEvent({
    eventId: `code-queue:task-updated:${task.id}:${reason}:${task.updatedAt}:${outputMaxSeq}:${hash({ status: task.status, queueId, readAt: task.readAt })}`,
    type: "task-updated",
    createdAt: ctx().nowIso(),
    sourceKind: "service",
    sourceId: "code-queue",
    aggregateType: "task",
    aggregateId: task.id,
    correlationId: task.id,
    tags: taskTags(task, queueId, ["task"]),
    payload: taskStatusPayload(task, queueId, reason, stepCount, outputMaxSeq),
  });
}

export function publishCodeQueueTraceStatsSnapshot(
  task: QueueTask,
  queueId: string,
  reason: string,
  stepCount: number,
  outputMaxSeq: number,
  statsOverride: Partial<{ llmStepCount: number; traceLineCount: number; readCount: number; editCount: number; runCount: number; errorCount: number }> = {},
): void {
  postOaEvent({
    eventId: `code-queue:trace-stats-snapshot:${task.id}:${task.updatedAt}:${stepCount}:${outputMaxSeq}`,
    type: "trace-stats-snapshot",
    createdAt: ctx().nowIso(),
    sourceKind: "service",
    sourceId: "code-queue",
    aggregateType: "task",
    aggregateId: task.id,
    correlationId: task.id,
    tags: taskTags(task, queueId, ["stats"]),
    payload: {
      ...taskStatusPayload(task, queueId, reason, stepCount, outputMaxSeq),
      scopeId: `task:${task.id}`,
      stats: {
        stepCount,
        llmStepCount: statsOverride.llmStepCount ?? stepCount,
        traceLineCount: statsOverride.traceLineCount ?? stepCount,
        outputMaxSeq,
        readCount: statsOverride.readCount ?? 0,
        editCount: statsOverride.editCount ?? 0,
        runCount: statsOverride.runCount ?? 0,
        errorCount: statsOverride.errorCount ?? (task.lastError ? 1 : 0),
        attempts: task.attempts.map((attempt) => ({ index: attempt.index, mode: attempt.mode, startedAt: attempt.startedAt, finishedAt: attempt.finishedAt, terminalStatus: attempt.terminalStatus })),
      },
    },
  });
}

export function publishCodeQueueTraceStep(task: QueueTask, queueId: string, output: LiveOutput, outputMaxSeq: number, attemptIndexOverride: number | null = null, revision = 0): void {
  const kind = outputTraceKind(output);
  const attemptIndex = taskOutputAttemptIndex(task, output, attemptIndexOverride);
  const attemptScopeId = attemptIndex === null ? null : taskAttemptScopeId(task.id, attemptIndex);
  postOaEvent({
    eventId: `code-queue:trace-step-created:${task.id}:${output.seq}:${Math.max(0, Math.floor(revision))}`,
    type: "trace-step-created",
    createdAt: output.at || ctx().nowIso(),
    sourceKind: "service",
    sourceId: "code-queue",
    aggregateType: "task",
    aggregateId: task.id,
    correlationId: task.id,
    tags: taskTags(task, queueId, attemptScopeId === null ? [] : [attemptScopeId], attemptIndex),
    payload: {
      taskId: task.id,
      queueId,
      scopeId: `task:${task.id}`,
      attemptIndex,
      attemptScopeId,
      seq: output.seq,
      outputSeq: output.seq,
      outputMaxSeq,
      kind,
      channel: output.channel,
      method: output.method ?? "",
      itemId: output.itemId ?? "",
      title: outputTitle(output, kind),
      status: task.status,
      summaryLines: outputSummaryLines(output),
      rawSeqs: outputRawSeqs(output),
    },
  });
}

export function publishCodeQueueQueueUpdated(queueId: string, reason: string): void {
  const at = ctx().nowIso();
  postOaEvent({
    eventId: `code-queue:queue-updated:${queueId || "all"}:${reason}:${at}`,
    type: "queue-updated",
    createdAt: at,
    sourceKind: "service",
    sourceId: "code-queue",
    aggregateType: "queue",
    aggregateId: queueId || "all",
    correlationId: `queue:${queueId || "all"}`,
    tags: ["service:code-queue", "queue", ...(queueId ? [`queue:${queueId}`] : [])],
    payload: { queueId, reason, updatedAt: at },
  });
}

export function publishCodeQueueJudgeEvent(task: QueueTask, queueId: string, type: string, payload: JsonRecord): void {
  const at = ctx().nowIso();
  const eventType = type.trim() || "judge-diagnostics";
  const attempt = positiveInteger(payload.attempt) ?? (task.currentAttempt || task.attempts.length || 0);
  postOaEvent({
    eventId: `code-queue:${eventType}:${task.id}:${attempt}:${payload.repairAttempt ?? "na"}:${at}:${hash(payload)}`,
    type: eventType,
    createdAt: at,
    sourceKind: "service",
    sourceId: "code-queue",
    aggregateType: "task",
    aggregateId: task.id,
    correlationId: task.id,
    tags: taskTags(task, queueId, ["judge", "diagnostics"], attempt > 0 ? attempt : null),
    payload: {
      taskId: task.id,
      queueId,
      attempt,
      ...payload,
      createdAt: at,
    },
  });
}

const traceStatsCache = new Map<string, { expiresAt: number; stats: OaTraceStats }>();
let traceStatsCacheLastPrune = 0;

function pruneTraceStatsCache(): void {
  const now = Date.now();
  if (now - traceStatsCacheLastPrune < 10_000) return;
  traceStatsCacheLastPrune = now;
  for (const [key, entry] of traceStatsCache) {
    if (entry.expiresAt <= now) traceStatsCache.delete(key);
  }
}

export async function readOaTraceStatsForScopeIds(scopeIds: string[]): Promise<Map<string, OaTraceStats>> {
  const result = new Map<string, OaTraceStats>();
  const uniqueScopeIds = Array.from(new Set(scopeIds.map((id) => String(id || "").trim()).filter(Boolean)));
  if (uniqueScopeIds.length === 0) return result;
  const now = Date.now();
  const uncachedIds: string[] = [];
  for (const id of uniqueScopeIds) {
    const cached = traceStatsCache.get(id);
    if (cached !== undefined && cached.expiresAt > now) {
      result.set(cached.stats.scopeId, cached.stats);
    } else {
      uncachedIds.push(id);
    }
  }
  if (uncachedIds.length === 0) { pruneTraceStatsCache(); return result; }
  const runtime = ctx();
  const url = new URL(`${runtime.baseUrl}/api/stats/trace`);
  url.searchParams.set("scopeIds", uncachedIds.join(","));
  url.searchParams.set("limit", String(Math.max(100, uncachedIds.length)));
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 2500);
  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`status=${response.status}`);
    const body = await response.json() as Record<string, unknown>;
    const stats = Array.isArray(body.stats) ? body.stats : [];
    const cacheTtl = 8_000;
    for (const item of stats) {
      if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
      const record = item as OaTraceStats;
      if (typeof record.scopeId !== "string") continue;
      result.set(record.scopeId, record);
      traceStatsCache.set(record.scopeId, { expiresAt: now + cacheTtl, stats: record });
    }
  } catch (error) {
    runtime.logger("warn", "oa_trace_stats_read_failed", { scopeCount: uncachedIds.length, error: error instanceof Error ? error.message : String(error) });
  } finally {
    clearTimeout(timer);
  }
  pruneTraceStatsCache();
  return result;
}

export async function readOaTraceStatsForTasks(taskIds: string[]): Promise<Map<string, OaTraceStats>> {
  const uniqueTaskIds = Array.from(new Set(taskIds.map((id) => String(id || "").trim()).filter(Boolean)));
  return readOaTraceStatsForScopeIds(uniqueTaskIds.map((id) => taskScopeId(id)));
}

export async function readOaTraceStatsForTask(taskId: string): Promise<OaTraceStats | null> {
  const map = await readOaTraceStatsForTasks([taskId]);
  return map.get(taskScopeId(taskId)) ?? null;
}

export async function readOaTraceStatsForTaskAttempts(taskId: string, attemptIndexes: number[]): Promise<Map<string, OaTraceStats>> {
  const uniqueAttempts = Array.from(new Set(attemptIndexes.filter((index) => Number.isInteger(index) && index > 0).map((index) => Math.floor(index))));
  return readOaTraceStatsForScopeIds([taskScopeId(taskId), ...uniqueAttempts.map((index) => taskAttemptScopeId(taskId, index))]);
}

function eventPayloadRecord(event: unknown): JsonRecord {
  if (typeof event !== "object" || event === null || Array.isArray(event)) return {};
  return asJsonRecord((event as Record<string, unknown>).payload);
}

function stringList(value: unknown, maxItems = 8): string[] {
  if (!Array.isArray(value)) return [];
  return value.map((item) => String(item ?? "").trimEnd()).filter((item) => item.length > 0).slice(0, maxItems);
}

function numberList(value: unknown, fallback: number): number[] {
  if (!Array.isArray(value)) return [fallback];
  const values = value.map((item) => Number(item)).filter((item) => Number.isFinite(item)).map((item) => Math.floor(item));
  return values.length > 0 ? values : [fallback];
}

function outputRawSeqs(output: LiveOutput): number[] {
  const rawSeqs = (output as LiveOutput & { rawSeqs?: unknown }).rawSeqs;
  const values = Array.isArray(rawSeqs)
    ? rawSeqs.map((item) => Number(item)).filter((item) => Number.isFinite(item)).map((item) => Math.floor(item))
    : [];
  return values.length > 0 ? Array.from(new Set(values)) : [output.seq];
}

function commandLifecycleStatus(payload: JsonRecord, title: string, summaryLines: string[]): string {
  const source = [title, ...summaryLines].join("\n");
  const status = /\bstatus=([A-Za-z0-9_-]+)/u.exec(source)?.[1];
  if (status !== undefined && status.length > 0) return status;
  const method = stringValue(payload.method);
  if (method.length > 0) return method;
  return stringValue(payload.status);
}

function traceStepFromEvent(event: unknown): OaTraceStepSummary | null {
  if (typeof event !== "object" || event === null || Array.isArray(event)) return null;
  const record = event as Record<string, unknown>;
  const payload = eventPayloadRecord(record);
  const seq = Number(payload.seq ?? payload.stepSeq ?? payload.outputSeq);
  if (!Number.isFinite(seq)) return null;
  const eventSequence = Number(record.sequence ?? 0);
  const title = stringValue(payload.title, stringValue(payload.method, stringValue(payload.kind))).slice(0, 300);
  const summaryLines = stringList(payload.summaryLines);
  return {
    eventSequence: Number.isFinite(eventSequence) ? Math.floor(eventSequence) : 0,
    seq: Math.floor(seq),
    at: stringValue(record.createdAt, stringValue(payload.createdAt, ctx().nowIso())),
    kind: stringValue(payload.kind, stringValue(payload.stepKind, stringValue(payload.channel, "system"))),
    title,
    status: commandLifecycleStatus(payload, title, summaryLines).slice(0, 120),
    summaryLines,
    rawSeqs: numberList(payload.rawSeqs, Math.floor(seq)),
    scopeId: stringValue(payload.scopeId, stringValue(payload.attemptScopeId)),
    attemptIndex: positiveInteger(payload.attemptIndex),
    source: "oa-event-flow",
  };
}

function traceStepLifecycleRank(step: OaTraceStepSummary): number {
  const source = [step.title, step.status, ...step.summaryLines].join("\n");
  if (/\bitem\/completed\b|status=completed\b|\bcompleted\b/iu.test(source)) return 2;
  if (/\bitem\/started\b|status=inProgress\b|\binProgress\b/iu.test(source)) return 1;
  return 0;
}

function mergeOaTraceStepSummary(existing: OaTraceStepSummary | undefined, incoming: OaTraceStepSummary): OaTraceStepSummary {
  if (existing === undefined) return incoming;
  const selected = traceStepLifecycleRank(incoming) >= traceStepLifecycleRank(existing) ? incoming : existing;
  return {
    ...existing,
    ...selected,
    eventSequence: Math.max(existing.eventSequence, incoming.eventSequence),
    rawSeqs: Array.from(new Set([...existing.rawSeqs, ...incoming.rawSeqs])),
  };
}

function eventNextAfterSeq(body: Record<string, unknown>, events: unknown[], fallback: number): number {
  const bodyNext = Number(body.nextAfterSeq);
  const eventNext = events.reduce<number>((max, event) => {
    if (typeof event !== "object" || event === null || Array.isArray(event)) return max;
    const sequence = Number((event as Record<string, unknown>).sequence);
    return Number.isFinite(sequence) ? Math.max(max, Math.floor(sequence)) : max;
  }, fallback);
  return Number.isFinite(bodyNext) && bodyNext > fallback ? Math.max(eventNext, Math.floor(bodyNext)) : eventNext;
}

const traceStepReadPageLimit = 500;
const traceStepReadMaxPages = 40;
const traceStepReadAttempts = 3;
const traceStepReadTimeoutMs = 10000;

async function waitMs(ms: number): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchOaTraceStepPage(url: URL): Promise<Record<string, unknown>> {
  let lastError: unknown = null;
  for (let attempt = 1; attempt <= traceStepReadAttempts; attempt += 1) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), traceStepReadTimeoutMs);
    try {
      const response = await fetch(url, { signal: controller.signal });
      if (!response.ok) throw new Error(`status=${response.status}`);
      return await response.json() as Record<string, unknown>;
    } catch (error) {
      lastError = error;
      if (attempt < traceStepReadAttempts) await waitMs(250 * attempt);
    } finally {
      clearTimeout(timer);
    }
  }
  throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "unknown error"));
}

export async function readOaTraceStepsForTask(taskId: string, attemptIndex: number | null = null): Promise<OaTraceStepSummary[]> {
  const cleanTaskId = String(taskId || "").trim();
  if (cleanTaskId.length === 0) return [];
  const runtime = ctx();
  const tags = [`task:${cleanTaskId}`, "trace"];
  if (attemptIndex !== null && Number.isInteger(attemptIndex) && attemptIndex > 0) tags.push(`attempt:${Math.floor(attemptIndex)}`);
  const bySeq = new Map<number, OaTraceStepSummary>();
  let afterSeq = 0;
  for (let page = 0; page < traceStepReadMaxPages; page += 1) {
    const url = new URL(`${runtime.baseUrl}/api/events`);
    url.searchParams.set("tags", tags.join(","));
    url.searchParams.set("type", "trace-step-created");
    url.searchParams.set("limit", String(traceStepReadPageLimit));
    url.searchParams.set("afterSeq", String(afterSeq));
    try {
      const body = await fetchOaTraceStepPage(url);
      const events = Array.isArray(body.events) ? body.events : [];
      for (const event of events) {
        const step = traceStepFromEvent(event);
        if (step !== null) bySeq.set(step.seq, mergeOaTraceStepSummary(bySeq.get(step.seq), step));
      }
      const nextAfterSeq = eventNextAfterSeq(body, events, afterSeq);
      if (events.length < traceStepReadPageLimit || nextAfterSeq <= afterSeq) break;
      afterSeq = nextAfterSeq;
    } catch (error) {
      runtime.logger("warn", "oa_trace_steps_read_failed", { taskId: cleanTaskId, attemptIndex, page, error: error instanceof Error ? error.message : String(error) });
      throw error;
    }
  }
  return Array.from(bySeq.values()).sort((left, right) => left.seq - right.seq);
}

function statNumber(stats: OaTraceStats | null | undefined, key: string): number | null {
  const value = Number(stats?.[key]);
  return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
}

function fallbackNumber(fallback: TraceStatsFallback | null | undefined, key: keyof TraceStatsFallback): number | null {
  const value = Number(fallback?.[key]);
  return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
}

function fallbackTraceStatsPatch(fallback: TraceStatsFallback | null | undefined, taskId = ""): JsonRecord {
  const stepCount = fallbackNumber(fallback, "stepCount") ?? fallbackNumber(fallback, "llmStepCount");
  const llmStepCount = fallbackNumber(fallback, "llmStepCount") ?? stepCount;
  const traceLineCount = fallbackNumber(fallback, "traceLineCount");
  const outputMaxSeq = fallbackNumber(fallback, "outputMaxSeq");
  const rawTracePresent = (stepCount ?? 0) > 0 || (traceLineCount ?? 0) > 0 || (outputMaxSeq ?? 0) > 0;
  const state = rawTracePresent ? "degraded" : "empty";
  const reason = rawTracePresent
    ? fallback?.reason || "oa-event-flow-stats-unavailable-raw-trace-present"
    : "no-trace-steps-yet";
  const traceStats: JsonRecord = {
    scopeId: taskId.length > 0 ? `task:${taskId}` : "task:unknown",
    source: "oa-event-flow",
    stepCount: stepCount ?? 0,
    llmStepCount: llmStepCount ?? stepCount ?? 0,
    traceLineCount: traceLineCount ?? stepCount ?? 0,
    outputMaxSeq: outputMaxSeq ?? 0,
    readCount: fallbackNumber(fallback, "readCount") ?? 0,
    editCount: fallbackNumber(fallback, "editCount") ?? 0,
    runCount: fallbackNumber(fallback, "runCount") ?? 0,
    errorCount: fallbackNumber(fallback, "errorCount") ?? 0,
    sourceHint: "raw-trace-fallback",
  };
  const patch: JsonRecord = {
    traceStats,
    statsSource: rawTracePresent ? "raw-trace-fallback" : "raw-trace-empty",
    traceStatsState: state,
    traceStatsReason: reason,
    statsUnavailable: true,
    statsSyncing: rawTracePresent,
    rawTraceStepCount: stepCount ?? 0,
  };
  if (stepCount !== null) patch.stepCount = stepCount;
  if (llmStepCount !== null) patch.llmStepCount = llmStepCount;
  if (traceLineCount !== null) patch.traceLineCount = traceLineCount;
  if (outputMaxSeq !== null) patch.outputMaxSeq = outputMaxSeq;
  for (const key of ["readCount", "editCount", "runCount", "errorCount"] as const) {
    const value = fallbackNumber(fallback, key);
    if (value !== null) patch[key] = value;
  }
  return patch;
}

export function applyOaTraceStatsToTaskJson(value: JsonValue, stats: OaTraceStats | null | undefined, fallback: TraceStatsFallback | null = null): JsonValue {
  if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
  if (stats === null || stats === undefined) {
    const taskId = typeof (value as JsonRecord).id === "string" ? String((value as JsonRecord).id) : "";
    return { ...(value as JsonRecord), ...fallbackTraceStatsPatch(fallback, taskId) } as unknown as JsonValue;
  }
  const stepCount = statNumber(stats, "stepCount");
  const outputMaxSeq = statNumber(stats, "outputMaxSeq");
  const patch: JsonRecord = {
    traceStats: stats,
    statsSource: "oa-event-flow",
    traceStatsState: "ready",
    traceStatsReason: null,
    statsUnavailable: false,
    statsSyncing: false,
  };
  if (stepCount !== null) {
    patch.stepCount = stepCount;
    patch.llmStepCount = stepCount;
  }
  if (outputMaxSeq !== null) patch.outputMaxSeq = outputMaxSeq;
  return { ...(value as JsonRecord), ...patch };
}

export function taskScopeId(taskId: string): string {
  return `task:${taskId}`;
}

export function taskAttemptScopeId(taskId: string, attemptIndex: number): string {
  return `${taskScopeId(taskId)}:attempt:${Math.floor(attemptIndex)}`;
}
