/**
 * Maps SDK messages to SSE events that the frontend understands.
 *
 * Sub-agent messages (those with parent_tool_use_id !== null) are routed
 * to 'agent_content' SSE events for structured rendering in AgentBlock.
 */

import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk'
import type { ErrorCategory, FinishReason } from '../../agent-types'

// SSE event types that the frontend (use-chat.ts) handles
export interface SseEvent {
  type: string
  [key: string]: unknown
}

// Per-agent streaming state (tool input accumulation, thinking)
interface AgentStreamState {
  currentToolId: string
  currentToolName: string
  inputJson: string
  inThinkingBlock: boolean
  thinkingText: string
  inTextBlock: boolean
  textAccumulator: string
}

// State tracker for the current streaming turn
export class MessageMapper {
  // ── Main-thread state ──
  private currentToolId = ''
  private currentToolName = ''
  private inputJson = ''
  private allBlocks: Record<string, unknown>[] = []
  private inThinkingBlock = false
  private thinkingText = ''
  private erroredToolUseIds = new Set<string>()
  private toolIdToName = new Map<string, string>()

  // ── Sub-agent state (keyed by parent_tool_use_id) ──
  private agentStreamState = new Map<string, AgentStreamState>()
  /** Accumulated sub-blocks per agent for DB persistence */
  private agentSubBlocks = new Map<string, Record<string, unknown>[]>()

  /** SDK-assigned session ID extracted from system init messages. */
  sdkSessionId: string | null = null
  /** Token usage from the final result message */
  inputTokens = 0
  outputTokens = 0
  resultSubtype: string | undefined
  finishReason: FinishReason | undefined
  totalCostUsd: number | undefined
  durationMs: number | undefined
  numTurns: number | undefined

  /**
   * Map a single SDK message to zero or more SSE events.
   */
  mapMessage(msg: SDKMessage): SseEvent[] {
    // Check for parent_tool_use_id to route sub-agent messages
    const parentId = ('parent_tool_use_id' in msg) ? (msg as Record<string, unknown>).parent_tool_use_id as string | null : null

    switch (msg.type) {
      case 'stream_event':
        if (parentId) return this.mapAgentStreamEvent(parentId, msg)
        return this.mapStreamEvent(msg)

      case 'assistant':
        if (parentId) return this.mapAgentAssistantMessage(parentId, msg)
        return this.mapAssistantMessage(msg)

      case 'tool_use_summary':
        return this.mapToolUseSummary(msg)

      case 'tool_progress':
        return this.mapToolProgress(msg)

      case 'result':
        if (parentId) return this.mapAgentResult(parentId, msg)
        return this.mapResult(msg)

      case 'system':
        return this.mapSystemMessage(msg)

      case 'user':
        if (parentId) return this.mapAgentUserMessage(parentId, msg)
        return this.mapUserMessage(msg)

      default:
        return []
    }
  }

  /**
   * Get all accumulated content blocks for DB persistence.
   */
  getBlocks(): Record<string, unknown>[] {
    // Build final blocks list: main blocks + agent_content blocks
    const result = [...this.allBlocks]
    for (const [parentId, subBlocks] of this.agentSubBlocks) {
      if (!result.some(b => b.type === 'agent_content' && b.parent_tool_use_id === parentId)) {
        result.push({
          type: 'agent_content',
          parent_tool_use_id: parentId,
          blocks: subBlocks,
        })
      }
    }
    return result
  }

  // ── Sub-agent stream event routing ──

  private getOrCreateAgentState(parentId: string): AgentStreamState {
    let state = this.agentStreamState.get(parentId)
    if (!state) {
      state = { currentToolId: '', currentToolName: '', inputJson: '', inThinkingBlock: false, thinkingText: '', inTextBlock: false, textAccumulator: '' }
      this.agentStreamState.set(parentId, state)
    }
    return state
  }

