import type { ChatEvent } from '../gen/apf-ai/chat';

/**
 * Translate one `apf.ai.v1.ChatEvent` into a single SSE frame for
 * the SPA, per ADR-0024 §"Sub-decision 2 — SSE bridge between BFF
 * and SPA". The mapping is intentionally one-to-one with the
 * proto `oneof` cases:
 *
 *   token       → `event: token`        / data = TokenEvent JSON
 *   citation    → `event: citation`     / data = CitationEvent JSON
 *   agent_step  → `event: agent-step`   / data = AgentStepEvent JSON
 *   tool_call   → `event: tool-call`    / data = ToolCallEvent JSON
 *   error       → `event: error`        / data = ErrorEvent JSON
 *   done        → `event: done`         / data = DoneEvent JSON
 *
 * SSE event names use kebab-case so the SPA's `EventSource` /
 * fetch-streaming consumer can dispatch with `.addEventListener('agent-step', …)`
 * without re-mapping proto camelCase. The terminal `done` frame is
 * the contract's stream-close marker — no `[DONE]` sentinel, per
 * ADR-0024.
 *
 * Returns `null` when the event carries no populated oneof case
 * (defensive — gRPC-js will not produce this in practice, but the
 * caller can safely skip on `null` rather than emit an empty
 * frame). The data payload is JSON.stringified; consumers parse
 * with `JSON.parse(event.data)`.
 */
export function chatEventToSseFrame(event: ChatEvent): string | null {
  if (event.token !== undefined) {
    return formatFrame('token', event.token);
  }
  if (event.citation !== undefined) {
    return formatFrame('citation', event.citation);
  }
  if (event.agentStep !== undefined) {
    return formatFrame('agent-step', event.agentStep);
  }
  if (event.toolCall !== undefined) {
    return formatFrame('tool-call', event.toolCall);
  }
  if (event.error !== undefined) {
    return formatFrame('error', event.error);
  }
  if (event.done !== undefined) {
    return formatFrame('done', event.done);
  }
  return null;
}

/**
 * Convenience used by the controller's error path: synthesise a
 * `urn:apf-ai:relay_error` event frame so the SPA receives a
 * structured failure rather than a torn-down connection. Matches the
 * shape of the AI service's own `ErrorEvent` so the SPA's renderer
 * does not need a second code path for relay-level failures vs
 * upstream model errors.
 */
export function relayErrorFrame(code: string, message: string, retriable = false): string {
  return formatFrame('error', { code, message, retriable });
}

function formatFrame(eventName: string, data: unknown): string {
  // SSE spec: every field line ends with `\n`, the frame is
  // terminated by a blank line (`\n\n`). `data:` is followed by a
  // single space convention. `JSON.stringify` produces no newlines
  // for plain objects, so a single `data:` line is correct; a
  // multi-line payload (not used by this codec) would require
  // splitting on `\n` and prefixing each part with `data:`.
  return `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
}
