import assert from "node:assert/strict";

export const ACTOR = { id: "usr_workbench_reader", username: "reader", displayName: "Reader", role: "user", status: "active" };

export async function getJson(port, path) {
  const response = await fetch(`http://127.0.0.1:${port}${path}`);
  return {
    status: response.status,
    body: await response.json()
  };
}

export async function waitForCondition(predicate, timeoutMs = 500) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (predicate()) return;
    await new Promise((resolve) => setTimeout(resolve, 5));
  }
  assert.fail("condition was not met before timeout");
}

export function createDurableFactsRuntimeStore({ sessions = [], queryError = null, queries = [] } = {}) {
  const facts = emptyFacts();
  for (const item of sessions) mergeFacts(facts, buildDurableFactsForSession(item));
  return createRuntimeStoreFromFacts(facts, { queryError, queries });
}

export function createRuntimeStoreFromFacts(facts, { queryError = null, queries = [] } = {}) {
  return {
    queries,
    async queryWorkbenchFacts(params = {}) {
      queries.push(params);
      if (queryError) throw queryError;
      const filtered = filterFacts(facts, params);
      return {
        facts: filtered,
        count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
        persistence: { adapter: "test-durable-workbench-facts", durable: true }
      };
    }
  };
}

export function buildDurableFactsForSession({ session, events = [], status = null, finalText = null, assistantText = null, runId = null, commandId = null, projectionStatus = null, projectionHealth = "healthy", lastProjectedSeq = null, omitSessions = false } = {}) {
  const traceId = session.lastTraceId ?? session.session?.lastTraceId;
  const projectedStatus = normalizeTestStatus(status ?? session.status);
  const terminal = testTerminalStatuses.has(projectedStatus);
  const normalizedEvents = normalizeTestEvents(events, session, { traceId });
  const timing = testTimingProjection({ session, events: normalizedEvents, status: projectedStatus, terminal });
  const normalizedMessages = normalizeTestMessages(session, { traceId, status: projectedStatus, finalText, terminal, timing });
  const explicitLastProjectedSeq = lastProjectedSeq !== null && lastProjectedSeq !== undefined ? Number(lastProjectedSeq) : NaN;
  const projectedSeq = Number.isFinite(explicitLastProjectedSeq) ? explicitLastProjectedSeq : Math.max(normalizedEvents.length, normalizedMessages.length, 0);
  const checkpointStatus = projectionStatus ?? (terminal ? "caught_up" : "projecting");
  return {
    sessions: omitSessions ? [] : [{
      sessionId: session.id,
      ownerUserId: session.ownerUserId,
      ownerRole: session.ownerRole ?? null,
      projectId: session.projectId ?? null,
      conversationId: session.conversationId ?? null,
      threadId: session.threadId ?? null,
      agentId: session.agentId ?? "hwlab-code-agent",
      status: projectedStatus,
      lastTraceId: traceId,
      providerProfile: session.session?.providerProfile ?? null,
      projectedSeq,
      sourceSeq: projectedSeq,
      sourceEventId: traceId,
      terminal,
      sealed: terminal,
      timing,
      startedAt: timing.startedAt,
      lastEventAt: timing.lastEventAt,
      finishedAt: timing.finishedAt,
      durationMs: timing.durationMs,
      createdAt: session.startedAt ?? session.createdAt ?? session.updatedAt ?? null,
      updatedAt: session.updatedAt ?? session.endedAt ?? session.startedAt ?? null,
      valuesRedacted: true
    }],
    messages: normalizedMessages,
    parts: normalizedMessages.filter((message) => message.text).map((message, index) => {
      const partType = message.role !== "user" && message.terminal ? "final_response" : "text";
      return {
        partId: `prt_${session.id}_${index}`,
        messageId: message.messageId,
        sessionId: session.id,
        turnId: message.turnId,
        traceId: message.traceId,
        partIndex: 0,
        partType,
        status: message.status,
        text: message.text,
        projectedSeq: message.projectedSeq,
        sourceSeq: message.sourceSeq,
        sourceEventId: message.sourceEventId,
        terminal: message.terminal,
        sealed: partType === "final_response" ? true : message.sealed,
        updatedAt: message.updatedAt,
        valuesRedacted: true
      };
    }),
    turns: traceId ? [{
      turnId: traceId,
      sessionId: session.id,
      traceId,
      messageId: normalizedMessages.find((message) => message.role !== "user")?.messageId ?? null,
      status: projectedStatus,
      projectedSeq,
      sourceSeq: projectedSeq,
      sourceEventId: traceId,
      terminal,
      sealed: terminal,
      assistantText: assistantText ?? null,
      finalResponse: finalText ? { text: finalText, status: projectedStatus, traceId, valuesPrinted: false } : null,
      diagnostic: { projectionStatus: checkpointStatus, projectionHealth, valuesRedacted: true },
      timing,
      startedAt: timing.startedAt,
      lastEventAt: timing.lastEventAt,
      finishedAt: timing.finishedAt,
      durationMs: timing.durationMs,
      createdAt: session.startedAt ?? session.updatedAt ?? null,
      updatedAt: session.updatedAt ?? null,
      valuesRedacted: true
    }] : [],
    traceEvents: normalizedEvents,
    checkpoints: traceId ? [{
      traceId,
      sessionId: session.id,
      turnId: traceId,
      runId,
      commandId,
      projectedSeq,
      sourceSeq: projectedSeq,
      sourceEventId: traceId,
      projectionStatus: checkpointStatus,
      projectionHealth,
      terminal,
      sealed: terminal,
      diagnostic: { projectionStatus: checkpointStatus, projectionHealth, valuesRedacted: true },
      timing,
      startedAt: timing.startedAt,
      lastEventAt: timing.lastEventAt,
      finishedAt: timing.finishedAt,
      durationMs: timing.durationMs,
      createdAt: session.startedAt ?? session.updatedAt ?? null,
      updatedAt: session.updatedAt ?? null,
      valuesRedacted: true
    }] : []
  };
}

