import type { ChatEvent, MessageCost } from '../types';

// Translates OpenCode's SSE events into our internal ChatEvent union — the OpenCode analog of
// sidecar/claude/stream-parser.ts. Shapes verified live against opencode 1.17.9's `GET /event`:
//
//   message.part.updated { part:{ id, type } }                        → learn partID→type
//   message.part.delta { partID, delta }                              → delta (only if partID is a text part)
//   message.part.updated { part:{ type:'tool', callID, tool, state } } → tool:start / tool:result
//   message.updated { info:{ role:'assistant', cost, tokens } }        → stash final cost
//   session.idle                                                       → result (turn complete)
//   session.error                                                      → error
//
// IMPORTANT: deltas always carry `field:'text'` even for the model's *reasoning* — reasoning and answer
// are only distinguishable by the delta's part TYPE (a message.part.updated declaring the part as
// `reasoning` vs `text` always precedes that part's deltas). So we gate deltas on partID being a `text`
// part; reasoning-part deltas are dropped (parity with the Claude harness, which hides thinking).
// createEventHandler flushes the assistant text buffer on tool:start and result, so no explicit `text`
// event is needed — the streamed answer deltas are enough.

export type OpenCodeEvent = {
  id?: string;
  type: string;
  properties?: Record<string, unknown>;
};

type ToolState = {
  status?: string;
  input?: Record<string, unknown>;
  output?: string;
  error?: string;
};

type OpenCodePart = {
  id?: string;
  type?: string;
  callID?: string;
  tool?: string;
  field?: string;
  state?: ToolState;
};

type AssistantInfo = {
  role?: string;
  cost?: number;
  tokens?: { input?: number; output?: number };
};

/**
 * Returns a stateful handler that maps raw OpenCode events to ChatEvents, invoking `onEvent` for each.
 * State tracks per-callID tool progress (to emit start/result exactly once) and the latest assistant
 * cost/tokens (emitted with the terminal `result`).
 */
export function createEventMapper(onEvent: (event: ChatEvent) => void) {
  const toolStarted = new Set<string>();
  const toolFinished = new Set<string>();
  const partTypes = new Map<string, string>(); // partID → type (declared before that part's deltas)
  let cost: MessageCost = { inputTokens: 0, outputTokens: 0, totalUSD: 0 };
  let done = false;

  const finish = (event: ChatEvent) => {
    if (done) return;
    done = true;
    onEvent(event);
  };

  return function handle(evt: OpenCodeEvent): void {
    const p = evt.properties ?? {};

    switch (evt.type) {
      case 'message.part.delta': {
        // Only stream deltas belonging to a `text` part — reasoning parts also emit field:'text' deltas.
        const partID = p.partID as string | undefined;
        if (partID && partTypes.get(partID) === 'text' && typeof p.delta === 'string' && p.delta.length > 0) {
          onEvent({ type: 'delta', text: p.delta });
        }
        return;
      }

      case 'message.part.updated': {
        const part = p.part as OpenCodePart | undefined;
        if (!part) return;
        if (part.id && part.type) partTypes.set(part.id, part.type);
        if (part.type !== 'tool' || !part.callID) return;
        const callID = part.callID;
        const status = part.state?.status;

        if ((status === 'running' || status === 'completed' || status === 'error') && !toolStarted.has(callID)) {
          toolStarted.add(callID);
          onEvent({
            type: 'tool:start',
            toolCallId: callID,
            toolName: part.tool ?? 'tool',
            toolInput: part.state?.input ?? {},
          });
        }

        if (status === 'completed' && !toolFinished.has(callID)) {
          toolFinished.add(callID);
          onEvent({ type: 'tool:result', toolCallId: callID, output: String(part.state?.output ?? ''), isError: false });
        } else if (status === 'error' && !toolFinished.has(callID)) {
          toolFinished.add(callID);
          onEvent({
            type: 'tool:result',
            toolCallId: callID,
            output: String(part.state?.error ?? 'Tool failed'),
            isError: true,
          });
        }
        return;
      }

      case 'message.updated': {
        const info = ((p.info as AssistantInfo | undefined) ?? (p as AssistantInfo)) ?? {};
        if (info.role === 'assistant' && info.tokens) {
          cost = {
            inputTokens: info.tokens.input ?? 0,
            outputTokens: info.tokens.output ?? 0,
            totalUSD: info.cost ?? 0,
          };
        }
        return;
      }

      case 'session.idle': {
        finish({ type: 'result', cost });
        return;
      }

      case 'session.error': {
        const error = p.error;
        const message = typeof error === 'string' ? error : ((error as { message?: string })?.message ?? 'OpenCode error');
        finish({ type: 'error', message });
        return;
      }

      default:
        return;
    }
  };
}
