// Current-native subagent (child Session) tracking for the mini transport.
//
// Discovers child Sessions of the active parent from four current sources:
//   1. projected subagent tool output (`structured.sessionID`) during hydration
//   2. the current session list filtered by `parentID` during hydration
//   3. the process-local active-session map during hydration
//   4. live events from unknown sessions whose `parentID` matches the parent
//
// Tracks one footer tab per child and a detail transcript for the selected
// child, reduced from the same current live event stream the parent uses.
// Detail transcripts rebuild from projected messages on discovery, selection,
// and reconnect, then continue from live deltas using the same
// projected-prefix dedup the parent transport uses.
//
// Per-child interruption uses `v2.session.interrupt(childID)`. Per-child
// backgrounding is intentionally absent: subagent jobs block the parent
// session, so only whole-session `v2.session.background(parentID)` exists.
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
import type { SessionMessage, SessionMessageAssistantTool, ToolPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@opencode-ai/tui/util/locale"
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types"

const CHILD_MESSAGE_LIMIT = 80
const CHILD_FRAME_LIMIT = 80
const CHILD_EVENT_BUFFER_LIMIT = 64
const FAMILY_LIST_LIMIT = 100
const FALLBACK_LABEL = "Subagent"

type V2Event = EventSubscribeOutput

export function outputText(content: ReadonlyArray<{ type: string; text?: string }>) {
  return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n")
}

export function legacyTool(input: {
  sessionID: string
  messageID: string
  callID: string
  name: string
  state: SessionMessageAssistantTool["state"]
  time: SessionMessageAssistantTool["time"]
  provider?: SessionMessageAssistantTool["provider"]
}): ToolPart {
  const base = {
    id: `prt_${input.callID}`,
    sessionID: input.sessionID,
    messageID: input.messageID,
    type: "tool" as const,
    callID: input.callID,
    tool: input.name,
  }
  if (input.state.status === "pending") {
    return {
      ...base,
      state: { status: "pending", input: {}, raw: input.state.input },
    }
  }
  if (input.state.status === "running") {
    return {
      ...base,
      state: {
        status: "running",
        input: input.state.input,
        title: input.name,
        metadata: { structured: input.state.structured, content: input.state.content, providerCall: input.provider },
        time: { start: input.time.ran ?? input.time.created },
      },
    }
  }
  if (input.state.status === "completed") {
    return {
      ...base,
      state: {
        status: "completed",
        input: input.state.input,
        output: outputText(input.state.content),
        title: input.name,
        metadata: {
          structured: input.state.structured,
          content: input.state.content,
          outputPaths: input.state.outputPaths,
          result: input.state.result,
          providerCall: input.provider,
        },
        time: { start: input.time.ran ?? input.time.created, end: input.time.completed ?? input.time.created },
      },
    }
  }
  return {
    ...base,
    state: {
      status: "error",
      input: input.state.input,
      error: input.state.error.message,
      metadata: {
        structured: input.state.structured,
        content: input.state.content,
        result: input.state.result,
        providerCall: input.provider,
      },
      time: { start: input.time.ran ?? input.time.created, end: input.time.completed ?? input.time.created },
    },
  }
}

export function toolCommit(part: ToolPart, phase: "start" | "progress" | "final"): StreamCommit {
  const status = part.state.status
  const text =
    status === "running"
      ? part.tool === "task"
        ? "running task"
        : `running ${part.tool}`
      : status === "completed"
        ? part.state.output
        : status === "error"
          ? part.state.error
          : ""
  return {
    kind: "tool",
    source: "tool",
    text,
    phase,
    messageID: part.messageID,
    partID: part.id,
    tool: part.tool,
    part,
    toolState: status === "error" ? "error" : status === "completed" ? "completed" : "running",
    toolError: status === "error" ? part.state.error : undefined,
  }
}

type Frame = {
  key: string
  commit: StreamCommit
}

type ToolTrack = {
  name: string
  input: Record<string, unknown>
  started: number
}

type ChildState = {
  sessionID: string
  label: string
  description: string
  status: FooterSubagentTab["status"]
  background: boolean
  title?: string
  callIDs: Set<string>
  lastUpdatedAt: number
  frames: Frame[]
  text: Map<string, string>
  projectedText: Map<string, string>
  reasoning: Map<string, string>
  projectedReasoning: Map<string, string>
  tools: Map<string, ToolTrack>
  finishedTools: Set<string>
  messageIDs: Set<string>
  prompts: Map<string, string>
  hydrated: boolean
}

export type SubagentTrackerInput = {
  sdk: OpenCodeClient
  sessionID: string
  thinking: boolean
  emit: () => void
}

export type SubagentTracker = {
  main(event: V2Event): void
  foreign(sessionID: string, event: V2Event): void
  hydrate(next: { messages: SessionMessage[]; active: Record<string, unknown> }): Promise<void>
  select(sessionID: string | undefined): void
  snapshot(): FooterSubagentState
}

function record(value: unknown): Record<string, unknown> | undefined {
  if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as Record<string, unknown>
  return undefined
}

function text(value: unknown): string | undefined {
  if (typeof value !== "string") return undefined
  const next = value.trim()
  return next || undefined
}

function childSessionID(structured: Record<string, unknown> | undefined) {
  const sessionID = text(structured?.sessionID)
  if (!sessionID || !sessionID.startsWith("ses")) return undefined
  const status = structured?.status
  if (status !== "running" && status !== "completed") return undefined
  return { sessionID, running: status === "running" }
}

function tab(child: ChildState): FooterSubagentTab {
  return {
    sessionID: child.sessionID,
    partID: `subagent:${child.sessionID}`,
    callID: `subagent:${child.sessionID}`,
    label: child.label,
    description: child.description || child.title || "",
    status: child.status,
    background: child.background ? true : undefined,
    title: child.title,
    toolCalls: child.callIDs.size > 0 ? child.callIDs.size : undefined,
    lastUpdatedAt: child.lastUpdatedAt,
  }
}

export function createSubagentTracker(input: SubagentTrackerInput): SubagentTracker {
  const children = new Map<string, ChildState>()
  // Live subagent tool calls in the parent, so tool.success structured output
  // can be joined with the call's input metadata.
  const pendingCalls = new Map<string, Record<string, unknown>>()
  // Foreign sessions already resolved through session.get. Non-children stay
  // cached so unrelated concurrent sessions are checked at most once.
  const checked = new Set<string>()
  // Foreign events buffered while a session.get discovery is in flight, so a
  // fast child (including its settled event) is not lost mid-discovery.
  const pendingEvents = new Map<string, V2Event[]>()
  const hydrationEvents = new Map<string, V2Event[]>()
  const hydrationOverflow = new Set<string>()
  const hydrations = new Map<string, Promise<void>>()
  let selected: string | undefined

  const ensureChild = (sessionID: string): ChildState => {
    const existing = children.get(sessionID)
    const child: ChildState = existing ?? {
      sessionID,
      label: FALLBACK_LABEL,
      description: "",
      status: "running",
      background: false,
      callIDs: new Set(),
      lastUpdatedAt: Date.now(),
      frames: [],
      text: new Map(),
      projectedText: new Map(),
      reasoning: new Map(),
      projectedReasoning: new Map(),
      tools: new Map(),
      finishedTools: new Set(),
      messageIDs: new Set(),
      prompts: new Map(),
      hydrated: false,
    }
    if (!existing) children.set(sessionID, child)
    // Adopting a child while its session.get discovery is still in flight:
    // drain the buffered events now. They arrived before whatever the caller
    // applies next, so replaying them first preserves bus order, and the
    // resolved discovery can no longer replay stale events (e.g. step.started)
    // after a terminal settled event was applied directly.
    const buffered = pendingEvents.get(sessionID)
    if (buffered) {
      pendingEvents.delete(sessionID)
      for (const event of buffered) reduce(child, event)
    }
    return child
  }

  const touch = (child: ChildState, timestamp?: number) => {
    child.lastUpdatedAt = Math.max(child.lastUpdatedAt, timestamp ?? Date.now())
  }

  const notifyDetail = (child: ChildState) => {
    if (child.sessionID === selected) input.emit()
  }

  const setFrame = (child: ChildState, key: string, commit: StreamCommit) => {
    const index = child.frames.findIndex((item) => item.key === key)
    if (index === -1) {
      child.frames.push({ key, commit })
      if (child.frames.length > CHILD_FRAME_LIMIT) child.frames.splice(0, child.frames.length - CHILD_FRAME_LIMIT)
      return
    }
    child.frames[index] = { key, commit }
  }

  const applyMeta = (child: ChildState, meta: Record<string, unknown> | undefined) => {
    if (!meta) return
    const agent = text(meta.agent)
    if (agent) child.label = Locale.titlecase(agent)
    const description = text(meta.description)
    if (description) child.description = description
    if (meta.background === true) child.background = true
  }

  const userFrame = (child: ChildState, messageID: string, value: string) => {
    if (child.messageIDs.has(messageID)) return false
    child.messageIDs.add(messageID)
    setFrame(child, `user:${messageID}`, {
      kind: "user",
      source: "system",
      text: value,
      phase: "start",
      messageID,
    })
    return true
  }

  const childTool = (child: ChildState, item: SessionMessageAssistantTool, messageID: string) => {
    const part = legacyTool({
      sessionID: child.sessionID,
      messageID,
      callID: item.id,
      name: item.name,
      state: item.state,
      time: item.time,
      provider: item.provider,
    })
    if (item.state.status === "pending") return
    child.callIDs.add(item.id)
    if (item.state.status === "running") {
      setFrame(child, `tool:${item.id}`, toolCommit(part, "start"))
      return
    }
    child.finishedTools.add(item.id)
    child.tools.delete(item.id)
    setFrame(child, `tool:${item.id}`, toolCommit(part, "final"))
  }

  const rebuild = (child: ChildState, messages: SessionMessage[]) => {
    child.frames = []
    child.text.clear()
    child.projectedText.clear()
    child.reasoning.clear()
    child.projectedReasoning.clear()
    child.finishedTools.clear()
    child.messageIDs.clear()
    child.callIDs.clear()
    for (const message of messages) {
      if (message.type === "user") {
        child.prompts.delete(message.id)
        userFrame(child, message.id, message.text)
        continue
      }
      if (message.type !== "assistant") continue
      child.messageIDs.add(message.id)
      for (const item of message.content) {
        if (item.type === "text") {
          child.text.set(item.id, item.text)
          child.projectedText.set(item.id, item.text)
          setFrame(child, `text:${item.id}`, {
            kind: "assistant",
            source: "assistant",
            text: item.text,
            phase: "progress",
            messageID: message.id,
            partID: item.id,
          })
          continue
        }
        if (item.type === "reasoning") {
          child.reasoning.set(item.id, item.text)
          child.projectedReasoning.set(item.id, item.text)
          if (input.thinking)
            setFrame(child, `reasoning:${item.id}`, {
              kind: "reasoning",
              source: "reasoning",
              text: `Thinking: ${item.text}`,
              phase: "progress",
              messageID: message.id,
              partID: item.id,
            })
          continue
        }
        childTool(child, item, message.id)
      }
      if (message.error) {
        setFrame(child, `error:${message.id}`, {
          kind: "error",
          source: "system",
          text: message.error.message,
          phase: "start",
          messageID: message.id,
        })
      }
    }
  }

  const hydrateChild = (child: ChildState): Promise<void> => {
    const existing = hydrations.get(child.sessionID)
    if (existing) return existing
    const pendingPrompts = new Map(child.prompts)
    const pendingTools = new Map(child.tools)
    let retry = false
    const task = input.sdk.message
      .list({ sessionID: child.sessionID, limit: CHILD_MESSAGE_LIMIT, order: "desc" })
      .then((response) => {
        const buffered = hydrationEvents.get(child.sessionID) ?? []
        hydrationEvents.delete(child.sessionID)
        if (hydrationOverflow.delete(child.sessionID)) {
          child.hydrated = false
          retry = true
          notifyDetail(child)
          return
        }
        for (const [id, prompt] of pendingPrompts) {
          if (!child.prompts.has(id)) child.prompts.set(id, prompt)
        }
        rebuild(child, structuredClone(response.data).toReversed() as SessionMessage[])
        for (const [id, tool] of pendingTools) {
          if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool)
        }
        for (const event of buffered) reduce(child, event)
        child.hydrated = true
        notifyDetail(child)
      })
      .catch(() => {
        hydrationEvents.delete(child.sessionID)
        hydrationOverflow.delete(child.sessionID)
      })
      .finally(() => {
        hydrations.delete(child.sessionID)
        if (retry) queueMicrotask(() => void hydrateChild(child))
      })
    hydrations.set(child.sessionID, task)
    return task
  }

  const discover = (sessionID: string) => {
    if (checked.has(sessionID) || children.has(sessionID) || sessionID === input.sessionID) return
    checked.add(sessionID)
    if (!pendingEvents.has(sessionID)) pendingEvents.set(sessionID, [])
    void input.sdk.session
      .get({ sessionID })
      .then((session) => {
        const buffered = pendingEvents.get(sessionID) ?? []
        pendingEvents.delete(sessionID)
        if (session.parentID !== input.sessionID) return
        const child = ensureChild(sessionID)
        if (session.agent) child.label = Locale.titlecase(session.agent)
        child.title = session.title
        for (const event of buffered) reduce(child, event)
        touch(child)
        input.emit()
        void hydrateChild(child)
      })
      .catch(() => {
        // Allow a later event to retry discovery after transient failures.
        pendingEvents.delete(sessionID)
        checked.delete(sessionID)
      })
  }

  const reduce = (child: ChildState, event: V2Event) => {
    if (event.type === "session.prompt.admitted") {
      child.prompts.set(event.data.inputID, event.data.prompt.text)
      return
    }
    if (event.type === "session.prompt.promoted") {
      const prompt = child.prompts.get(event.data.inputID) ?? ""
      child.prompts.delete(event.data.inputID)
      if (userFrame(child, event.data.inputID, prompt)) {
        touch(child, event.created)
        notifyDetail(child)
      }
      return
    }
    if (event.type === "session.step.started") {
      touch(child, event.created)
      if (child.label === FALLBACK_LABEL && event.data.agent) child.label = Locale.titlecase(event.data.agent)
      if (child.status !== "running") child.status = "running"
      input.emit()
      return
    }
    if (event.type === "session.text.delta") {
      const projected = child.projectedText.get(event.data.textID)
      const covered = projected?.indexOf(event.data.delta) ?? -1
      if (projected && covered >= 0) {
        child.projectedText.set(event.data.textID, projected.slice(covered + event.data.delta.length))
        return
      }
      const next = (child.text.get(event.data.textID) ?? "") + event.data.delta
      child.text.set(event.data.textID, next)
      setFrame(child, `text:${event.data.textID}`, {
        kind: "assistant",
        source: "assistant",
        text: next,
        phase: "progress",
        messageID: event.data.assistantMessageID,
        partID: event.data.textID,
      })
      touch(child, event.created)
      notifyDetail(child)
      return
    }
    if (event.type === "session.text.ended") {
      child.text.set(event.data.textID, event.data.text)
      child.projectedText.delete(event.data.textID)
      setFrame(child, `text:${event.data.textID}`, {
        kind: "assistant",
        source: "assistant",
        text: event.data.text,
        phase: "progress",
        messageID: event.data.assistantMessageID,
        partID: event.data.textID,
      })
      touch(child, event.created)
      notifyDetail(child)
      return
    }
    if (event.type === "session.reasoning.delta") {
      const projected = child.projectedReasoning.get(event.data.reasoningID)
      const covered = projected?.indexOf(event.data.delta) ?? -1
      if (projected && covered >= 0) {
        child.projectedReasoning.set(event.data.reasoningID, projected.slice(covered + event.data.delta.length))
        return
      }
      const next = (child.reasoning.get(event.data.reasoningID) ?? "") + event.data.delta
      child.reasoning.set(event.data.reasoningID, next)
      if (!input.thinking) return
      setFrame(child, `reasoning:${event.data.reasoningID}`, {
        kind: "reasoning",
        source: "reasoning",
        text: `Thinking: ${next}`,
        phase: "progress",
        messageID: event.data.assistantMessageID,
        partID: event.data.reasoningID,
      })
      notifyDetail(child)
      return
    }
    if (event.type === "session.reasoning.ended") {
      child.reasoning.set(event.data.reasoningID, event.data.text)
      child.projectedReasoning.delete(event.data.reasoningID)
      if (!input.thinking) return
      setFrame(child, `reasoning:${event.data.reasoningID}`, {
        kind: "reasoning",
        source: "reasoning",
        text: `Thinking: ${event.data.text}`,
        phase: "progress",
        messageID: event.data.assistantMessageID,
        partID: event.data.reasoningID,
      })
      notifyDetail(child)
      return
    }
    if (event.type === "session.tool.input.started") {
      if (child.finishedTools.has(event.data.callID)) return
      child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.created })
      return
    }
    if (event.type === "session.tool.called") {
      if (child.finishedTools.has(event.data.callID)) return
      const current = child.tools.get(event.data.callID)
      child.tools.set(event.data.callID, {
        name: event.data.tool,
        input: event.data.input,
        started: current?.started ?? event.created,
      })
      childTool(
        child,
        structuredClone({
          type: "tool",
          id: event.data.callID,
          name: event.data.tool,
          provider: event.data.provider,
          state: { status: "running", input: event.data.input, structured: {}, content: [] },
          time: { created: current?.started ?? event.created, ran: event.created },
        }) as SessionMessageAssistantTool,
        event.data.assistantMessageID,
      )
      touch(child, event.created)
      notifyDetail(child)
      return
    }
    if (event.type === "session.tool.success" || event.type === "session.tool.failed") {
      if (child.finishedTools.has(event.data.callID)) return
      const current = child.tools.get(event.data.callID)
      const failed = event.type === "session.tool.failed"
      childTool(
        child,
        structuredClone({
          type: "tool",
          id: event.data.callID,
          name: current?.name ?? "tool",
          provider: event.data.provider,
          state: failed
            ? {
                status: "error",
                input: current?.input ?? {},
                structured: {},
                content: [],
                error: event.data.error,
                result: event.data.result,
              }
            : {
                status: "completed",
                input: current?.input ?? {},
                structured: event.data.structured,
                content: event.data.content,
                outputPaths: event.data.outputPaths,
                result: event.data.result,
              },
          time: {
            created: current?.started ?? event.created,
            ran: current?.started,
            completed: event.created,
          },
        }) as SessionMessageAssistantTool,
        event.data.assistantMessageID,
      )
      touch(child, event.created)
      notifyDetail(child)
      return
    }
    if (event.type === "session.step.failed") {
      setFrame(child, `error:step:${event.data.assistantMessageID}`, {
        kind: "error",
        source: "system",
        text: event.data.error.message,
        phase: "start",
        messageID: event.data.assistantMessageID,
      })
      touch(child, event.created)
      notifyDetail(child)
      return
    }
    if (event.type === "session.execution.settled") {
      child.status =
        event.data.outcome === "success" ? "completed" : event.data.outcome === "interrupted" ? "cancelled" : "error"
      touch(child, event.created)
      input.emit()
    }
  }

  const mainTool = (item: SessionMessageAssistantTool, active?: Record<string, unknown>) => {
    if (item.name !== "subagent" || item.state.status !== "completed") return
    const found = childSessionID(record(item.state.structured))
    if (!found) return
    const child = ensureChild(found.sessionID)
    applyMeta(child, record(item.state.input))
    if (found.running) child.background = true
    if (child.status === "running") {
      const running = found.running && (!active || found.sessionID in active)
      child.status = running ? "running" : "completed"
    }
    touch(child, item.time.completed ?? item.time.created)
  }

  return {
    main(event) {
      if (event.type === "session.tool.called") {
        if (event.data.tool === "subagent") pendingCalls.set(event.data.callID, event.data.input)
        return
      }
      if (event.type === "session.tool.failed") {
        pendingCalls.delete(event.data.callID)
        return
      }
      if (event.type !== "session.tool.success") return
      const pending = pendingCalls.get(event.data.callID)
      pendingCalls.delete(event.data.callID)
      const found = childSessionID(record(event.data.structured))
      if (!found) return
      const child = ensureChild(found.sessionID)
      applyMeta(child, pending)
      if (found.running) {
        child.background = true
        child.status = "running"
      }
      if (!found.running && child.status === "running") child.status = "completed"
      touch(child, event.created)
      input.emit()
      if (!child.hydrated) void hydrateChild(child)
    },
    foreign(sessionID, event) {
      const child = children.get(sessionID)
      if (child) {
        if (hydrations.has(sessionID)) {
          const buffered = hydrationEvents.get(sessionID) ?? []
          if (buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event)
          else hydrationOverflow.add(sessionID)
          hydrationEvents.set(sessionID, buffered)
        }
        reduce(child, event)
        return
      }
      discover(sessionID)
      const buffered = pendingEvents.get(sessionID)
      if (buffered && buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event)
    },
    async hydrate(next) {
      for (const message of next.messages) {
        if (message.type !== "assistant") continue
        for (const item of message.content) {
          if (item.type === "tool") mainTool(item, next.active)
        }
      }
      // Family index: adopt children directly from the current session list so
      // historical subagents beyond the projected message window still get tabs.
      const family = await input.sdk.session
        .list({ parentID: input.sessionID, limit: FAMILY_LIST_LIMIT, order: "desc" })
        .then((response) => response.data)
        .catch(() => [])
      for (const session of family) {
        const child = ensureChild(session.id)
        if (session.agent && child.label === FALLBACK_LABEL) child.label = Locale.titlecase(session.agent)
        if (!child.title) child.title = session.title
        touch(child, session.time.updated)
      }
      for (const sessionID of Object.keys(next.active)) discover(sessionID)
      for (const child of children.values()) {
        // Reconnect can miss a child's settled event; the active map is the
        // authoritative live signal for still-running children.
        if (child.status === "running" && !(child.sessionID in next.active)) child.status = "completed"
      }
      const current = selected ? children.get(selected) : undefined
      if (current) await hydrateChild(current)
      if (children.size > 0) input.emit()
    },
    select(sessionID) {
      selected = sessionID
      const child = sessionID ? children.get(sessionID) : undefined
      if (child && !child.hydrated) void hydrateChild(child)
      input.emit()
    },
    snapshot() {
      const tabs = [...children.values()].map(tab).toSorted((a, b) => {
        const active = Number(b.status === "running") - Number(a.status === "running")
        if (active !== 0) return active
        return b.lastUpdatedAt - a.lastUpdatedAt
      })
      const child = selected ? children.get(selected) : undefined
      const details: Record<string, FooterSubagentDetail> = child
        ? { [child.sessionID]: { sessionID: child.sessionID, commits: child.frames.map((item) => item.commit) } }
        : {}
      return { tabs, details, permissions: [], questions: [] }
    },
  }
}
