/**
 * @license
 * Copyright 2025 Qwen Team
 * SPDX-License-Identifier: Apache-2.0
 */

import { randomUUID } from 'node:crypto';
import type {
  Config,
  ToolCallRequestInfo,
  ToolCallResponseInfo,
  SessionMetrics,
  ServerGeminiStreamEvent,
  TaskResultDisplay,
} from '@qwen-code/qwen-code-core';
import { GeminiEventType, ToolErrorType } from '@qwen-code/qwen-code-core';
import type { Part, GenerateContentResponseUsageMetadata } from '@google/genai';
import type {
  CLIAssistantMessage,
  CLIMessage,
  CLIPermissionDenial,
  CLIResultMessage,
  CLIResultMessageError,
  CLIResultMessageSuccess,
  CLIUserMessage,
  ContentBlock,
  ExtendedUsage,
  TextBlock,
  ThinkingBlock,
  ToolResultBlock,
  ToolUseBlock,
  Usage,
} from '../types.js';
import { functionResponsePartsToString } from '../../utils/nonInteractiveHelpers.js';

/**
 * Internal state for managing a single message context (main agent or subagent).
 */
export interface MessageState {
  messageId: string | null;
  blocks: ContentBlock[];
  openBlocks: Set<number>;
  usage: Usage;
  messageStarted: boolean;
  finalized: boolean;
  currentBlockType: ContentBlock['type'] | null;
}

/**
 * Options for building result messages.
 * Used by both streaming and non-streaming JSON output adapters.
 */
export interface ResultOptions {
  readonly isError: boolean;
  readonly errorMessage?: string;
  readonly durationMs: number;
  readonly apiDurationMs: number;
  readonly numTurns: number;
  readonly usage?: ExtendedUsage;
  readonly stats?: SessionMetrics;
  readonly summary?: string;
  readonly subtype?: string;
}

/**
 * Interface for message emission strategies.
 * Implementations decide whether to emit messages immediately (streaming)
 * or collect them for batch emission (non-streaming).
 * This interface defines the common message emission methods that
 * all JSON output adapters should implement.
 */
export interface MessageEmitter {
  emitMessage(message: CLIMessage): void;
  emitUserMessage(parts: Part[], parentToolUseId?: string | null): void;
  emitToolResult(
    request: ToolCallRequestInfo,
    response: ToolCallResponseInfo,
    parentToolUseId?: string | null,
  ): void;
  emitSystemMessage(subtype: string, data?: unknown): void;
}

/**
 * JSON-focused output adapter interface.
 * Handles structured JSON output for both streaming and non-streaming modes.
 * This interface defines the complete API that all JSON output adapters must implement.
 */
export interface JsonOutputAdapterInterface extends MessageEmitter {
  startAssistantMessage(): void;
  processEvent(event: ServerGeminiStreamEvent): void;
  finalizeAssistantMessage(): CLIAssistantMessage;
  emitResult(options: ResultOptions): void;

  startSubagentAssistantMessage?(parentToolUseId: string): void;
  processSubagentToolCall?(
    toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
    parentToolUseId: string,
  ): void;
  finalizeSubagentAssistantMessage?(
    parentToolUseId: string,
  ): CLIAssistantMessage;
  emitSubagentErrorResult?(
    errorMessage: string,
    numTurns: number,
    parentToolUseId: string,
  ): void;

  getSessionId(): string;
  getModel(): string;
}

/**
 * Abstract base class for JSON output adapters.
 * Contains shared logic for message building, state management, and content block handling.
 */
export abstract class BaseJsonOutputAdapter {
  protected readonly config: Config;

  // Main agent message state
  protected mainAgentMessageState: MessageState;

  // Subagent message states keyed by parentToolUseId
  protected subagentMessageStates = new Map<string, MessageState>();

  // Last assistant message for result generation
  protected lastAssistantMessage: CLIAssistantMessage | null = null;

  // Track permission denials (execution denied tool calls)
  protected permissionDenials: CLIPermissionDenial[] = [];

  constructor(config: Config) {
    this.config = config;
    this.mainAgentMessageState = this.createMessageState();
  }

