import type { ACPMessage } from '#cli/core/acp'
import {
  isEditLikeToolName,
  isFetchLikeToolName,
  isReadLikeToolName,
  isSearchLikeToolName,
  isWriteLikeToolName,
} from '#cli/core/tool-metadata'
import type { StreamEventMetadata, ToolCallInfo } from './stream-runner-utils'
import { safeJson } from '#cli/core/utils'
import { extractTextContent } from './content'
import { truncateReadToolOutput } from './read-output'
import {
  normalizeSubagentId,
  normalizeSubagentType,
  normalizeTaskDescription,
  normalizeToolName,
} from './stream-runner-utils'

type PendingExploreCall = Readonly<{
  toolCallId: string
  description: string
  subagentType: string
  createdAtMs: number
}>

interface SubagentTextBuffer {
  chunks: string[]
  chars: number
  lastSeenAtMs: number
}

interface SubagentNestedOutputState {
  lines: number
  remainder: string
  truncated: boolean
}

interface ToolOutputQueueEntry {
  assistantId: string
  toolCallId: string
  toolName: string
  delta: string
}

type ToolCallLookup = Readonly<{
  findCallInfo: (callId: string) => ToolCallInfo | null
}>

interface RawToolCall {
  id?: string
  function?: {
    name?: string
    arguments?: unknown
  }
}

export type SubagentRouter = Readonly<{
  handleSubagentEvent: (params: {
    msg: ACPMessage
    md: StreamEventMetadata
    assistantId: string
  }) => Promise<boolean>
  trackExploreCall: (toolCallId: string, args: Record<string, unknown>) => void
  bindTaskResultSubagent: (
    toolCallId: string,
    structuredContent: unknown,
  ) => void
}>

interface SubagentRouteEntry {
  toolCallId: string
  lastSeenAtMs: number
  completedAtMs?: number
}

interface NestedToolEntry {
  toolName: string
  lastSeenAtMs: number
}

interface NestedOutputEntry {
  state: SubagentNestedOutputState
  lastSeenAtMs: number
}

