import type { ChatEvent, TurnMessage } from '../../api/chat/types';

// Translation from a harness's ChatEvent stream to the browser-facing turn messages, moved here from the
// main server (it was `createEventHandler` in `chat/websocket.ts`, once per harness). Both the agent and
// the opencode sidecar use this one copy. It lives with the process that produces the
// stream because it is stateful: `delta` events accumulate into a buffer that has to be flushed as one
// `assistant:text` at the next boundary (a tool call, or the end of the turn). A consumer downstream of
// a socket that can drop cannot hold that state correctly.
//
// Pure apart from the buffer, and the buffer is the whole of it — which is what makes it testable.

export type TurnOutput = {
  msg: TurnMessage;
  /** false only for `assistant:delta`: superseded by the `assistant:text` that follows, never persisted. */
  durable: boolean;
};

export type TurnStream = {
  push: (event: ChatEvent) => TurnOutput[];
};

export function createTurnStream(sessionId: string): TurnStream {
  // One buffer per speaker, keyed by the Task tool call that owns it ('' for the main agent). A subagent
  // runs concurrently with the agent that spawned it, so a single buffer interleaved their sentences and
  // flushed the result as one message attributed to whoever happened to hit the next boundary.
  const buffers = new Map<string, string>();

  /** Only set the field when there IS a parent, so main-agent messages stay byte-identical on the wire. */
  const parented = (parent: string) => (parent ? { parentToolUseId: parent } : {});

  // Emit whatever deltas have accumulated as one complete message. Called at every boundary where that
  // speaker stops talking, so the transcript holds text blocks rather than a thousand fragments.
  function flush(parent: string): TurnOutput[] {
    const text = buffers.get(parent);
    if (!text) return [];
    buffers.delete(parent);
    return [{ msg: { type: 'assistant:text', text, ...parented(parent) }, durable: true }];
  }

  /** A turn's end invalidates every buffer, the subagents' included. */
  function flushAll(): TurnOutput[] {
    return [...buffers.keys()].flatMap(flush);
  }

  function push(event: ChatEvent): TurnOutput[] {
    const parent = ('parentToolUseId' in event ? event.parentToolUseId : undefined) ?? '';

    switch (event.type) {
      case 'session':
        // Not durable: the id names the log rather than belonging in it, and a client replaying the log
        // had to know the id to ask for it. Persisting it would put a second copy of the answer inside
        // the question.
        return [{ msg: { type: 'session:claude', claudeSessionId: event.claudeSessionId }, durable: false }];

      case 'delta':
        buffers.set(parent, (buffers.get(parent) ?? '') + event.text);
        return [{ msg: { type: 'assistant:delta', text: event.text, ...parented(parent) }, durable: false }];

      case 'text': {
        // An explicit full text block wins over the accumulated deltas that produced it.
        const text = event.text || buffers.get(parent) || '';
        buffers.delete(parent);
        return text ? [{ msg: { type: 'assistant:text', text, ...parented(parent) }, durable: true }] : [];
      }

      case 'tool:start':
        return [
          ...flush(parent),
          {
            msg: {
              type: 'tool:start',
              toolCallId: event.toolCallId,
              toolName: event.toolName,
              toolInput: event.toolInput,
              ...parented(parent),
            },
            durable: true,
          },
        ];

      case 'tool:result':
        return [
          {
            msg: {
              type: 'tool:result',
              toolCallId: event.toolCallId,
              output: event.output,
              isError: event.isError,
              ...parented(parent),
            },
            durable: true,
          },
        ];

      case 'result':
        return [
          ...flushAll(),
          {
            msg: { type: 'result', sessionId, cost: event.cost, claudeSessionId: event.claudeSessionId },
            durable: true,
          },
        ];

      case 'error':
        return [{ msg: { type: 'error', message: event.message }, durable: true }];

      case 'stopped':
        return [{ msg: { type: 'stopped' }, durable: true }];

      case 'task:started':
        return [
          {
            msg: {
              type: 'task:started',
              taskId: event.taskId,
              description: event.description,
              taskType: event.taskType,
            },
            durable: true,
          },
        ];

      case 'task:notification':
        return [
          {
            msg: { type: 'task:notification', taskId: event.taskId, status: event.status, summary: event.summary },
            durable: true,
          },
        ];

      case 'compact:start':
        // Flushed first: whatever the agent had said before compaction began is finished text, and the
        // notice belongs after it rather than in the middle of a paragraph it interrupted.
        return [...flushAll(), { msg: { type: 'compact:start', trigger: event.trigger }, durable: true }];

      case 'compact:done':
        return [
          {
            msg: {
              type: 'compact:done',
              trigger: event.trigger,
              preTokens: event.preTokens,
              durationMs: event.durationMs,
            },
            durable: true,
          },
        ];
    }
  }

  return { push };
}