  /**
   * Creates a new message state with default values.
   */
  protected createMessageState(): MessageState {
    return {
      messageId: null,
      blocks: [],
      openBlocks: new Set<number>(),
      usage: this.createUsage(),
      messageStarted: false,
      finalized: false,
      currentBlockType: null,
    };
  }

  /**
   * Gets or creates message state for a given context.
   *
   * @param parentToolUseId - null for main agent, string for subagent
   * @returns MessageState for the context
   */
  protected getMessageState(parentToolUseId: string | null): MessageState {
    if (parentToolUseId === null) {
      return this.mainAgentMessageState;
    }

    let state = this.subagentMessageStates.get(parentToolUseId);
    if (!state) {
      state = this.createMessageState();
      this.subagentMessageStates.set(parentToolUseId, state);
    }
    return state;
  }

  /**
   * Creates a Usage object from metadata.
   *
   * @param metadata - Optional usage metadata from Gemini API
   * @returns Usage object
   */
  protected createUsage(
    metadata?: GenerateContentResponseUsageMetadata | null,
  ): Usage {
    const usage: Usage = {
      input_tokens: 0,
      output_tokens: 0,
    };

    if (!metadata) {
      return usage;
    }

    if (typeof metadata.promptTokenCount === 'number') {
      usage.input_tokens = metadata.promptTokenCount;
    }
    if (typeof metadata.candidatesTokenCount === 'number') {
      usage.output_tokens = metadata.candidatesTokenCount;
    }
    if (typeof metadata.cachedContentTokenCount === 'number') {
      usage.cache_read_input_tokens = metadata.cachedContentTokenCount;
    }
    if (typeof metadata.totalTokenCount === 'number') {
      usage.total_tokens = metadata.totalTokenCount;
    }

    return usage;
  }

  /**
   * Builds a CLIAssistantMessage from the current message state.
   *
   * @param parentToolUseId - null for main agent, string for subagent
   * @returns CLIAssistantMessage
   */
  protected buildMessage(parentToolUseId: string | null): CLIAssistantMessage {
    const state = this.getMessageState(parentToolUseId);

    if (!state.messageId) {
      throw new Error('Message not started');
    }

    // Enforce constraint: assistant message must contain only a single type of ContentBlock
    if (state.blocks.length > 0) {
      const blockTypes = new Set(state.blocks.map((block) => block.type));
      if (blockTypes.size > 1) {
        throw new Error(
          `Assistant message must contain only one type of ContentBlock, found: ${Array.from(blockTypes).join(', ')}`,
        );
      }
    }

    // Determine stop_reason based on content block types
    // If the message contains only tool_use blocks, set stop_reason to 'tool_use'
    const stopReason =
      state.blocks.length > 0 &&
      state.blocks.every((block) => block.type === 'tool_use')
        ? 'tool_use'
        : null;

    return {
      type: 'assistant',
      uuid: state.messageId,
      session_id: this.config.getSessionId(),
      parent_tool_use_id: parentToolUseId,
      message: {
        id: state.messageId,
        type: 'message',
        role: 'assistant',
        model: this.config.getModel(),
        content: state.blocks,
        stop_reason: stopReason,
        usage: state.usage,
      },
    };
  }

  /**
   * Finalizes pending blocks (text or thinking) by closing them.
   *
   * @param state - Message state to finalize blocks for
   * @param parentToolUseId - null for main agent, string for subagent (optional, defaults to null)
   */
  protected finalizePendingBlocks(
    state: MessageState,
    parentToolUseId?: string | null,
  ): void {
    const actualParentToolUseId = parentToolUseId ?? null;
    const lastBlock = state.blocks[state.blocks.length - 1];
    if (!lastBlock) {
      return;
    }

    if (lastBlock.type === 'text') {
      const index = state.blocks.length - 1;
      this.onBlockClosed(state, index, actualParentToolUseId);
      this.closeBlock(state, index);
    } else if (lastBlock.type === 'thinking') {
      const index = state.blocks.length - 1;
      this.onBlockClosed(state, index, actualParentToolUseId);
      this.closeBlock(state, index);
    }
  }