  private pushAgentSubBlock(parentId: string, block: Record<string, unknown>): void {
    let blocks = this.agentSubBlocks.get(parentId)
    if (!blocks) {
      blocks = []
      this.agentSubBlocks.set(parentId, blocks)
    }
    blocks.push(block)
  }

  private mapAgentStreamEvent(parentId: string, msg: Extract<SDKMessage, { type: 'stream_event' }>): SseEvent[] {
    const event = msg.event
    const state = this.getOrCreateAgentState(parentId)
    const events: SseEvent[] = []

    switch (event.type) {
      case 'content_block_start': {
        const block = event.content_block
        if (block.type === 'tool_use') {
          // Finalize any pending text block before starting tool
          if (state.inTextBlock && state.textAccumulator) {
            this.pushAgentSubBlock(parentId, { type: 'text', text: state.textAccumulator })
            state.inTextBlock = false
            state.textAccumulator = ''
          }
          state.currentToolId = block.id
          state.currentToolName = block.name
          state.inputJson = ''
        } else if (block.type === 'thinking') {
          // Finalize any pending text block before thinking
          if (state.inTextBlock && state.textAccumulator) {
            this.pushAgentSubBlock(parentId, { type: 'text', text: state.textAccumulator })
            state.inTextBlock = false
            state.textAccumulator = ''
          }
          state.inThinkingBlock = true
          state.thinkingText = ''
        } else if (block.type === 'text') {
          state.inTextBlock = true
          state.textAccumulator = ''
        }
        break
      }

      case 'content_block_delta': {
        const delta = event.delta
        if (delta.type === 'text_delta') {
          state.textAccumulator += delta.text
          events.push({
            type: 'agent_content',
            parent_tool_use_id: parentId,
            block_type: 'text_delta',
            text: delta.text,
          })
        } else if (delta.type === 'thinking_delta') {
          const text = (delta as unknown as Record<string, unknown>).thinking as string || ''
          state.thinkingText += text
          events.push({
            type: 'agent_content',
            parent_tool_use_id: parentId,
            block_type: 'thinking_delta',
            text,
          })
        } else if (delta.type === 'input_json_delta') {
          state.inputJson += delta.partial_json
        }
        break
      }

      case 'content_block_stop': {
        if (state.inThinkingBlock) {
          if (state.thinkingText) {
            const block = { type: 'thinking', text: state.thinkingText }
            this.pushAgentSubBlock(parentId, block)
            events.push({
              type: 'agent_content',
              parent_tool_use_id: parentId,
              block_type: 'thinking',
              text: state.thinkingText,
            })
          }
          state.inThinkingBlock = false
          state.thinkingText = ''
        } else if (state.currentToolId) {
          let parsedInput: Record<string, unknown> = {}
          try { parsedInput = JSON.parse(state.inputJson || '{}') } catch { /* empty */ }
          const block = {
            type: 'tool_use',
            id: state.currentToolId,
            name: state.currentToolName,
            input: parsedInput,
          }
          this.pushAgentSubBlock(parentId, block)
          this.toolIdToName.set(state.currentToolId, state.currentToolName)
          events.push({
            type: 'agent_content',
            parent_tool_use_id: parentId,
            block_type: 'tool_use',
            id: state.currentToolId,
            name: state.currentToolName,
            input: parsedInput,
          })
          state.currentToolId = ''
          state.currentToolName = ''
          state.inputJson = ''
        } else if (state.inTextBlock) {
          // Finalize text block — persist for DB storage
          if (state.textAccumulator) {
            this.pushAgentSubBlock(parentId, { type: 'text', text: state.textAccumulator })
          }
          state.inTextBlock = false
          state.textAccumulator = ''
        }
        break
      }
    }

    return events
  }