export function normalizeTestMessages(session, { traceId, status, finalText, terminal, timing }) {
  const raw = Array.isArray(session.session?.messages) ? session.session.messages : [];
  return raw.map((message, index) => {
    const role = message.role ?? (index % 2 === 0 ? "user" : "agent");
    const isAssistant = role === "agent" || role === "assistant";
    const text = isAssistant && finalText ? finalText : String(message.text ?? message.content ?? "");
    const messageStatus = isAssistant && finalText ? status : normalizeTestStatus(message.status ?? (role === "user" ? "sent" : status));
    const messageTraceId = message.traceId ?? traceId;
    const projectedSeq = Number.isFinite(Number(message.projectedSeq)) ? Number(message.projectedSeq) : index + 1;
    const sourceSeq = Number.isFinite(Number(message.sourceSeq)) ? Number(message.sourceSeq) : projectedSeq;
    return {
      messageId: message.messageId ?? `msg_${session.id}_${index}`,
      sessionId: session.id,
      turnId: message.turnId ?? messageTraceId,
      traceId: messageTraceId,
      role,
      status: messageStatus,
      projectedSeq,
      sourceSeq,
      sourceEventId: `${session.id}:message:${index}`,
      terminal: isAssistant && terminal,
      sealed: isAssistant && terminal,
      text,
      ...(isAssistant ? { timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs } : {}),
      createdAt: message.createdAt ?? session.startedAt ?? session.updatedAt ?? null,
      updatedAt: message.updatedAt ?? session.updatedAt ?? null,
      valuesRedacted: true
    };
  });
}

export function testTimingProjection({ session, events = [], status, terminal }) {
  const startedAt = timestampIso(session.startedAt ?? session.createdAt ?? events[0]?.createdAt ?? events[0]?.occurredAt);
  const lastEvent = events.at(-1) ?? null;
  const lastEventAt = timestampIso(lastEvent?.updatedAt ?? lastEvent?.createdAt ?? lastEvent?.occurredAt ?? session.updatedAt);
  const finishedAt = terminal ? timestampIso(session.endedAt ?? lastEventAt ?? session.updatedAt) : null;
  const durationMs = terminal ? elapsedMs(startedAt, finishedAt ?? lastEventAt) : null;
  return { startedAt, lastEventAt, finishedAt, durationMs, status, valuesRedacted: true };
}