  /**
   * Opens a block (adds to openBlocks set).
   *
   * @param state - Message state
   * @param index - Block index
   * @param _block - Content block
   */
  protected openBlock(
    state: MessageState,
    index: number,
    _block: ContentBlock,
  ): void {
    state.openBlocks.add(index);
  }

  /**
   * Closes a block (removes from openBlocks set).
   *
   * @param state - Message state
   * @param index - Block index
   */
  protected closeBlock(state: MessageState, index: number): void {
    if (!state.openBlocks.has(index)) {
      return;
    }
    state.openBlocks.delete(index);
  }

  /**
   * Guarantees that a single assistant message aggregates only one
   * content block category (text, thinking, or tool use). When a new
   * block type is requested, the current message is finalized and a fresh
   * assistant message is started to honour the single-type constraint.
   *
   * @param state - Message state
   * @param targetType - Target block type
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected ensureBlockTypeConsistency(
    state: MessageState,
    targetType: ContentBlock['type'],
    parentToolUseId: string | null,
  ): void {
    if (state.currentBlockType === targetType) {
      return;
    }

    if (state.currentBlockType === null) {
      state.currentBlockType = targetType;
      return;
    }

    // Finalize current message and start new one
    this.finalizeAssistantMessageInternal(state, parentToolUseId);
    this.startAssistantMessageInternal(state);
    state.currentBlockType = targetType;
  }

  /**
   * Starts a new assistant message, resetting state.
   *
   * @param state - Message state to reset
   */
  protected startAssistantMessageInternal(state: MessageState): void {
    state.messageId = randomUUID();
    state.blocks = [];
    state.openBlocks = new Set<number>();
    state.usage = this.createUsage();
    state.messageStarted = false;
    state.finalized = false;
    state.currentBlockType = null;
  }

  /**
   * Finalizes an assistant message.
   *
   * @param state - Message state to finalize
   * @param parentToolUseId - null for main agent, string for subagent
   * @returns CLIAssistantMessage
   */
  protected finalizeAssistantMessageInternal(
    state: MessageState,
    parentToolUseId: string | null,
  ): CLIAssistantMessage {
    if (state.finalized) {
      return this.buildMessage(parentToolUseId);
    }
    state.finalized = true;

    this.finalizePendingBlocks(state, parentToolUseId);
    const orderedOpenBlocks = Array.from(state.openBlocks).sort(
      (a, b) => a - b,
    );
    for (const index of orderedOpenBlocks) {
      this.onBlockClosed(state, index, parentToolUseId);
      this.closeBlock(state, index);
    }

    const message = this.buildMessage(parentToolUseId);
    this.emitMessageImpl(message);
    return message;
  }

  /**
   * Abstract method for emitting messages. Implementations decide whether
   * to emit immediately (streaming) or collect for batch emission.
   * Note: The message object already contains parent_tool_use_id field,
   * so it doesn't need to be passed as a separate parameter.
   *
   * @param message - Message to emit (already contains parent_tool_use_id if applicable)
   */
  protected abstract emitMessageImpl(message: CLIMessage): void;

  /**
   * Abstract method to determine if stream events should be emitted.
   *
   * @returns true if stream events should be emitted
   */
  protected abstract shouldEmitStreamEvents(): boolean;