  /**
   * Extract sub-agent assistant message content (text, thinking, tool_use)
   * into agent_content events. This is the primary source of the agent's
   * "conversation flow" — what it said, thought, and which tools it called.
   */
  private mapAgentAssistantMessage(parentId: string, msg: Extract<SDKMessage, { type: 'assistant' }>): SseEvent[] {
    const events: SseEvent[] = []
    for (const block of msg.message.content) {
      if (block.type === 'text') {
        if (block.text) {
          this.pushAgentSubBlock(parentId, { type: 'text', text: block.text })
          events.push({
            type: 'agent_content',
            parent_tool_use_id: parentId,
            block_type: 'text',
            text: block.text,
          })
        }
      } else if (block.type === 'thinking') {
        const thinkBlock = block as { type: 'thinking'; thinking: string }
        if (thinkBlock.thinking) {
          this.pushAgentSubBlock(parentId, { type: 'thinking', text: thinkBlock.thinking })
          events.push({
            type: 'agent_content',
            parent_tool_use_id: parentId,
            block_type: 'thinking',
            text: thinkBlock.thinking,
          })
        }
      } else if (block.type === 'tool_use') {
        const toolBlock = block as { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }
        this.pushAgentSubBlock(parentId, {
          type: 'tool_use',
          id: toolBlock.id,
          name: toolBlock.name,
          input: toolBlock.input,
        })
        this.toolIdToName.set(toolBlock.id, toolBlock.name)
        events.push({
          type: 'agent_content',
          parent_tool_use_id: parentId,
          block_type: 'tool_use',
          id: toolBlock.id,
          name: toolBlock.name,
          input: toolBlock.input,
        })
      }
    }
    return events
  }

  private mapAgentUserMessage(parentId: string, msg: Extract<SDKMessage, { type: 'user' }>): SseEvent[] {
    const events: SseEvent[] = []
    const message = msg.message
    if ('content' in message && Array.isArray(message.content)) {
      for (const block of message.content) {
        if (typeof block !== 'object' || block === null || !('type' in block)) continue
        if (block.type === 'tool_result') {
          const toolResult = block as { type: 'tool_result'; tool_use_id: string; content?: unknown; is_error?: boolean }
          const content = typeof toolResult.content === 'string'
              ? toolResult.content
              : JSON.stringify(toolResult.content) || ''
          const truncatedContent = content.length > 3000 ? content.slice(0, 3000) + ' [truncated]' : content

          // Check if this tool_result is for a main-thread tool_use (e.g. Agent)
          // If so, also create a main-thread tool_result for DB persistence
          const isMainThreadTool = this.allBlocks.some(
              (b) => b.type === 'tool_use' && b.id === toolResult.tool_use_id
          )
          if (isMainThreadTool) {
            const toolName = this.toolIdToName.get(toolResult.tool_use_id) || ''
            const isAgentTool = toolName === 'Agent' || toolName === 'delegate_to_agent'

            // For Agent tool results: extract the agent's full text output as sub-blocks.
            // The content is an array like [{"type":"text","text":"full process text..."}].
            // This is the primary source of the agent's visible work process.
            let rawContent = toolResult.content
            if (typeof rawContent === 'string') {
              try { rawContent = JSON.parse(rawContent) } catch { /* not JSON */ }
            }

            if (isAgentTool && Array.isArray(rawContent)) {
              const contentBlocks = rawContent as { type: string; text?: string }[]
              const textParts: string[] = []
              for (const cb of contentBlocks) {
                if (cb.type === 'text' && cb.text) {
                  const cleanText = this.stripSdkMetadata(cb.text)
                  if (cleanText) {
                    this.pushAgentSubBlock(toolResult.tool_use_id, { type: 'text', text: cleanText })
                    events.push({
                      type: 'agent_content',
                      parent_tool_use_id: toolResult.tool_use_id,
                      block_type: 'text',
                      text: cleanText,
                    })
                    textParts.push(cleanText)
                  }
                }
              }
              const fullText = textParts.join('\n') || content
              if (!this.allBlocks.some((b) => b.type === 'tool_result' && b.tool_use_id === toolResult.tool_use_id)) {
                this.allBlocks.push({
                  type: 'tool_result',
                  tool_use_id: toolResult.tool_use_id,
                  content: fullText,
                  is_error: !!toolResult.is_error,
                })
                events.push({
                  type: 'tool_result',
                  tool_use_id: toolResult.tool_use_id,
                  name: toolName,
                  result: fullText,
                  is_error: !!toolResult.is_error,
                })
              }
            } else {
              if (!this.allBlocks.some((b) => b.type === 'tool_result' && b.tool_use_id === toolResult.tool_use_id)) {
                this.allBlocks.push({
                  type: 'tool_result',
                  tool_use_id: toolResult.tool_use_id,
                  content: truncatedContent,
                  is_error: !!toolResult.is_error,
                })
                events.push({
                  type: 'tool_result',
                  tool_use_id: toolResult.tool_use_id,
                  name: toolName || 'unknown',
                  result: truncatedContent.length > 2000 ? truncatedContent.slice(0, 2000) + ' [truncated]' : truncatedContent,
                  is_error: !!toolResult.is_error,
                })
              }
            }
            continue // Don't duplicate as agent sub-block
          }

          const subBlock = {
            type: 'tool_result',
            tool_use_id: toolResult.tool_use_id,
            content: truncatedContent,
            is_error: !!toolResult.is_error,
          }
          this.pushAgentSubBlock(parentId, subBlock)
          events.push({
            type: 'agent_content',
            parent_tool_use_id: parentId,
            block_type: 'tool_result',
            tool_use_id: toolResult.tool_use_id,
            content: subBlock.content,
            is_error: subBlock.is_error,
          })
        }
      }
    }
    return events
  }