export function timestampIso(value) {
  const ms = Date.parse(String(value ?? ""));
  return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
}

export function elapsedMs(startedAt, endedAt) {
  const start = Date.parse(String(startedAt ?? ""));
  const end = Date.parse(String(endedAt ?? ""));
  if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
  return end - start;
}

export function normalizeTestEvents(events, session, { traceId }) {
  return events.map((event, index) => {
    const projectedSeq = Number.isFinite(Number(event.projectedSeq)) ? Number(event.projectedSeq) : Number.isFinite(Number(event.seq)) ? Number(event.seq) : index + 1;
    const sourceSeq = Number.isFinite(Number(event.sourceSeq)) ? Number(event.sourceSeq) : null;
    return {
      ...event,
      id: event.id ?? `wte_${session.id}_${index}`,
      traceId: event.traceId ?? traceId,
      sessionId: event.sessionId ?? session.id,
      turnId: event.turnId ?? traceId,
      messageId: event.messageId ?? null,
      sourceSeq,
      sourceEventId: event.sourceEventId ?? `${traceId}:${sourceSeq ?? projectedSeq}:${index}`,
      projectedSeq,
      eventType: event.eventType ?? event.type ?? event.label ?? "event",
      terminal: event.terminal === true,
      sealed: event.terminal === true,
      occurredAt: event.occurredAt ?? event.createdAt ?? session.updatedAt ?? null,
      updatedAt: event.updatedAt ?? event.createdAt ?? session.updatedAt ?? null,
      valuesRedacted: true
    };
  });
}

export function filterFacts(facts, params = {}) {
  const families = testFactFamilySet(params);
  const afterProjectedSeq = Number.parseInt(params.afterProjectedSeq ?? "", 10);
  const filtered = {
    sessions: families.has("sessions") ? facts.sessions.filter((record) => matchesFact(record, params, ["sessionId", "ownerUserId", "projectId", "conversationId", "threadId", "status"]) && matchesTraceFact(record.lastTraceId, params)) : [],
    messages: families.has("messages") ? facts.messages.filter((record) => matchesFact(record, params, ["messageId", "sessionId", "turnId", "traceId", "role", "status"])) : [],
    parts: families.has("parts") ? facts.parts.filter((record) => matchesFact(record, params, ["partId", "messageId", "sessionId", "turnId", "traceId", "partType", "status"])) : [],
    turns: families.has("turns") ? facts.turns.filter((record) => matchesFact(record, params, ["turnId", "sessionId", "traceId", "messageId", "status"])) : [],
    traceEvents: families.has("traceEvents") ? facts.traceEvents
      .filter((record) => matchesFact(record, params, ["id", "traceId", "sessionId", "turnId", "messageId", "eventType"]))
      .filter((record) => !Number.isInteger(afterProjectedSeq) || afterProjectedSeq <= 0 || Number(record.projectedSeq) > afterProjectedSeq)
      .sort((left, right) => Number(left.projectedSeq) - Number(right.projectedSeq)) : [],
    checkpoints: families.has("checkpoints") ? facts.checkpoints.filter((record) => matchesFact(record, params, ["traceId", "sessionId", "turnId", "runId", "commandId", "projectionStatus", "projectionHealth"])) : []
  };
  if (workbenchTestFactOrder(params, "sessions") === "updated_desc") {
    filtered.sessions.sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")) || String(right.sessionId ?? "").localeCompare(String(left.sessionId ?? "")));
  }
  const limit = Number.parseInt(params.limit ?? "", 10);
  if (!Number.isInteger(limit) || limit <= 0) return filtered;
  return Object.fromEntries(Object.entries(filtered).map(([key, value]) => [key, value.slice(0, limit)]));
}