  /**
   * Hook method called when a text block is created.
   * Subclasses can override this to emit stream events.
   *
   * @param state - Message state
   * @param index - Block index
   * @param block - Text block that was created
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onTextBlockCreated(
    _state: MessageState,
    _index: number,
    _block: TextBlock,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Hook method called when text content is appended.
   * Subclasses can override this to emit stream events.
   *
   * @param state - Message state
   * @param index - Block index
   * @param fragment - Text fragment that was appended
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onTextAppended(
    _state: MessageState,
    _index: number,
    _fragment: string,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Hook method called when a thinking block is created.
   * Subclasses can override this to emit stream events.
   *
   * @param state - Message state
   * @param index - Block index
   * @param block - Thinking block that was created
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onThinkingBlockCreated(
    _state: MessageState,
    _index: number,
    _block: ThinkingBlock,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Hook method called when thinking content is appended.
   * Subclasses can override this to emit stream events.
   *
   * @param state - Message state
   * @param index - Block index
   * @param fragment - Thinking fragment that was appended
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onThinkingAppended(
    _state: MessageState,
    _index: number,
    _fragment: string,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Hook method called when a tool_use block is created.
   * Subclasses can override this to emit stream events.
   *
   * @param state - Message state
   * @param index - Block index
   * @param block - Tool use block that was created
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onToolUseBlockCreated(
    _state: MessageState,
    _index: number,
    _block: ToolUseBlock,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Hook method called when tool_use input is set.
   * Subclasses can override this to emit stream events.
   *
   * @param state - Message state
   * @param index - Block index
   * @param input - Tool use input that was set
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onToolUseInputSet(
    _state: MessageState,
    _index: number,
    _input: unknown,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Hook method called when a block is closed.
   * Subclasses can override this to emit stream events.
   *
   * @param state - Message state
   * @param index - Block index
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onBlockClosed(
    _state: MessageState,
    _index: number,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Hook method called to ensure message is started.
   * Subclasses can override this to emit message_start events.
   *
   * @param state - Message state
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected onEnsureMessageStarted(
    _state: MessageState,
    _parentToolUseId: string | null,
  ): void {
    // Default implementation does nothing
  }

  /**
   * Gets the session ID from config.
   *
   * @returns Session ID
   */
  getSessionId(): string {
    return this.config.getSessionId();
  }

  /**
   * Gets the model name from config.
   *
   * @returns Model name
   */
  getModel(): string {
    return this.config.getModel();
  }

  // ========== Main Agent APIs ==========

  /**
   * Starts a new assistant message for the main agent.
   * This is a shared implementation used by both streaming and non-streaming adapters.
   */
  startAssistantMessage(): void {
    this.startAssistantMessageInternal(this.mainAgentMessageState);
  }

  /**
   * Processes a stream event from the Gemini API.
   * This is a shared implementation used by both streaming and non-streaming adapters.
   *
   * @param event - Stream event from Gemini API
   */
  processEvent(event: ServerGeminiStreamEvent): void {
    const state = this.mainAgentMessageState;
    if (state.finalized) {
      return;
    }

    switch (event.type) {
      case GeminiEventType.Content:
        this.appendText(state, event.value, null);
        break;
      case GeminiEventType.Citation:
        if (typeof event.value === 'string') {
          this.appendText(state, `\n${event.value}`, null);
        }
        break;
      case GeminiEventType.Thought:
        this.appendThinking(
          state,
          event.value.subject,
          event.value.description,
          null,
        );
        break;
      case GeminiEventType.ToolCallRequest:
        this.appendToolUse(state, event.value, null);
        break;
      case GeminiEventType.Finished:
        if (event.value?.usageMetadata) {
          state.usage = this.createUsage(event.value.usageMetadata);
        }
        this.finalizePendingBlocks(state, null);
        break;
      default:
        break;
    }
  }

  // ========== Subagent APIs ==========

  /**
   * Starts a new assistant message for a subagent.
   * This is a shared implementation used by both streaming and non-streaming adapters.
   *
   * @param parentToolUseId - Parent tool use ID
   */
  startSubagentAssistantMessage(parentToolUseId: string): void {
    const state = this.getMessageState(parentToolUseId);
    this.startAssistantMessageInternal(state);
  }

  /**
   * Finalizes a subagent assistant message.
   * This is a shared implementation used by both streaming and non-streaming adapters.
   *
   * @param parentToolUseId - Parent tool use ID
   * @returns CLIAssistantMessage
   */
  finalizeSubagentAssistantMessage(
    parentToolUseId: string,
  ): CLIAssistantMessage {
    const state = this.getMessageState(parentToolUseId);
    const message = this.finalizeAssistantMessageInternal(
      state,
      parentToolUseId,
    );
    this.updateLastAssistantMessage(message);
    return message;
  }

  /**
   * Emits a subagent error result message.
   * This is a shared implementation used by both streaming and non-streaming adapters.
   *
   * @param errorMessage - Error message
   * @param numTurns - Number of turns
   * @param parentToolUseId - Parent tool use ID
   */
  emitSubagentErrorResult(
    errorMessage: string,
    numTurns: number,
    parentToolUseId: string,
  ): void {
    const state = this.getMessageState(parentToolUseId);
    // Finalize any pending assistant message
    if (state.messageStarted && !state.finalized) {
      this.finalizeSubagentAssistantMessage(parentToolUseId);
    }

    const errorResult = this.buildSubagentErrorResult(errorMessage, numTurns);
    this.emitMessageImpl(errorResult);
  }