  private mapSemanticToolEvents(toolUseId: string, toolName: string, input: Record<string, unknown>): SseEvent[] {
    if (toolName === 'AskUserQuestion') {
      const questions = Array.isArray(input.questions) ? input.questions : []
      return [{
        type: 'question_request',
        tool_use_id: toolUseId,
        input,
        questions,
      }]
    }

    if (toolName === 'ExitPlanMode') {
      const planContent = typeof input.plan === 'string'
          ? input.plan
          : typeof input.content === 'string'
              ? input.content
              : undefined
      return [{
        type: 'plan_review_request',
        tool_use_id: toolUseId,
        input,
        ...(planContent !== undefined ? { plan_content: planContent } : {}),
      }]
    }

    if (toolName === 'TodoWrite' && Array.isArray(input.todos)) {
      return [{
        type: 'todo_update',
        tool_use_id: toolUseId,
        input,
        todos: input.todos,
      }]
    }

    return []
  }

  private classifyError(raw: string): ErrorCategory {
    if (/40[13]|unauthorized|invalid.*key|api.key.*invalid/i.test(raw)) return 'auth'
    if (/429|rate.limit|too.many.request/i.test(raw)) return 'rate_limit'
    if (/quota|insufficient.*balance|credit|billing/i.test(raw)) return 'quota'
    if (/model.*not.found|invalid.*model|not_found.*model|unknown.*model|unsupported.*model/i.test(raw)) return 'model'
    if (/timeout|timed?.out|ECONNREFUSED|ECONNRESET|ENOTFOUND|network|fetch.failed|dns/i.test(raw)) return 'network'
    if (/permission.denied|operation.not.permitted|access.denied|forbidden/i.test(raw)) return 'permission'
    if (/overloaded|capacity|503|service.unavailable/i.test(raw)) return 'service'
    if (/token.*limit|context.*length|too.long/i.test(raw)) return 'token_limit'
    return 'unknown'
  }

  // ── Main-thread handlers (unchanged logic, scoped to parent) ──