export function createSubagentRouter(
  opts: Readonly<{
    toolCallLookup: ToolCallLookup
    findMostRecentExploreToolCallId: (
      match: Readonly<{ description: string, subagentType: string }>,
    ) => string | null
    upsertToolOutputStart: (
      assistantId: string,
      toolCallId: string,
      toolName: string,
    ) => void
    appendToolOutputDelta: (
      assistantId: string,
      toolCallId: string,
      toolName: string,
      delta: string,
    ) => void
    queueToolOutputDelta: (entry: ToolOutputQueueEntry) => void
    flushPendingUi: (opts?: { force?: boolean }) => void
    maybeFlushAndYieldUi: (opts?: { force?: boolean }) => Promise<void>
    maybeLogStreamStats: (opts?: { force?: boolean }) => void
  }>,
): SubagentRouter {
  const exploreToolCallIdBySubagentId = new Map<string, SubagentRouteEntry>()
  const pendingExploreCalls: PendingExploreCall[] = []
  const pendingSubagentTextBySubagentId = new Map<string, SubagentTextBuffer>()
  const subagentNestedToolNameByCallId = new Map<string, NestedToolEntry>()
  const subagentNestedToolOutputStateByCallId = new Map<
    string,
    NestedOutputEntry
  >()
  const MAX_SUBAGENT_ROUTES = 200
  const MAX_PENDING_EXPLORE_CALLS = 200
  const MAX_NESTED_TOOL_CALLS = 500
  const ROUTE_TTL_MS = 10 * 60 * 1000
  const COMPLETED_ROUTE_TTL_MS = 30 * 1000
  const PENDING_TEXT_TTL_MS = 2 * 60 * 1000
  const PENDING_EXPLORE_TTL_MS = 5 * 60 * 1000
  const NESTED_TTL_MS = 5 * 60 * 1000

  const enforceMapCapacity = <K, V>(map: Map<K, V>, max: number): void => {
    while (map.size > max) {
      const first = map.keys().next().value
      if (first == null)
        break
      map.delete(first)
    }
  }

  const touchMapEntry = <K, V>(map: Map<K, V>, key: K, value: V): void => {
    map.delete(key)
    map.set(key, value)
  }

  const flushPendingSubagentTextToExplore = (
    subagentId: string,
    exploreCallId: string,
  ): void => {
    const sid = normalizeSubagentId(subagentId)
    const callId = String(exploreCallId ?? '').trim()
    if (!sid || !callId)
      return
    const pending = pendingSubagentTextBySubagentId.get(sid)
    if (!pending || pending.chars <= 0)
      return
    pendingSubagentTextBySubagentId.delete(sid)

    const info = opts.toolCallLookup.findCallInfo(callId)
    if (!info)
      return
    opts.upsertToolOutputStart(info.assistantId, callId, info.toolName)
    const joined
      = pending.chunks.length === 1 ? pending.chunks[0]! : pending.chunks.join('')
    if (joined) {
      opts.appendToolOutputDelta(
        info.assistantId,
        callId,
        info.toolName,
        joined,
      )
    }
  }

  const trackSubagentRoute = (subagentId: string, toolCallId: string): void => {
    if (!subagentId || !toolCallId)
      return
    const prev = exploreToolCallIdBySubagentId.get(subagentId)
    const entry: SubagentRouteEntry = {
      toolCallId,
      lastSeenAtMs: Date.now(),
      completedAtMs: prev?.completedAtMs,
    }
    touchMapEntry(exploreToolCallIdBySubagentId, subagentId, entry)
    enforceMapCapacity(exploreToolCallIdBySubagentId, MAX_SUBAGENT_ROUTES)
  }

  const getSubagentRoute = (subagentId: string): string | null => {
    if (!subagentId)
      return null
    const entry = exploreToolCallIdBySubagentId.get(subagentId)
    if (!entry)
      return null
    entry.lastSeenAtMs = Date.now()
    touchMapEntry(exploreToolCallIdBySubagentId, subagentId, entry)
    return entry.toolCallId
  }

  const markRouteCompleted = (
    toolCallId: string,
    subagentId?: string,
  ): void => {
    const now = Date.now()
    if (subagentId) {
      const entry = exploreToolCallIdBySubagentId.get(subagentId)
      if (entry && entry.toolCallId === toolCallId) {
        entry.completedAtMs = now
        entry.lastSeenAtMs = now
        touchMapEntry(exploreToolCallIdBySubagentId, subagentId, entry)
        return
      }
    }
    for (const [sid, entry] of exploreToolCallIdBySubagentId.entries()) {
      if (entry.toolCallId !== toolCallId)
        continue
      entry.completedAtMs = now
      entry.lastSeenAtMs = now
      touchMapEntry(exploreToolCallIdBySubagentId, sid, entry)
    }
  }

  const trackNestedToolCall = (
    nestedToolCallId: string,
    toolName: string,
  ): void => {
    if (!nestedToolCallId)
      return
    const entry: NestedToolEntry = { toolName, lastSeenAtMs: Date.now() }
    touchMapEntry(subagentNestedToolNameByCallId, nestedToolCallId, entry)
    enforceMapCapacity(subagentNestedToolNameByCallId, MAX_NESTED_TOOL_CALLS)
  }

  const readNestedToolName = (nestedToolCallId: string): string => {
    const entry = subagentNestedToolNameByCallId.get(nestedToolCallId)
    if (!entry)
      return ''
    entry.lastSeenAtMs = Date.now()
    touchMapEntry(subagentNestedToolNameByCallId, nestedToolCallId, entry)
    return entry.toolName
  }

  const setNestedOutputState = (
    nestedToolCallId: string,
    state: SubagentNestedOutputState,
  ): void => {
    const entry: NestedOutputEntry = { state, lastSeenAtMs: Date.now() }
    touchMapEntry(
      subagentNestedToolOutputStateByCallId,
      nestedToolCallId,
      entry,
    )
    enforceMapCapacity(
      subagentNestedToolOutputStateByCallId,
      MAX_NESTED_TOOL_CALLS,
    )
  }

  const readNestedOutputState = (
    nestedToolCallId: string,
  ): SubagentNestedOutputState | null => {
    const entry = subagentNestedToolOutputStateByCallId.get(nestedToolCallId)
    if (!entry)
      return null
    entry.lastSeenAtMs = Date.now()
    touchMapEntry(
      subagentNestedToolOutputStateByCallId,
      nestedToolCallId,
      entry,
    )
    return entry.state
  }

  const cleanupStaleEntries = (now: number): void => {
    for (let i = pendingExploreCalls.length - 1; i >= 0; i--) {
      if (now - pendingExploreCalls[i].createdAtMs > PENDING_EXPLORE_TTL_MS)
        pendingExploreCalls.splice(i, 1)
    }

    for (const [sid, pending] of pendingSubagentTextBySubagentId.entries()) {
      if (now - pending.lastSeenAtMs <= PENDING_TEXT_TTL_MS)
        continue
      const route = getSubagentRoute(sid)
      if (route)
        flushPendingSubagentTextToExplore(sid, route)
      pendingSubagentTextBySubagentId.delete(sid)
    }

    for (const [sid, entry] of exploreToolCallIdBySubagentId.entries()) {
      const completed = entry.completedAtMs
        ? now - entry.completedAtMs > COMPLETED_ROUTE_TTL_MS
        : false
      const stale = now - entry.lastSeenAtMs > ROUTE_TTL_MS
      if (!completed && !stale)
        continue
      const route = entry.toolCallId
      if (route)
        flushPendingSubagentTextToExplore(sid, route)
      exploreToolCallIdBySubagentId.delete(sid)
      pendingSubagentTextBySubagentId.delete(sid)
    }

    for (const [callId, entry] of subagentNestedToolNameByCallId.entries()) {
      if (now - entry.lastSeenAtMs <= NESTED_TTL_MS)
        continue
      subagentNestedToolNameByCallId.delete(callId)
      subagentNestedToolOutputStateByCallId.delete(callId)
    }
  }

  const queuePendingSubagentText = (
    subagentId: string,
    delta: string,
  ): void => {
    const sid = normalizeSubagentId(subagentId)
    const d = String(delta ?? '')
    if (!sid || !d)
      return
    const prev = pendingSubagentTextBySubagentId.get(sid)
    if (prev) {
      prev.chunks.push(d)
      prev.chars += d.length
      prev.lastSeenAtMs = Date.now()
      return
    }
    pendingSubagentTextBySubagentId.set(sid, {
      chunks: [d],
      chars: d.length,
      lastSeenAtMs: Date.now(),
    })
  }

  const resolveExploreToolCallIdForSubagent = (
    md: StreamEventMetadata,
  ): string | null => {
    const subagentId = normalizeSubagentId(md?.subagentId ?? md?.subagent_id)
    const existing = subagentId ? getSubagentRoute(subagentId) : null
    if (existing)
      return existing

    const taskDescription = normalizeTaskDescription(
      md?.taskDescription ?? md?.task_description ?? md?.description,
    )
    const subagentType = normalizeSubagentType(
      md?.subagentType ?? md?.subagent_type,
    )

    if (taskDescription || subagentType) {
      for (let i = pendingExploreCalls.length - 1; i >= 0; i--) {
        const p = pendingExploreCalls[i]
        if (taskDescription && p.description !== taskDescription)
          continue
        if (subagentType && p.subagentType !== subagentType)
          continue
        pendingExploreCalls.splice(i, 1)
        if (subagentId)
          trackSubagentRoute(subagentId, p.toolCallId)
        return p.toolCallId
      }
    }

    const scanned = opts.findMostRecentExploreToolCallId({
      description: taskDescription,
      subagentType,
    })
    if (scanned) {
      if (subagentId)
        trackSubagentRoute(subagentId, scanned)
      return scanned
    }

    return null
  }

  const safeParseToolArgs = (
    rawArgs: unknown,
  ): Record<string, unknown> | null => {
    if (!rawArgs)
      return null
    if (typeof rawArgs === 'object' && !Array.isArray(rawArgs))
      return rawArgs as Record<string, unknown>
    if (typeof rawArgs === 'string') {
      const s = rawArgs.trim()
      if (!s)
        return null
      try {
        const parsed = JSON.parse(s)
        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
          return parsed as Record<string, unknown>
      }
      catch {
        // ignore
      }
    }
    return null
  }

  const summarizeSubagentToolArgsForUi = (
    toolName: string,
    callId: string,
    rawArgs: unknown,
  ): Record<string, unknown> => {
    const lower = String(toolName ?? '')
      .trim()
      .toLowerCase()
    const id = String(callId ?? '').trim()
    const argsObj = safeParseToolArgs(rawArgs) ?? {}
    const out: Record<string, unknown> = id ? { _call_id: id } : {}

    const pickString = (v: unknown): string => (typeof v === 'string' ? v : '')
    const pickNumber = (v: unknown): number | null =>
      typeof v === 'number' && Number.isFinite(v) ? v : null

    if (isReadLikeToolName(lower)) {
      const filePath = pickString(
        (argsObj as Record<string, unknown>).file_path,
      ).trim()
      if (filePath)
        out.file_path = filePath
      const limit = pickNumber((argsObj as Record<string, unknown>).limit)
      const offset = pickNumber((argsObj as Record<string, unknown>).offset)
      if (offset && offset > 0)
        out.offset = offset
      if (limit && limit > 0)
        out.limit = limit
      return out
    }
    if (isSearchLikeToolName(lower)) {
      const query = pickString(
        (argsObj as Record<string, unknown>).query,
      ).trim() || pickString((argsObj as Record<string, unknown>).pattern).trim()
      if (query)
        out.query = query
      const filePath = pickString(
        (argsObj as Record<string, unknown>).file_path,
      ).trim() || pickString((argsObj as Record<string, unknown>).path).trim()
      if (filePath)
        out.file_path = filePath
      return out
    }
    if (lower === 'bash') {
      const description = pickString(
        (argsObj as Record<string, unknown>).description,
      ).trim()
      if (description)
        out.description = description
      const command = pickString(
        (argsObj as Record<string, unknown>).command,
      ).trim()
      if (command && command.length <= 200)
        out.command = command
      return out
    }
    if (lower === 'websearch') {
      const query = pickString(
        (argsObj as Record<string, unknown>).query,
      ).trim()
      if (query)
        out.query = query
      return out
    }
    if (isFetchLikeToolName(lower)) {
      const url = pickString((argsObj as Record<string, unknown>).url).trim()
      if (url)
        out.url = url
      return out
    }
    if (isWriteLikeToolName(lower)) {
      const filePath = pickString(
        (argsObj as Record<string, unknown>).file_path,
      ).trim() || pickString((argsObj as Record<string, unknown>).path).trim()
      if (filePath)
        out.file_path = filePath
      return out
    }
    if (isEditLikeToolName(lower)) {
      const filePath = pickString(
        (argsObj as Record<string, unknown>).file_path,
      ).trim() || pickString((argsObj as Record<string, unknown>).path).trim()
      if (filePath)
        out.file_path = filePath
      return out
    }

    return out
  }

  const formatSubagentToolCallLine = (
    toolName: string,
    callId: string,
    rawArgs: unknown,
  ): string => {
    const args = summarizeSubagentToolArgsForUi(toolName, callId, rawArgs)
    const argsText = safeJson(args)
    return `\n[tool] ${toolName}${argsText && argsText !== '{}' ? ` ${argsText}` : ''}\n`
  }

  const appendNestedToolOutputDelta = (
    nestedToolCallId: string,
    delta: string,
  ): string => {
    const id = String(nestedToolCallId ?? '').trim()
    if (!id)
      return ''
    const maxLines = 10
    const d = String(delta ?? '').replace(/\r/g, '')
    if (!d)
      return ''

    const prev = readNestedOutputState(id) ?? {
      lines: 0,
      remainder: '',
      truncated: false,
    }
    if (prev.truncated) {
      setNestedOutputState(id, prev)
      return ''
    }

    const combined = `${prev.remainder}${d}`
    const parts = combined.split('\n')
    const endsWithNewline = combined.endsWith('\n')
    const complete = parts.slice(0, -1)
    const nextRemainder = endsWithNewline
      ? ''
      : String(parts[parts.length - 1] ?? '')

    let out = ''
    let lines = prev.lines
    for (const line of complete) {
      if (lines >= maxLines) {
        if (!prev.truncated) {
          out += '...\n'
        }
        setNestedOutputState(id, { lines, remainder: '', truncated: true })
        return out
      }
      out += `${line}\n`
      lines += 1
    }

    if (nextRemainder) {
      if (lines >= maxLines) {
        out += '...\n'
        setNestedOutputState(id, { lines, remainder: '', truncated: true })
        return out
      }
      // Keep partial line in remainder, but also stream it so the UI feels live.
      out += nextRemainder
    }

    const next = { lines, remainder: nextRemainder, truncated: false }
    setNestedOutputState(id, next)
    return out
  }

  const handleSubagentEvent = async (params: {
    msg: ACPMessage
    md: StreamEventMetadata
    assistantId: string
  }): Promise<boolean> => {
    if (!params.md?.subagent)
      return false

    cleanupStaleEntries(Date.now())

    const { msg, md } = params
    const subagentId = normalizeSubagentId(md?.subagentId ?? md?.subagent_id)
    const exploreCallId = resolveExploreToolCallIdForSubagent(md)
    const callInfo = exploreCallId
      ? opts.toolCallLookup.findCallInfo(exploreCallId)
      : null

    // Ensure ordering: any buffered main-agent deltas land before nested Explore output.
    opts.flushPendingUi({ force: true })

    if (!exploreCallId || !callInfo) {
      // Buffer until we can resolve the parent Explore tool call.
      // SelfReflectionCritic now emits a synthetic Task call (`name: 'task'`)
      // tool_call_end before its events, so it should be resolved above.
      // Any remaining unmatched events are buffered and flushed once the
      // route is established (e.g. when the tool_result arrives).
      if (msg.role === 'tool') {
        const output = extractTextContent(msg.content)
        const truncated = truncateReadToolOutput(String(output ?? ''), {
          maxLines: 10,
          ellipsisLine: '...',
        })
        if (truncated.output && subagentId)
          queuePendingSubagentText(subagentId, truncated.output)
      }
      else {
        const text = extractTextContent(msg.content)
        if (text && subagentId)
          queuePendingSubagentText(subagentId, text)
        const rawToolCalls = Array.isArray(
          (msg as { tool_calls?: unknown }).tool_calls,
        )
          ? ((msg as { tool_calls?: RawToolCall[] }).tool_calls ?? [])
          : []
        if (rawToolCalls.length > 0) {
          const lines: string[] = []
          for (const tc of rawToolCalls) {
            const toolName = normalizeToolName(tc?.function?.name)
            const rawArgs = tc?.function?.arguments
            const nestedId = String(tc?.id ?? '').trim()
            if (nestedId)
              trackNestedToolCall(nestedId, toolName)
            lines.push(formatSubagentToolCallLine(toolName, nestedId, rawArgs))
          }
          if (subagentId)
            queuePendingSubagentText(subagentId, lines.join(''))
        }
      }
      opts.maybeLogStreamStats()
      return true
    }

    // Route all subagent-visible output under the originating Explore tool call.
    const toolName = callInfo.toolName
    opts.upsertToolOutputStart(callInfo.assistantId, exploreCallId, toolName)
    if (subagentId) {
      trackSubagentRoute(subagentId, exploreCallId)
      flushPendingSubagentTextToExplore(subagentId, exploreCallId)
    }

    if (msg.role === 'tool') {
      const output = extractTextContent(msg.content)
      const nestedToolCallId = String(
        (msg as { tool_call_id?: unknown })?.tool_call_id
        ?? md?.tool_call_id
        ?? '',
      ).trim()
      const nestedToolName = nestedToolCallId
        ? readNestedToolName(nestedToolCallId)
        : ''
      const isError
        = (msg as { metadata?: StreamEventMetadata })?.metadata?.error === true
      const header = nestedToolName
        ? `[result] ${nestedToolName}${isError ? ' (error)' : ''}${nestedToolCallId ? ` ${nestedToolCallId}` : ''}\n`
        : `[result]${isError ? ' (error)' : ''}${nestedToolCallId ? ` ${nestedToolCallId}` : ''}\n`
      const truncated = truncateReadToolOutput(String(output ?? ''), {
        maxLines: 10,
        ellipsisLine: '...',
      })
      const body = truncated.output
        ? `${String(truncated.output).replace(/\r/g, '')}\n`
        : ''
      const delta = `\n${header}${body}`
      opts.queueToolOutputDelta({
        assistantId: callInfo.assistantId,
        toolCallId: exploreCallId,
        toolName,
        delta,
      })
      if (nestedToolCallId) {
        subagentNestedToolNameByCallId.delete(nestedToolCallId)
        subagentNestedToolOutputStateByCallId.delete(nestedToolCallId)
      }
    }
    else {
      // Compact tool call markers (subagent tool calls are not rendered in the main transcript).
      const subToolCalls = Array.isArray(
        (msg as { tool_calls?: unknown }).tool_calls,
      )
        ? ((msg as { tool_calls?: RawToolCall[] }).tool_calls ?? [])
        : []
      if (subToolCalls.length > 0) {
        const lines: string[] = []
        for (const tc of subToolCalls) {
          const nestedId = String(tc?.id ?? '').trim()
          const nestedName = normalizeToolName(tc?.function?.name)
          if (nestedId)
            trackNestedToolCall(nestedId, nestedName)
          const rawArgs = tc?.function?.arguments
          lines.push(formatSubagentToolCallLine(nestedName, nestedId, rawArgs))
        }
        const delta = lines.join('')
        if (delta) {
          opts.queueToolOutputDelta({
            assistantId: callInfo.assistantId,
            toolCallId: exploreCallId,
            toolName,
            delta,
          })
        }
      }

      // Tool output signals sometimes come through as boolean flags instead of `originalEventType`.
      if (md?.tool_output_start) {
        const nestedToolName = normalizeToolName(md?.toolName ?? md?.tool_name)
        const nestedToolCallId = String(
          md?.tool_call_id ?? md?.toolCallId ?? '',
        ).trim()
        if (nestedToolCallId)
          trackNestedToolCall(nestedToolCallId, nestedToolName)
        if (nestedToolCallId) {
          setNestedOutputState(nestedToolCallId, {
            lines: 0,
            remainder: '',
            truncated: false,
          })
        }
        opts.queueToolOutputDelta({
          assistantId: callInfo.assistantId,
          toolCallId: exploreCallId,
          toolName,
          delta: `\n[output] ${nestedToolName} ${nestedToolCallId}\n`,
        })
        await opts.maybeFlushAndYieldUi()
        opts.maybeLogStreamStats()
        return true
      }
      else if (md?.tool_output_delta) {
        const nestedToolCallId = String(
          md?.tool_call_id ?? md?.toolCallId ?? '',
        ).trim()
        const delta = String(md?.delta ?? extractTextContent(msg.content) ?? '')
        if (nestedToolCallId && delta) {
          const clipped = appendNestedToolOutputDelta(nestedToolCallId, delta)
          if (clipped) {
            opts.queueToolOutputDelta({
              assistantId: callInfo.assistantId,
              toolCallId: exploreCallId,
              toolName,
              delta: clipped,
            })
            await opts.maybeFlushAndYieldUi()
            opts.maybeLogStreamStats()
          }
        }
        return true
      }

      const text = extractTextContent(msg.content)
      if (text) {
        opts.queueToolOutputDelta({
          assistantId: callInfo.assistantId,
          toolCallId: exploreCallId,
          toolName,
          delta: text,
        })
      }
    }

    await opts.maybeFlushAndYieldUi()
    opts.maybeLogStreamStats()
    return true
  }

  const trackExploreCall = (
    toolCallId: string,
    args: Record<string, unknown>,
  ): void => {
    const desc = normalizeTaskDescription(args?.description)
    const subType = normalizeSubagentType(
      (args as Record<string, unknown>)?.subagent_type
      ?? (args as Record<string, unknown>)?.subagentType,
    )
    cleanupStaleEntries(Date.now())
    pendingExploreCalls.push({
      toolCallId,
      description: desc,
      subagentType: subType,
      createdAtMs: Date.now(),
    })
    while (pendingExploreCalls.length > MAX_PENDING_EXPLORE_CALLS)
      pendingExploreCalls.shift()
  }

  const bindTaskResultSubagent = (
    toolCallId: string,
    structuredContent: unknown,
  ): void => {
    if (
      !toolCallId
      || !structuredContent
      || typeof structuredContent !== 'object'
    ) {
      return
    }
    const subagentId = normalizeSubagentId(
      (structuredContent as { subagentId?: unknown }).subagentId,
    )
    if (!subagentId)
      return
    trackSubagentRoute(subagentId, toolCallId)
    markRouteCompleted(toolCallId, subagentId)
    flushPendingSubagentTextToExplore(subagentId, toolCallId)
  }

  return { handleSubagentEvent, trackExploreCall, bindTaskResultSubagent }
}