  /**
   * Processes a subagent tool call.
   * This is a shared implementation used by both streaming and non-streaming adapters.
   * Uses template method pattern with hooks for stream events.
   *
   * @param toolCall - Tool call information
   * @param parentToolUseId - Parent tool use ID
   */
  processSubagentToolCall(
    toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
    parentToolUseId: string,
  ): void {
    const state = this.getMessageState(parentToolUseId);

    // Finalize any pending text message before starting tool_use
    const hasText =
      state.blocks.some((b) => b.type === 'text') ||
      (state.currentBlockType === 'text' && state.blocks.length > 0);
    if (hasText) {
      this.finalizeSubagentAssistantMessage(parentToolUseId);
      this.startSubagentAssistantMessage(parentToolUseId);
    }

    // Ensure message is started before appending tool_use
    if (!state.messageId || !state.messageStarted) {
      this.startAssistantMessageInternal(state);
    }

    this.ensureBlockTypeConsistency(state, 'tool_use', parentToolUseId);
    this.ensureMessageStarted(state, parentToolUseId);
    this.finalizePendingBlocks(state, parentToolUseId);

    const { index } = this.createSubagentToolUseBlock(
      state,
      toolCall,
      parentToolUseId,
    );

    // Process tool use block creation and closure
    // Subclasses can override hook methods to emit stream events
    this.processSubagentToolUseBlock(state, index, toolCall, parentToolUseId);

    // Finalize tool_use message immediately
    this.finalizeSubagentAssistantMessage(parentToolUseId);
    this.startSubagentAssistantMessage(parentToolUseId);
  }

  /**
   * Processes a tool use block for subagent.
   * This method is called by processSubagentToolCall to handle tool use block creation,
   * input setting, and closure. Subclasses can override this to customize behavior.
   *
   * @param state - Message state
   * @param index - Block index
   * @param toolCall - Tool call information
   * @param parentToolUseId - Parent tool use ID
   */
  protected processSubagentToolUseBlock(
    state: MessageState,
    index: number,
    toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
    parentToolUseId: string,
  ): void {
    // Emit tool_use block creation event (with empty input)
    const startBlock: ToolUseBlock = {
      type: 'tool_use',
      id: toolCall.callId,
      name: toolCall.name,
      input: {},
    };
    this.onToolUseBlockCreated(state, index, startBlock, parentToolUseId);
    this.onToolUseInputSet(state, index, toolCall.args ?? {}, parentToolUseId);
    this.onBlockClosed(state, index, parentToolUseId);
    this.closeBlock(state, index);
  }

  /**
   * Updates the last assistant message.
   * Subclasses can override this to customize tracking behavior.
   *
   * @param message - Assistant message to track
   */
  protected updateLastAssistantMessage(message: CLIAssistantMessage): void {
    this.lastAssistantMessage = message;
  }

  // ========== Shared Content Block Methods ==========