  private mapStreamEvent(msg: Extract<SDKMessage, { type: 'stream_event' }>): SseEvent[] {
    const event = msg.event
    const events: SseEvent[] = []

    switch (event.type) {
      case 'content_block_start': {
        const block = event.content_block
        if (block.type === 'tool_use') {
          this.currentToolId = block.id
          this.currentToolName = block.name
          this.inputJson = ''
          events.push({ type: 'tool_use_start', id: block.id, name: block.name })
        } else if (block.type === 'thinking') {
          this.inThinkingBlock = true
          this.thinkingText = ''
          events.push({ type: 'thinking_start' })
        }
        break
      }

      case 'content_block_delta': {
        const delta = event.delta
        if (delta.type === 'text_delta') {
          events.push({ type: 'text_delta', text: delta.text })
        } else if (delta.type === 'thinking_delta') {
          const text = (delta as unknown as Record<string, unknown>).thinking as string || ''
          this.thinkingText += text
          events.push({ type: 'thinking_delta', text })
        } else if (delta.type === 'input_json_delta') {
          this.inputJson += delta.partial_json
        }
        break
      }

      case 'content_block_stop': {
        if (this.inThinkingBlock) {
          if (this.thinkingText) {
            this.allBlocks.push({ type: 'thinking', text: this.thinkingText })
          }
          this.inThinkingBlock = false
          this.thinkingText = ''
        } else if (this.currentToolId) {
          let parsedInput: Record<string, unknown> = {}
          try { parsedInput = JSON.parse(this.inputJson || '{}') } catch { /* empty */ }
          events.push({
            type: 'tool_use',
            id: this.currentToolId,
            name: this.currentToolName,
            input: parsedInput,
          })
          events.push(...this.mapSemanticToolEvents(this.currentToolId, this.currentToolName, parsedInput))
          if (this.currentToolName === 'Agent' || this.currentToolName === 'Task') {
            const description = typeof parsedInput.description === 'string' ? parsedInput.description : undefined
            const prompt = typeof parsedInput.prompt === 'string' ? parsedInput.prompt : undefined
            if (description || prompt) {
              events.push({
                type: 'agent_start',
                parent_tool_use_id: this.currentToolId,
                ...(description ? { description } : {}),
                ...(prompt ? { prompt } : {}),
              })
            }
          }
          this.allBlocks.push({
            type: 'tool_use',
            id: this.currentToolId,
            name: this.currentToolName,
            input: parsedInput,
          })
          this.toolIdToName.set(this.currentToolId, this.currentToolName)
          this.currentToolId = ''
          this.currentToolName = ''
          this.inputJson = ''
        }
        break
      }
    }

    return events
  }

  private mapAssistantMessage(msg: Extract<SDKMessage, { type: 'assistant' }>): SseEvent[] {
    const events: SseEvent[] = []

    for (const block of msg.message.content) {
      if (block.type === 'text') {
        this.allBlocks.push({ type: 'text', text: block.text })
      } else if (block.type === 'thinking') {
        const thinkBlock = block as { type: 'thinking'; thinking: string }
        if (thinkBlock.thinking && !this.allBlocks.some(b => b.type === 'thinking')) {
          this.allBlocks.push({ type: 'thinking', text: thinkBlock.thinking })
        }
      }
    }

    return events
  }

  private mapToolUseSummary(msg: Extract<SDKMessage, { type: 'tool_use_summary' }>): SseEvent[] {
    const toolUseIds: string[] = msg.preceding_tool_use_ids

    for (const toolUseId of toolUseIds) {
      if (!this.allBlocks.some((b) => b.type === 'tool_result' && b.tool_use_id === toolUseId)) {
        const isError = this.erroredToolUseIds.has(toolUseId)
        this.allBlocks.push({
          type: 'tool_result',
          tool_use_id: toolUseId,
          content: msg.summary,
          is_error: isError,
        })
      }
    }

    const resultEvents = toolUseIds.map((toolUseId) => {
      const isError = this.erroredToolUseIds.has(toolUseId)
      return {
        type: 'tool_result',
        tool_use_id: toolUseId,
        name: '',
        result: msg.summary.length > 2000 ? msg.summary.slice(0, 2000) + ' [truncated]' : msg.summary,
        is_error: isError,
      }
    })

    return [
      ...resultEvents,
      {
        type: 'tool_summary',
        tool_use_ids: toolUseIds,
        summary: msg.summary,
      },
    ]
  }