export function workbenchTestFactOrder(params = {}, family = "") {
  const value = params[`${family}Order`] ?? params.order;
  return value === "updated_desc" ? "updated_desc" : "updated_asc";
}

export function matchesFact(record, params, fields) {
  return fields.every((field) => {
    if (params[field] !== undefined && params[field] !== null && record[field] !== params[field]) return false;
    const values = Array.isArray(params[`${field}s`]) ? params[`${field}s`].map((item) => String(item ?? "")).filter(Boolean) : [];
    return values.length === 0 || values.includes(String(record[field] ?? ""));
  });
}

export function matchesTraceFact(value, params) {
  if (params.traceId && value !== params.traceId) return false;
  const traceIds = Array.isArray(params.traceIds) ? params.traceIds.map((item) => String(item ?? "")).filter(Boolean) : [];
  return traceIds.length === 0 || traceIds.includes(String(value ?? ""));
}

export function testFactFamilySet(params = {}) {
  const all = ["sessions", "messages", "parts", "turns", "traceEvents", "checkpoints"];
  const raw = params.families ?? params.factFamilies;
  if (!raw) return new Set(all);
  const values = Array.isArray(raw) ? raw : String(raw).split(",");
  const selected = values.map((item) => String(item ?? "").trim()).filter((item) => all.includes(item));
  return new Set(selected.length ? selected : all);
}

export function mergeFacts(target, source) {
  for (const key of Object.keys(target)) target[key].push(...source[key]);
  return target;
}

export function emptyFacts() {
  return { sessions: [], messages: [], parts: [], turns: [], traceEvents: [], checkpoints: [] };
}

export function normalizeTestStatus(value) {
  const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
  if (status === "active" || status === "busy" || status === "processing") return "running";
  if (status === "cancelled") return "canceled";
  return status || "unknown";
}

const testTerminalStatuses = new Set(["completed", "failed", "blocked", "timeout", "canceled", "idle"]);

export async function getSseEvents(port, path, count) {
  const controller = new AbortController();
  const response = await fetch(`http://127.0.0.1:${port}${path}`, { signal: controller.signal });
  assert.equal(response.status, 200);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  const events = [];
  try {
    while (events.length < count) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      for (;;) {
        const index = buffer.indexOf("\n\n");
        if (index < 0) break;
        const block = buffer.slice(0, index);
        buffer = buffer.slice(index + 2);
        events.push(parseSseBlock(block));
        if (events.length >= count) break;
      }
    }
  } finally {
    controller.abort();
  }
  return events;
}

export function parseSseBlock(block) {
  const lines = block.split("\n");
  const event = lines.find((line) => line.startsWith("event:"))?.slice(6).trim() ?? "message";
  const id = lines.find((line) => line.startsWith("id:"))?.slice(3).trim() ?? null;
  const data = lines.filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
  return { event, id, data: JSON.parse(data) };
}

export function createFakeKafkaFactory() {
  let eachMessage = null;
  let offset = 0;
  let subscribedTopic = "hwlab.event.v1";
  const consumer = {
    connect: async () => undefined,
    subscribe: async (input = {}) => { subscribedTopic = String(input.topic ?? subscribedTopic); },
    run: async (input = {}) => { eachMessage = input.eachMessage; },
    stop: async () => undefined,
    disconnect: async () => undefined
  };
  return {
    factory: () => ({ consumer: () => consumer }),
    emit: async (value = {}) => {
      await waitFor(() => typeof eachMessage === "function");
      await eachMessage({
        topic: subscribedTopic,
        partition: 0,
        message: {
          offset: String(offset++),
          key: Buffer.from(value.traceId ?? value.sessionId ?? "fake"),
          timestamp: String(Date.now()),
          value: Buffer.from(JSON.stringify(value))
        }
      });
    }
  };
}

export async function waitFor(predicate, timeoutMs = 1000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (predicate()) return;
    await new Promise((resolve) => setTimeout(resolve, 10));
  }
  throw new Error("waitFor timed out");
}