  /**
   * Appends text content to the current message.
   * Uses template method pattern with hooks for stream events.
   *
   * @param state - Message state
   * @param fragment - Text fragment to append
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected appendText(
    state: MessageState,
    fragment: string,
    parentToolUseId: string | null,
  ): void {
    if (fragment.length === 0) {
      return;
    }

    this.ensureBlockTypeConsistency(state, 'text', parentToolUseId);
    this.ensureMessageStarted(state, parentToolUseId);

    let current = state.blocks[state.blocks.length - 1] as
      | TextBlock
      | undefined;
    const isNewBlock = !current || current.type !== 'text';
    if (isNewBlock) {
      current = { type: 'text', text: '' } satisfies TextBlock;
      const index = state.blocks.length;
      state.blocks.push(current);
      this.openBlock(state, index, current);
      this.onTextBlockCreated(state, index, current, parentToolUseId);
    }

    // current is guaranteed to be defined here (either existing or newly created)
    current!.text += fragment;
    const index = state.blocks.length - 1;
    this.onTextAppended(state, index, fragment, parentToolUseId);
  }

  /**
   * Appends thinking content to the current message.
   * Uses template method pattern with hooks for stream events.
   *
   * @param state - Message state
   * @param subject - Thinking subject
   * @param description - Thinking description
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected appendThinking(
    state: MessageState,
    subject?: string,
    description?: string,
    parentToolUseId?: string | null,
  ): void {
    const actualParentToolUseId = parentToolUseId ?? null;
    const fragment = [subject?.trim(), description?.trim()]
      .filter((value) => value && value.length > 0)
      .join(': ');
    if (!fragment) {
      return;
    }

    this.ensureBlockTypeConsistency(state, 'thinking', actualParentToolUseId);
    this.ensureMessageStarted(state, actualParentToolUseId);

    let current = state.blocks[state.blocks.length - 1] as
      | ThinkingBlock
      | undefined;
    const isNewBlock = !current || current.type !== 'thinking';
    if (isNewBlock) {
      current = {
        type: 'thinking',
        thinking: '',
        signature: subject,
      } satisfies ThinkingBlock;
      const index = state.blocks.length;
      state.blocks.push(current);
      this.openBlock(state, index, current);
      this.onThinkingBlockCreated(state, index, current, actualParentToolUseId);
    }

    // current is guaranteed to be defined here (either existing or newly created)
    current!.thinking = `${current!.thinking ?? ''}${fragment}`;
    const index = state.blocks.length - 1;
    this.onThinkingAppended(state, index, fragment, actualParentToolUseId);
  }

  /**
   * Appends a tool_use block to the current message.
   * Uses template method pattern with hooks for stream events.
   *
   * @param state - Message state
   * @param request - Tool call request info
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected appendToolUse(
    state: MessageState,
    request: ToolCallRequestInfo,
    parentToolUseId: string | null,
  ): void {
    this.ensureBlockTypeConsistency(state, 'tool_use', parentToolUseId);
    this.ensureMessageStarted(state, parentToolUseId);
    this.finalizePendingBlocks(state, parentToolUseId);

    const index = state.blocks.length;
    const block: ToolUseBlock = {
      type: 'tool_use',
      id: request.callId,
      name: request.name,
      input: request.args,
    };
    state.blocks.push(block);
    this.openBlock(state, index, block);

    // Emit tool_use block creation event (with empty input)
    const startBlock: ToolUseBlock = {
      type: 'tool_use',
      id: request.callId,
      name: request.name,
      input: {},
    };
    this.onToolUseBlockCreated(state, index, startBlock, parentToolUseId);
    this.onToolUseInputSet(state, index, request.args ?? {}, parentToolUseId);

    this.onBlockClosed(state, index, parentToolUseId);
    this.closeBlock(state, index);
  }

  /**
   * Ensures that a message has been started.
   * Calls hook method for subclasses to emit message_start events.
   *
   * @param state - Message state
   * @param parentToolUseId - null for main agent, string for subagent
   */
  protected ensureMessageStarted(
    state: MessageState,
    parentToolUseId: string | null,
  ): void {
    if (state.messageStarted) {
      return;
    }
    state.messageStarted = true;
    this.onEnsureMessageStarted(state, parentToolUseId);
  }

  /**
   * Creates and adds a tool_use block to the state.
   * This is a shared helper method used by processSubagentToolCall implementations.
   *
   * @param state - Message state
   * @param toolCall - Tool call information
   * @param parentToolUseId - Parent tool use ID
   * @returns The created block and its index
   */
  protected createSubagentToolUseBlock(
    state: MessageState,
    toolCall: NonNullable<TaskResultDisplay['toolCalls']>[number],
    _parentToolUseId: string,
  ): { block: ToolUseBlock; index: number } {
    const index = state.blocks.length;
    const block: ToolUseBlock = {
      type: 'tool_use',
      id: toolCall.callId,
      name: toolCall.name,
      input: toolCall.args || {},
    };
    state.blocks.push(block);
    this.openBlock(state, index, block);
    return { block, index };
  }