  private mapToolProgress(msg: Extract<SDKMessage, { type: 'tool_progress' }>): SseEvent[] {
    const parentId = (msg as Record<string, unknown>).parent_tool_use_id as string | null
    if (parentId) {
      // Sub-agent tool progress
      return [{
        type: 'agent_content',
        parent_tool_use_id: parentId,
        block_type: 'tool_progress',
        tool_use_id: msg.tool_use_id,
        tool_name: msg.tool_name,
        elapsed_time_seconds: msg.elapsed_time_seconds,
      }]
    }
    return [{
      type: 'tool_progress',
      tool_use_id: msg.tool_use_id,
      tool_name: msg.tool_name,
      elapsed_time_seconds: msg.elapsed_time_seconds,
    }]
  }

  private mapSystemMessage(msg: SDKMessage): SseEvent[] {
    const m = msg as Record<string, unknown>
    if (m.subtype === 'init') {
      if (typeof m.session_id === 'string') {
        this.sdkSessionId = m.session_id
        return [{
          type: 'session_init',
          session_id: m.session_id,
          ...(typeof m.model === 'string' ? { model: m.model } : {}),
        }]
      }
    }
    if (m.subtype === 'api_retry') {
      return [{
        type: 'api_retry',
        ...(typeof m.attempt === 'number' ? { attempt: m.attempt } : {}),
        ...(typeof m.max_retries === 'number' ? { max_retries: m.max_retries } : {}),
        ...(typeof m.retry_delay_ms === 'number' ? { retry_delay_ms: m.retry_delay_ms } : {}),
        ...(typeof m.error_status === 'number' || m.error_status === null ? { error_status: m.error_status } : {}),
        ...(typeof m.error === 'string' ? { error: m.error } : {}),
      }]
    }
    return []
  }

