/**
 * Claude Code NDJSON Stream Parser
 *
 * Parses the stream-json output format from Claude Code CLI into ChatEvents.
 * Handles: stream_event (deltas), assistant (text/tool_use), user (tool_result),
 * system init, and result messages.
 */

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

type SessionCallback = (sessionId: string) => void;

type StreamParserCallbacks = {
  onEvent: (event: ChatEvent) => void;
  onSessionId: SessionCallback;
};

type ParseState = {
  textBuffer: string;
  gotResult: boolean;
};

function flushTextBuffer(state: ParseState, onEvent: (event: ChatEvent) => void): void {
  if (state.textBuffer) {
    onEvent({ type: 'text', text: state.textBuffer });
    state.textBuffer = '';
  }
}

function handleStreamEvent(msg: Record<string, unknown>, state: ParseState, onEvent: (event: ChatEvent) => void): void {
  const event = msg.event as Record<string, unknown> | undefined;
  if (event?.type === 'content_block_delta') {
    const delta = event.delta as Record<string, unknown> | undefined;
    if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
      state.textBuffer += delta.text;
      onEvent({ type: 'delta', text: delta.text });
    }
  }
}

function handleAssistant(msg: Record<string, unknown>, state: ParseState, onEvent: (event: ChatEvent) => void): void {
  const message = msg.message as Record<string, unknown> | undefined;
  const content = message?.content as Array<Record<string, unknown>> | undefined;
  if (!Array.isArray(content)) return;

  for (const block of content) {
    if (block.type === 'text' && typeof block.text === 'string') {
      onEvent({ type: 'text', text: block.text });
      state.textBuffer = '';
    } else if (block.type === 'tool_use') {
      flushTextBuffer(state, onEvent);
      onEvent({
        type: 'tool:start',
        toolCallId: (block.id as string) ?? '',
        toolName: (block.name as string) ?? 'unknown',
        toolInput: (block.input as Record<string, unknown>) ?? {},
      });
    }
  }
}

function handleUser(msg: Record<string, unknown>, onEvent: (event: ChatEvent) => void): void {
  const message = msg.message as Record<string, unknown> | undefined;
  const content = message?.content as Array<Record<string, unknown>> | undefined;
  if (!Array.isArray(content)) return;

  for (const block of content) {
    if (block.type === 'tool_result') {
      let output = '';
      if (typeof block.content === 'string') {
        output = block.content;
      } else if (Array.isArray(block.content)) {
        output = (block.content as Array<Record<string, unknown>>)
          .filter((c) => c.type === 'text')
          .map((c) => c.text as string)
          .join('\n');
      }
      onEvent({
        type: 'tool:result',
        toolCallId: (block.tool_use_id as string) ?? '',
        output,
        isError: (block.is_error as boolean) ?? false,
      });
    }
  }
}

function handleResult(
  msg: Record<string, unknown>,
  state: ParseState,
  callbacks: StreamParserCallbacks,
): void {
  state.gotResult = true;

  const isError = (msg.is_error as boolean) ?? false;
  const resultText = (msg.result as string) ?? '';

  if (isError) {
    flushTextBuffer(state, callbacks.onEvent);
    callbacks.onEvent({ type: 'error', message: resultText || 'Claude Code returned an error' });
    return;
  }

  flushTextBuffer(state, callbacks.onEvent);

  const usage = msg.usage as Record<string, number> | undefined;
  const cost: MessageCost = {
    inputTokens: usage?.input_tokens ?? 0,
    outputTokens: usage?.output_tokens ?? 0,
    totalUSD: (msg.total_cost_usd as number) ?? 0,
  };

  const sessionId = msg.session_id as string | undefined;
  if (sessionId) {
    callbacks.onSessionId(sessionId);
  }

  callbacks.onEvent({ type: 'result', cost });
}

function handleSystem(msg: Record<string, unknown>, callbacks: StreamParserCallbacks): void {
  const subtype = msg.subtype as string | undefined;
  if (subtype === 'init') {
    const sessionId = msg.session_id as string | undefined;
    if (sessionId) callbacks.onSessionId(sessionId);
  } else if (subtype === 'task_started') {
    callbacks.onEvent({
      type: 'task:started',
      taskId: (msg.task_id as string) ?? '',
      description: (msg.description as string) ?? '',
      taskType: msg.task_type as string | undefined,
    });
  } else if (subtype === 'task_notification') {
    callbacks.onEvent({
      type: 'task:notification',
      taskId: (msg.task_id as string) ?? '',
      status: (msg.status as 'completed' | 'failed' | 'stopped') ?? 'completed',
      summary: (msg.summary as string) ?? '',
    });
  }
}

/**
 * Map a single already-parsed message object (a Claude Code stream-json message, or the equivalent
 * Agent SDK message — same shapes) to ChatEvents. Returns false if the message type was ignored.
 */
export function processMessage(
  msg: Record<string, unknown>,
  state: ParseState,
  callbacks: StreamParserCallbacks,
): boolean {
  const type = msg.type as string;

  switch (type) {
    case 'stream_event':
      handleStreamEvent(msg, state, callbacks.onEvent);
      break;
    case 'assistant':
      handleAssistant(msg, state, callbacks.onEvent);
      break;
    case 'user':
      handleUser(msg, callbacks.onEvent);
      break;
    case 'system':
      handleSystem(msg, callbacks);
      break;
    case 'result':
      handleResult(msg, state, callbacks);
      break;
    default:
      return false;
  }

  return true;
}

/**
 * Process a single NDJSON line from Claude Code's stream output.
 * Returns false if the line was skipped (empty or malformed), true otherwise.
 */
export function processLine(
  line: string,
  state: ParseState,
  callbacks: StreamParserCallbacks,
): boolean {
  if (!line.trim()) return false;

  let msg: Record<string, unknown>;
  try {
    msg = JSON.parse(line) as Record<string, unknown>;
  } catch {
    return false;
  }

  return processMessage(msg, state, callbacks);
}

/**
 * Create a fresh parse state for a new stream.
 */
export function createParseState(): ParseState {
  return { textBuffer: '', gotResult: false };
}

/**
 * Read an NDJSON stream and process each line.
 * Returns the final parse state.
 */
export async function parseStream(
  stdout: ReadableStream<Uint8Array>,
  callbacks: StreamParserCallbacks,
): Promise<ParseState> {
  const reader = stdout.getReader();
  const decoder = new TextDecoder();
  const state = createParseState();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value, { stream: true });
    buffer += chunk;
    const lines = buffer.split('\n');
    buffer = lines.pop()!;
    for (const line of lines) {
      processLine(line, state, callbacks);
    }
  }

  if (buffer.trim()) {
    processLine(buffer, state, callbacks);
  }

  return state;
}

export type { ParseState, StreamParserCallbacks };