  /**
   * Emits a user message.
   * @param parts - Array of Part objects
   * @param parentToolUseId - Optional parent tool use ID for subagent messages
   */
  emitUserMessage(parts: Part[], parentToolUseId?: string | null): void {
    const content = partsToContentBlock(parts);
    const message: CLIUserMessage = {
      type: 'user',
      uuid: randomUUID(),
      session_id: this.getSessionId(),
      parent_tool_use_id: parentToolUseId ?? null,
      message: {
        role: 'user',
        content,
      },
    };
    this.emitMessageImpl(message);
  }

  /**
   * Emits a tool result message.
   * Collects execution denied tool calls for inclusion in result messages.
   * @param request - Tool call request info
   * @param response - Tool call response info
   * @param parentToolUseId - Parent tool use ID (null for main agent)
   */
  emitToolResult(
    request: ToolCallRequestInfo,
    response: ToolCallResponseInfo,
    parentToolUseId: string | null = null,
  ): void {
    // Track permission denials (execution denied errors)
    if (
      response.error &&
      response.errorType === ToolErrorType.EXECUTION_DENIED
    ) {
      const denial: CLIPermissionDenial = {
        tool_name: request.name,
        tool_use_id: request.callId,
        tool_input: request.args,
      };
      this.permissionDenials.push(denial);
    }

    const block: ToolResultBlock = {
      type: 'tool_result',
      tool_use_id: request.callId,
      is_error: Boolean(response.error),
    };
    const content = toolResultContent(response);
    if (content !== undefined) {
      block.content = content;
    }

    const message: CLIUserMessage = {
      type: 'user',
      uuid: randomUUID(),
      session_id: this.getSessionId(),
      parent_tool_use_id: parentToolUseId,
      message: {
        role: 'user',
        content: [block],
      },
    };
    this.emitMessageImpl(message);
  }

  /**
   * Emits a system message.
   * @param subtype - System message subtype
   * @param data - Optional data payload
   */
  emitSystemMessage(subtype: string, data?: unknown): void {
    const systemMessage = {
      type: 'system',
      subtype,
      uuid: randomUUID(),
      session_id: this.getSessionId(),
      parent_tool_use_id: null,
      data,
    } as const;
    this.emitMessageImpl(systemMessage);
  }

  /**
   * Builds a result message from options.
   * Helper method used by both emitResult implementations.
   * Includes permission denials collected from execution denied tool calls.
   * @param options - Result options
   * @param lastAssistantMessage - Last assistant message for text extraction
   * @returns CLIResultMessage
   */
  protected buildResultMessage(
    options: ResultOptions,
    lastAssistantMessage: CLIAssistantMessage | null,
  ): CLIResultMessage {
    const usage = options.usage ?? createExtendedUsage();
    const resultText =
      options.summary ??
      (lastAssistantMessage
        ? extractTextFromBlocks(lastAssistantMessage.message.content)
        : '');

    const baseUuid = randomUUID();
    const baseSessionId = this.getSessionId();

    if (options.isError) {
      const errorMessage = options.errorMessage ?? 'Unknown error';
      return {
        type: 'result',
        subtype:
          (options.subtype as CLIResultMessageError['subtype']) ??
          'error_during_execution',
        uuid: baseUuid,
        session_id: baseSessionId,
        is_error: true,
        duration_ms: options.durationMs,
        duration_api_ms: options.apiDurationMs,
        num_turns: options.numTurns,
        usage,
        permission_denials: [...this.permissionDenials],
        error: { message: errorMessage },
      };
    } else {
      const success: CLIResultMessageSuccess & { stats?: SessionMetrics } = {
        type: 'result',
        subtype:
          (options.subtype as CLIResultMessageSuccess['subtype']) ?? 'success',
        uuid: baseUuid,
        session_id: baseSessionId,
        is_error: false,
        duration_ms: options.durationMs,
        duration_api_ms: options.apiDurationMs,
        num_turns: options.numTurns,
        result: resultText,
        usage,
        permission_denials: [...this.permissionDenials],
      };

      if (options.stats) {
        success.stats = options.stats;
      }

      return success;
    }
  }