  private mapUserMessage(msg: Extract<SDKMessage, { type: 'user' }>): SseEvent[] {
    const events: SseEvent[] = []
    const message = msg.message
    if ('content' in message && Array.isArray(message.content)) {
      for (const block of message.content) {
        if (typeof block !== 'object' || block === null || !('type' in block)) continue

        if (block.type === 'tool_result') {
          const toolResult = block as { type: 'tool_result'; tool_use_id: string; content?: unknown; is_error?: boolean }
          const toolName = this.toolIdToName.get(toolResult.tool_use_id) || 'unknown'
          if (toolName === 'WebSearch' || toolName === 'WebFetch' || toolName.includes('web') || toolName.includes('Web')) {
            const contentStr = typeof toolResult.content === 'string'
                ? toolResult.content.slice(0, 1000)
                : JSON.stringify(toolResult.content)?.slice(0, 1000)
            console.log(`[forge-web-tool-result] ${toolName} (${toolResult.tool_use_id}):`, JSON.stringify({
              is_error: toolResult.is_error,
              content_preview: contentStr,
            }))
          }
          if (toolResult.is_error) {
            const errorContent = typeof toolResult.content === 'string'
                ? toolResult.content
                : JSON.stringify(toolResult.content)
            console.error(`[forge-tool-error] Tool ${toolName} (${toolResult.tool_use_id}) failed:`, errorContent?.slice(0, 500))
            this.erroredToolUseIds.add(toolResult.tool_use_id)
          }

          // For Agent tool results: content is an array of blocks containing the agent's
          // full text output (reasoning, scoring, analysis). Extract text blocks as
          // agent_content sub-blocks so they're visible in the expanded AgentBlock.
          const isAgentTool = toolName === 'Agent' || toolName === 'delegate_to_agent'
          if (isAgentTool && Array.isArray(toolResult.content)) {
            const contentBlocks = toolResult.content as { type: string; text?: string }[]
            const textParts: string[] = []
            for (const cb of contentBlocks) {
              if (cb.type === 'text' && cb.text) {
                const cleanText = this.stripSdkMetadata(cb.text)
                if (cleanText) {
                  this.pushAgentSubBlock(toolResult.tool_use_id, { type: 'text', text: cleanText })
                  events.push({
                    type: 'agent_content',
                    parent_tool_use_id: toolResult.tool_use_id,
                    block_type: 'text',
                    text: cleanText,
                  })
                  textParts.push(cleanText)
                }
              }
            }
            // Also store as regular tool_result with full text (not truncated)
            const fullText = textParts.join('\n') || JSON.stringify(toolResult.content) || ''
            this.allBlocks.push({
              type: 'tool_result',
              tool_use_id: toolResult.tool_use_id,
              content: fullText,
              is_error: !!toolResult.is_error,
            })
            events.push({
              type: 'tool_result',
              tool_use_id: toolResult.tool_use_id,
              name: toolName,
              result: fullText,
              is_error: !!toolResult.is_error,
            })
          } else {
            const resultText = typeof toolResult.content === 'string'
                ? (toolResult.content.length > 2000 ? toolResult.content.slice(0, 2000) + ' [truncated]' : toolResult.content)
                : JSON.stringify(toolResult.content)?.slice(0, 2000) || ''
            events.push({
              type: 'tool_result',
              tool_use_id: toolResult.tool_use_id,
              name: toolName,
              result: resultText,
              is_error: !!toolResult.is_error,
            })
            // Persist to allBlocks so reopened (history) sessions can expand the
            // tool card and see results. Without this only tool_use is stored.
            if (!this.allBlocks.some((b) => b.type === 'tool_result' && b.tool_use_id === toolResult.tool_use_id)) {
              this.allBlocks.push({
                type: 'tool_result',
                tool_use_id: toolResult.tool_use_id,
                content: resultText,
                is_error: !!toolResult.is_error,
              })
            }
          }

          const rawContent = toolResult.content
              ? this.extractRawContent(toolResult.content)
              : null
          const rawContentBlock = rawContent ?? { type: 'text', text: toolResult.is_error ? '(tool returned an error)' : '(empty result)' }
          events.push({
            type: 'tool_raw_result',
            tool_use_id: toolResult.tool_use_id,
            tool_name: toolName,
            raw_content: rawContentBlock,
          })
          // Persist raw_content too — ToolResultContent prefers raw_content over
          // tool_result.content when expanding, so this keeps history consistent
          // with the live streaming view.
          if (!this.allBlocks.some((b) => b.type === 'tool_raw_result' && b.tool_use_id === toolResult.tool_use_id)) {
            this.allBlocks.push({
              type: 'tool_raw_result',
              tool_use_id: toolResult.tool_use_id,
              tool_name: toolName,
              raw_content: rawContentBlock,
            })
          }
        }

        if (block.type === 'web_search_tool_result') {
          const wsBlock = block as { type: 'web_search_tool_result'; tool_use_id: string; content: unknown }
          console.log(`[forge-web-search-result] API-level web_search_tool_result received:`, JSON.stringify({
            tool_use_id: wsBlock.tool_use_id,
            content_type: typeof wsBlock.content,
            content_preview: JSON.stringify(wsBlock.content)?.slice(0, 500),
          }))
          const results = this.extractWebSearchResults(wsBlock.content)
          if (results) {
            events.push({
              type: 'tool_raw_result',
              tool_use_id: wsBlock.tool_use_id,
              tool_name: 'WebSearch',
              raw_content: results,
            })
            events.push({
              type: 'web_search_result',
              tool_use_id: wsBlock.tool_use_id,
              results: results.results,
            })
          }
        }
      }
    }
    return events
  }

  /** Strip SDK-appended metadata (agentId, usage stats) from agent text output */
  private stripSdkMetadata(text: string): string {
    // Remove trailing metadata like: \n\nagentId: xxx (for resuming...)\n<usage>...</usage>
    return text
        .replace(/\n*agentId:\s*\S+\s*\(for resuming[^)]*\)\s*/g, '')
        .replace(/<usage>[\s\S]*?<\/usage>\s*/g, '')
        .trim()
  }

  private extractRawContent(content: unknown): Record<string, unknown> | null {
    if (typeof content === 'string') {
      if (!content) return null
      return { type: 'text', text: content.length > 3000 ? content.slice(0, 3000) + '\n[truncated]' : content }
    }
    if (Array.isArray(content)) {
      const webResults = this.extractWebSearchResults(content)
      if (webResults) return webResults

      const texts = content
          .filter((item: Record<string, unknown>) => item?.type === 'text' && item?.text)
          .map((item: Record<string, unknown>) => String(item.text))
      if (texts.length > 0) {
        const joined = texts.join('\n')
        return { type: 'text', text: joined.length > 3000 ? joined.slice(0, 3000) + '\n[truncated]' : joined }
      }
    }
    return null
  }

  private extractWebSearchResults(content: unknown): Record<string, unknown> | null {
    if (!Array.isArray(content)) return null
    const results = content.filter(
        (item: Record<string, unknown>) => item?.type === 'web_search_result' && item?.title && item?.url
    )
    if (results.length === 0) return null
    return {
      type: 'web_search',
      results: results.map((r: Record<string, unknown>) => ({
        title: String(r.title),
        url: String(r.url),
      })),
    }
  }

  private mapAgentResult(parentId: string, msg: Extract<SDKMessage, { type: 'result' }>): SseEvent[] {
    const usage = 'usage' in msg && msg.usage
        ? msg.usage as { input_tokens?: number; output_tokens?: number }
        : undefined

    if (msg.subtype === 'success') {
      return [{
        type: 'agent_done',
        parent_tool_use_id: parentId,
        status: 'success',
        ...(typeof usage?.input_tokens === 'number' ? { input_tokens: usage.input_tokens } : {}),
        ...(typeof usage?.output_tokens === 'number' ? { output_tokens: usage.output_tokens } : {}),
      }]
    }

    const rawError = msg.subtype.startsWith('error') && 'errors' in msg && Array.isArray(msg.errors)
        ? msg.errors.join('; ')
        : 'Execution failed'
    const category = this.classifyError(rawError)
    return [{
      type: 'agent_error',
      parent_tool_use_id: parentId,
      error: rawError,
      category,
    }]
  }

  private mapResult(msg: Extract<SDKMessage, { type: 'result' }>): SseEvent[] {
    if ('usage' in msg && msg.usage) {
      const usage = msg.usage as { input_tokens?: number; output_tokens?: number }
      this.inputTokens = usage.input_tokens || 0
      this.outputTokens = usage.output_tokens || 0
    }

    const m = msg as Record<string, unknown>
    this.resultSubtype = msg.subtype
    this.totalCostUsd = typeof m.total_cost_usd === 'number' ? m.total_cost_usd : undefined
    this.durationMs = typeof m.duration_ms === 'number' ? m.duration_ms : undefined
    this.numTurns = typeof m.num_turns === 'number' ? m.num_turns : undefined

    if (msg.subtype === 'success') {
      this.finishReason = 'success'
      return []
    }

    const isUserAbort = msg.subtype.startsWith('error') && 'errors' in msg && Array.isArray(msg.errors)
        && msg.errors.some((e: unknown) => /interrupt|user.?abort/i.test(String(e)))

    if (isUserAbort) {
      this.finishReason = 'user_abort'
      return [{ type: 'cancelled', reason: 'user_abort' }]
    }

    this.finishReason = 'error'
    const rawError = msg.subtype.startsWith('error') && 'errors' in msg && Array.isArray(msg.errors)
        ? msg.errors.join('; ')
        : 'Execution failed'
    const category = this.classifyError(rawError)
    return [{ type: 'error', error: rawError, category }]
  }
}