  /**
   * Builds a subagent error result message.
   * Helper method used by both emitSubagentErrorResult implementations.
   * Note: Subagent permission denials are not included here as they are tracked
   * separately and would be included in the main agent's result message.
   * @param errorMessage - Error message
   * @param numTurns - Number of turns
   * @returns CLIResultMessageError
   */
  protected buildSubagentErrorResult(
    errorMessage: string,
    numTurns: number,
  ): CLIResultMessageError {
    const usage: ExtendedUsage = {
      input_tokens: 0,
      output_tokens: 0,
    };

    return {
      type: 'result',
      subtype: 'error_during_execution',
      uuid: randomUUID(),
      session_id: this.getSessionId(),
      is_error: true,
      duration_ms: 0,
      duration_api_ms: 0,
      num_turns: numTurns,
      usage,
      permission_denials: [],
      error: { message: errorMessage },
    };
  }
}

/**
 * Converts Part array to ContentBlock array.
 * Handles various Part types including text, functionResponse, and other types.
 * For functionResponse parts, extracts the output content.
 * For other non-text parts, converts them to text representation.
 *
 * @param parts - Array of Part objects
 * @returns Array of ContentBlock objects (primarily TextBlock)
 */
export function partsToContentBlock(parts: Part[]): ContentBlock[] {
  const blocks: ContentBlock[] = [];
  let currentTextBlock: TextBlock | null = null;

  for (const part of parts) {
    let textContent: string | null = null;

    // Handle text parts
    if ('text' in part && typeof part.text === 'string') {
      textContent = part.text;
    }
    // Handle functionResponse parts - extract output content
    else if ('functionResponse' in part && part.functionResponse) {
      const output =
        part.functionResponse.response?.['output'] ??
        part.functionResponse.response?.['content'] ??
        '';
      textContent =
        typeof output === 'string' ? output : JSON.stringify(output);
    }
    // Handle other part types - convert to JSON string
    else {
      textContent = JSON.stringify(part);
    }

    // If we have text content, add it to the current text block or create a new one
    if (textContent !== null && textContent.length > 0) {
      if (currentTextBlock === null) {
        currentTextBlock = {
          type: 'text',
          text: textContent,
        };
        blocks.push(currentTextBlock);
      } else {
        // Append to existing text block
        currentTextBlock.text += textContent;
      }
    }
  }

  // Return blocks array, or empty array if no content
  return blocks;
}

/**
 * Converts Part array to string representation.
 * This is a legacy function kept for backward compatibility.
 * For new code, prefer using partsToContentBlock.
 *
 * @param parts - Array of Part objects
 * @returns String representation
 */
export function partsToString(parts: Part[]): string {
  return parts
    .map((part) => {
      if ('text' in part && typeof part.text === 'string') {
        return part.text;
      }
      return JSON.stringify(part);
    })
    .join('');
}

/**
 * Extracts content from tool response.
 * Uses functionResponsePartsToString to properly handle functionResponse parts,
 * which correctly extracts output content from functionResponse objects rather
 * than simply concatenating text or JSON.stringify.
 *
 * @param response - Tool call response
 * @returns String content or undefined
 */
export function toolResultContent(
  response: ToolCallResponseInfo,
): string | undefined {
  if (response.error) {
    return response.error.message;
  }
  if (
    typeof response.resultDisplay === 'string' &&
    response.resultDisplay.trim().length > 0
  ) {
    return response.resultDisplay;
  }
  if (response.responseParts && response.responseParts.length > 0) {
    // Always use functionResponsePartsToString to properly handle
    // functionResponse parts that contain output content
    return functionResponsePartsToString(response.responseParts);
  }
  return undefined;
}

/**
 * Extracts text from content blocks.
 *
 * @param blocks - Array of content blocks
 * @returns Extracted text
 */
export function extractTextFromBlocks(blocks: ContentBlock[]): string {
  return blocks
    .filter((block) => block.type === 'text')
    .map((block) => (block.type === 'text' ? block.text : ''))
    .join('');
}

/**
 * Creates an extended usage object with default values.
 *
 * @returns ExtendedUsage object
 */
export function createExtendedUsage(): ExtendedUsage {
  return {
    input_tokens: 0,
    output_tokens: 0,
  };
}
