"use client"

import {
  memo,
  Fragment,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react"
import { Plus, RefreshCw, X } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { disposeTauriListener } from "@/lib/tauri-listener"
import { useFolderContext } from "@/contexts/folder-context"
import { useTabContext } from "@/contexts/tab-context"
import { useSessionStats } from "@/contexts/session-stats-context"
import { cn } from "@/lib/utils"
import { useConnectionLifecycle } from "@/hooks/use-connection-lifecycle"
import { MessageListView } from "@/components/message/message-list-view"
import { ConversationShell } from "@/components/chat/conversation-shell"
import { AgentSelector } from "@/components/chat/agent-selector"
import { ChatInput } from "@/components/chat/chat-input"
import {
  createConversation,
  openSettingsWindow,
  updateConversationExternalId,
  updateConversationStatus,
} from "@/lib/tauri"
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
import {
  invalidateDetailCache,
  refreshDetailCache,
  useDbMessageDetail,
} from "@/hooks/use-db-message-detail"
import {
  extractUserImagesFromDraft,
  extractUserResourcesFromDraft,
  getPromptDraftDisplayText,
} from "@/lib/prompt-draft"
import type {
  AcpEvent,
  AgentType,
  ContentBlock,
  MessageTurn,
  PromptDraft,
} from "@/lib/types"
import {
  buildConversationDraftStorageKey,
  buildNewConversationDraftStorageKey,
  moveMessageInputDraft,
} from "@/lib/message-input-draft"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

interface ConversationTabViewProps {
  tabId: string
  conversationId: number | null
  agentType: AgentType
  workingDir?: string
  isActive: boolean
  reloadSignal: number
}

function buildOptimisticUserTurnFromDraft(
  draft: PromptDraft,
  attachedResourcesFallback: string
): MessageTurn {
  const displayText = getPromptDraftDisplayText(
    draft,
    attachedResourcesFallback
  )
  const resources = extractUserResourcesFromDraft(draft)
  const resourceLines = resources.map((resource) => {
    const label = resource.uri.toLowerCase().startsWith("file://")
      ? resource.name
      : `@${resource.name}`
    return `[${label}](${resource.uri})`
  })
  const text = [displayText, ...resourceLines].join("\n").trim()

  const blocks: ContentBlock[] = []
  for (const image of extractUserImagesFromDraft(draft)) {
    blocks.push({
      type: "image",
      data: image.data,
      mime_type: image.mime_type,
      uri: image.uri ?? null,
    })
  }
  blocks.push({ type: "text", text })

  return {
    id: `optimistic-${crypto.randomUUID()}`,
    role: "user",
    blocks,
    timestamp: new Date().toISOString(),
  }
}

function normalizeErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message
  return String(error)
}

function isExpectedAutoLinkError(error: unknown): boolean {
  if (!error || typeof error !== "object") return false
  return (error as { alerted?: unknown }).alerted === true
}

function buildVirtualConversationId(seed: string): number {
  let hash = 0
  for (let i = 0; i < seed.length; i += 1) {
    hash = (hash * 31 + seed.charCodeAt(i)) | 0
  }
  const normalized = Math.abs(hash) + 1
  return -normalized
}

const ConversationTabView = memo(function ConversationTabView({
  tabId,
  conversationId,
  agentType,
  workingDir,
  isActive,
  reloadSignal,
}: ConversationTabViewProps) {
  const t = useTranslations("Folder.conversation")
  const tWelcome = useTranslations("Folder.chat.welcomeInputPanel")
  const sharedT = useTranslations("Folder.chat.shared")
  const { folder, folderId, refreshConversations } = useFolderContext()
  const { bindConversationTab } = useTabContext()
  const { setSessionStats } = useSessionStats()
  const {
    acknowledgePersistedDetail,
    appendOptimisticTurn,
    migrateConversation,
    setExternalId,
    setLiveMessage,
    setSyncState,
  } = useConversationRuntime()

  const temporaryConversationId = useMemo(
    () => buildVirtualConversationId(`draft-${tabId}`),
    [tabId]
  )
  const [createdConversationId, setCreatedConversationId] = useState<
    number | null
  >(null)
  const dbConversationId = conversationId ?? createdConversationId
  const [draftAgentType, setDraftAgentType] = useState<AgentType>(agentType)
  const selectedAgent = conversationId != null ? agentType : draftAgentType
  const [modeId, setModeId] = useState<string | null>(null)
  const [sendSignal, setSendSignal] = useState(0)
  const [agentsLoaded, setAgentsLoaded] = useState(false)
  const [usableAgentCount, setUsableAgentCount] = useState(0)
  const [agentConnectError, setAgentConnectError] = useState<string | null>(
    null
  )
  const [hasSentMessage, setHasSentMessage] = useState(false)

  const hasPersistedConversation = dbConversationId != null
  const canAutoConnect =
    hasPersistedConversation || (agentsLoaded && usableAgentCount > 0)
  const effectiveConversationId = dbConversationId ?? temporaryConversationId

  const latestReloadSignal = useRef(reloadSignal)
  const pendingReloadState = useRef<{
    signal: number
    sawLoading: boolean
  } | null>(null)
  const dbConvIdRef = useRef<number | null>(conversationId)
  const statusUpdatedRef = useRef(false)
  const selectedAgentRef = useRef(selectedAgent)
  const createConversationPendingRef = useRef(false)
  const reconcileTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const externalIdSavedRef = useRef(false)
  const sessionIdRef = useRef<string | null>(null)

  useEffect(() => {
    dbConvIdRef.current = dbConversationId
  }, [dbConversationId])

  useEffect(() => {
    selectedAgentRef.current = selectedAgent
  }, [selectedAgent])

  const {
    detail,
    loading: detailLoading,
    error: detailError,
    refetch: refetchConversationDetail,
  } = useDbMessageDetail(effectiveConversationId)

  useEffect(() => {
    if (!isActive) return
    setSessionStats(detail?.session_stats ?? null)
  }, [detail?.session_stats, isActive, setSessionStats])

  const externalId = detail?.summary.external_id ?? undefined
  const draftStorageKey = useMemo(() => {
    if (dbConversationId != null) {
      return buildConversationDraftStorageKey(selectedAgent, dbConversationId)
    }
    return buildNewConversationDraftStorageKey({ folderId })
  }, [dbConversationId, folderId, selectedAgent])
  const workingDirForConnection = useMemo(() => {
    if (dbConversationId != null) {
      return detailLoading ? undefined : folder?.path
    }
    return workingDir ?? folder?.path
  }, [dbConversationId, detailLoading, folder?.path, workingDir])

  const {
    conn,
    modeLoading,
    configOptionsLoading,
    autoConnectError,
    handleFocus,
    handleSend: lifecycleSend,
    handleSetConfigOption,
    handleCancel,
    handleRespondPermission,
  } = useConnectionLifecycle({
    contextKey: tabId,
    agentType: selectedAgent,
    isActive: isActive && canAutoConnect,
    workingDir: workingDirForConnection,
    sessionId: dbConversationId != null ? externalId : undefined,
  })
  const {
    status: connStatus,
    connect: connConnect,
    disconnect: connDisconnect,
    sessionId: connSessionId,
  } = conn
  const connStatusRef = useRef(connStatus)
  useEffect(() => {
    connStatusRef.current = connStatus
  }, [connStatus])
  const isConnecting =
    connStatus === "connecting" || connStatus === "downloading"
  const connectionModes = useMemo(
    () => conn.modes?.available_modes ?? [],
    [conn.modes?.available_modes]
  )
  const connectionConfigOptions = useMemo(
    () => conn.configOptions ?? [],
    [conn.configOptions]
  )
  const connectionCommands = useMemo(
    () => conn.availableCommands ?? [],
    [conn.availableCommands]
  )
  const selectedModeId = useMemo(() => {
    if (connectionModes.length === 0) return null
    if (modeId && connectionModes.some((mode) => mode.id === modeId)) {
      return modeId
    }
    return conn.modes?.current_mode_id ?? connectionModes[0]?.id ?? null
  }, [conn.modes?.current_mode_id, connectionModes, modeId])

  const clearReconcileTimer = useCallback(() => {
    if (!reconcileTimerRef.current) return
    clearTimeout(reconcileTimerRef.current)
    reconcileTimerRef.current = null
  }, [])

  const refreshFromDb = useCallback(
    async (refreshConversationId: number) => {
      try {
        const refreshed = await refreshDetailCache(refreshConversationId)
        // Skip ACK during prompting to avoid clearing liveMessage /
        // resetting syncState while streaming. The useEffect with the
        // connStatus === "prompting" guard will handle it naturally
        // once prompting ends.
        if (connStatusRef.current === "prompting") return
        acknowledgePersistedDetail(refreshConversationId, refreshed)
      } catch (error) {
        setSyncState(refreshConversationId, "failed")
        console.error(
          "[ConversationTabView] refresh detail cache failed:",
          error
        )
      }
    },
    [acknowledgePersistedDetail, setSyncState]
  )

  useEffect(() => {
    if (connSessionId) {
      sessionIdRef.current = connSessionId
    }
  }, [connSessionId])

  useEffect(() => {
    setLiveMessage(effectiveConversationId, conn.liveMessage ?? null)
    return () => {
      setLiveMessage(effectiveConversationId, null)
    }
  }, [conn.liveMessage, effectiveConversationId, setLiveMessage])

  useEffect(() => {
    if (!dbConversationId) return
    setExternalId(dbConversationId, detail?.summary.external_id ?? null)
  }, [dbConversationId, detail?.summary.external_id, setExternalId])

  useEffect(() => {
    if (!dbConversationId) return
    if (!connSessionId) return
    setExternalId(dbConversationId, connSessionId)
  }, [connSessionId, dbConversationId, setExternalId])

  const trySaveExternalId = useCallback(() => {
    if (
      externalIdSavedRef.current ||
      !dbConvIdRef.current ||
      !sessionIdRef.current
    ) {
      return
    }
    externalIdSavedRef.current = true
    updateConversationExternalId(
      dbConvIdRef.current,
      sessionIdRef.current
    ).catch((e: unknown) =>
      console.error("[ConversationTabView] update external_id:", e)
    )
  }, [])

  useEffect(() => {
    if (connSessionId) {
      trySaveExternalId()
    }
  }, [connSessionId, trySaveExternalId])

  useEffect(() => {
    if (!dbConversationId) return
    if (!detail) return
    if (connStatus === "prompting") return
    acknowledgePersistedDetail(dbConversationId, detail)
  }, [acknowledgePersistedDetail, connStatus, dbConversationId, detail])

  const prevConnStatusRef = useRef(connStatus)
  useEffect(() => {
    const wasPrompting = prevConnStatusRef.current === "prompting"
    prevConnStatusRef.current = connStatus
    if (!wasPrompting || connStatus === "prompting") return

    setSyncState(effectiveConversationId, "reconciling")
    const persistedId = dbConvIdRef.current
    if (!persistedId) return

    invalidateDetailCache(persistedId)
    clearReconcileTimer()
    reconcileTimerRef.current = setTimeout(() => {
      void refreshFromDb(persistedId)
    }, 1200)

    if (connStatus !== "disconnected" && connStatus !== "error") {
      updateConversationStatus(persistedId, "pending_review")
        .then(() => refreshConversations())
        .catch((e: unknown) =>
          console.error("[ConversationTabView] update status:", e)
        )
    }
  }, [
    clearReconcileTimer,
    connStatus,
    effectiveConversationId,
    refreshConversations,
    refreshFromDb,
    setSyncState,
  ])

  useEffect(() => {
    if (connStatus === "connected" || connStatus === "prompting") {
      statusUpdatedRef.current = false
      return
    }
    if (statusUpdatedRef.current) return
    const persistedId = dbConvIdRef.current
    if (!persistedId) return
    if (connStatus === "disconnected") {
      statusUpdatedRef.current = true
      updateConversationStatus(persistedId, "completed")
        .then(() => refreshConversations())
        .catch((e) => console.error("[ConversationTabView] update status:", e))
    } else if (connStatus === "error") {
      statusUpdatedRef.current = true
      updateConversationStatus(persistedId, "cancelled")
        .then(() => refreshConversations())
        .catch((e) => console.error("[ConversationTabView] update status:", e))
    }
  }, [connStatus, refreshConversations])

  useEffect(() => {
    if (dbConversationId == null) return
    if (reloadSignal === latestReloadSignal.current) return
    latestReloadSignal.current = reloadSignal
    pendingReloadState.current = {
      signal: reloadSignal,
      sawLoading: false,
    }
    refetchConversationDetail()
  }, [dbConversationId, reloadSignal, refetchConversationDetail])

  useEffect(() => {
    const pending = pendingReloadState.current
    if (!pending) return

    if (detailLoading) {
      pending.sawLoading = true
      return
    }

    if (!pending.sawLoading) return

    pendingReloadState.current = null

    if (detailError) {
      toast.error(t("reloadFailed", { message: detailError }))
      return
    }

    toast.success(t("reloaded"))
  }, [detailLoading, detailError, t])

  useEffect(() => clearReconcileTimer, [clearReconcileTimer])

  const handleSend = useCallback(
    (draft: PromptDraft, selectedModeIdArg?: string | null) => {
      if (!hasPersistedConversation && !canAutoConnect) {
        setAgentConnectError(tWelcome("enableAgentFirstPlaceholder"))
        return
      }
      if (connStatus !== "connected") return

      const optimisticTurn = buildOptimisticUserTurnFromDraft(
        draft,
        sharedT("attachedResources")
      )
      appendOptimisticTurn(
        effectiveConversationId,
        optimisticTurn,
        optimisticTurn.id
      )
      setSendSignal((prev) => prev + 1)
      setSyncState(effectiveConversationId, "awaiting_persist")
      setHasSentMessage(true)
      lifecycleSend(draft, selectedModeIdArg)

      const persistedId = dbConvIdRef.current
      if (persistedId) {
        updateConversationStatus(persistedId, "in_progress")
          .then(() => refreshConversations())
          .catch((e: unknown) =>
            console.error("[ConversationTabView] update status:", e)
          )
        statusUpdatedRef.current = false
        return
      }

      if (createConversationPendingRef.current) return
      createConversationPendingRef.current = true
      const title = getPromptDraftDisplayText(
        draft,
        sharedT("attachedResources")
      ).slice(0, 80)
      createConversation(folderId, selectedAgent, title)
        .then((newConversationId) => {
          dbConvIdRef.current = newConversationId
          setCreatedConversationId(newConversationId)
          migrateConversation(temporaryConversationId, newConversationId)
          setExternalId(newConversationId, sessionIdRef.current ?? null)
          bindConversationTab(tabId, newConversationId, selectedAgent, title)
          moveMessageInputDraft(
            buildNewConversationDraftStorageKey({ folderId }),
            buildConversationDraftStorageKey(selectedAgent, newConversationId)
          )
          trySaveExternalId()
          statusUpdatedRef.current = false
          updateConversationStatus(newConversationId, "in_progress")
            .then(() => refreshConversations())
            .catch((e: unknown) =>
              console.error("[ConversationTabView] update status:", e)
            )
          void refreshFromDb(newConversationId)
        })
        .catch((e: unknown) =>
          console.error("[ConversationTabView] create conversation:", e)
        )
        .finally(() => {
          createConversationPendingRef.current = false
        })
    },
    [
      appendOptimisticTurn,
      bindConversationTab,
      canAutoConnect,
      connStatus,
      effectiveConversationId,
      folderId,
      hasPersistedConversation,
      lifecycleSend,
      migrateConversation,
      refreshConversations,
      refreshFromDb,
      selectedAgent,
      setExternalId,
      setSyncState,
      sharedT,
      tWelcome,
      tabId,
      temporaryConversationId,
      trySaveExternalId,
    ]
  )

  const handleOpenAgentsSettings = useCallback(() => {
    openSettingsWindow("agents", { agentType: selectedAgent }).catch((err) => {
      console.error(
        "[ConversationTabView] failed to open settings window:",
        err
      )
    })
  }, [selectedAgent])

  const handleAgentSelect = useCallback(
    (nextAgentType: AgentType) => {
      if (nextAgentType === selectedAgentRef.current) return
      if (dbConvIdRef.current) return

      setDraftAgentType(nextAgentType)
      setModeId(null)
      setAgentConnectError(null)
      connDisconnect()
        .catch((e) =>
          console.error("[ConversationTabView] disconnect old agent:", e)
        )
        .finally(() => {
          if (!workingDirForConnection) return
          connConnect(nextAgentType, workingDirForConnection, undefined, {
            source: "auto_link",
          })
            .then(() => {
              setAgentConnectError(null)
            })
            .catch((e) => {
              setAgentConnectError(normalizeErrorMessage(e))
              if (!isExpectedAutoLinkError(e)) {
                console.error("[ConversationTabView] switch agent:", e)
              }
            })
        })
    },
    [connConnect, connDisconnect, workingDirForConnection]
  )

  const handleAnswerQuestion = useCallback(
    (answer: string) => {
      if (connStatus !== "connected") return
      const optimisticTurn: MessageTurn = {
        id: `optimistic-${crypto.randomUUID()}`,
        role: "user",
        blocks: [{ type: "text", text: answer }],
        timestamp: new Date().toISOString(),
      }
      appendOptimisticTurn(
        effectiveConversationId,
        optimisticTurn,
        optimisticTurn.id
      )
      setSendSignal((prev) => prev + 1)
      setSyncState(effectiveConversationId, "awaiting_persist")
      lifecycleSend(
        { blocks: [{ type: "text", text: answer }], displayText: answer },
        null
      )
    },
    [
      appendOptimisticTurn,
      connStatus,
      effectiveConversationId,
      lifecycleSend,
      setSyncState,
    ]
  )

  const showDraftHeader = !hasPersistedConversation
  const isWelcomeMode = showDraftHeader && !hasSentMessage

  const messageListNode = (
    <MessageListView
      conversationId={effectiveConversationId}
      agentType={selectedAgent}
      connStatus={connStatus}
      isActive={isActive}
      sendSignal={sendSignal}
      sessionStats={detail?.session_stats ?? null}
      detailLoading={detailLoading}
      detailError={detailError}
      hideEmptyState={showDraftHeader}
    />
  )

  return (
    <ConversationShell
      status={connStatus}
      promptCapabilities={conn.promptCapabilities}
      defaultPath={workingDirForConnection}
      error={conn.error}
      pendingPermission={conn.pendingPermission}
      pendingQuestion={conn.pendingQuestion}
      onFocus={handleFocus}
      onSend={handleSend}
      onCancel={handleCancel}
      onRespondPermission={handleRespondPermission}
      onAnswerQuestion={handleAnswerQuestion}
      modes={connectionModes}
      configOptions={connectionConfigOptions}
      modeLoading={modeLoading}
      configOptionsLoading={configOptionsLoading}
      selectedModeId={selectedModeId}
      onModeChange={setModeId}
      onConfigOptionChange={handleSetConfigOption}
      availableCommands={connectionCommands}
      attachmentTabId={tabId}
      draftStorageKey={draftStorageKey}
      hideInput={isWelcomeMode}
      isActive={isActive}
    >
      {isWelcomeMode ? (
        <div className="flex h-full min-h-0 flex-col items-center justify-center">
          <div className="flex w-full max-w-2xl flex-col gap-4 px-4">
            <AgentSelector
              defaultAgentType={selectedAgent}
              onSelect={handleAgentSelect}
              onAgentsLoaded={(agents) => {
                setAgentsLoaded(true)
                setUsableAgentCount(
                  agents.filter((agent) => agent.enabled && agent.available)
                    .length
                )
              }}
              onOpenAgentsSettings={handleOpenAgentsSettings}
              disabled={isConnecting || dbConversationId != null}
            />
            {autoConnectError || agentConnectError ? (
              <button
                type="button"
                onClick={handleOpenAgentsSettings}
                className="w-full cursor-pointer rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-center text-xs text-destructive transition-colors hover:bg-destructive/10"
              >
                <div
                  className="overflow-hidden text-ellipsis whitespace-nowrap text-center"
                  title={autoConnectError ?? agentConnectError ?? ""}
                >
                  {autoConnectError ?? agentConnectError}
                </div>
              </button>
            ) : null}
            <ChatInput
              status={connStatus}
              promptCapabilities={conn.promptCapabilities}
              defaultPath={workingDirForConnection}
              onFocus={handleFocus}
              onSend={handleSend}
              onCancel={handleCancel}
              modes={connectionModes}
              configOptions={connectionConfigOptions}
              modeLoading={modeLoading}
              configOptionsLoading={configOptionsLoading}
              selectedModeId={selectedModeId}
              onModeChange={setModeId}
              onConfigOptionChange={handleSetConfigOption}
              availableCommands={connectionCommands}
              attachmentTabId={tabId}
              draftStorageKey={draftStorageKey}
              isActive={isActive}
            />
          </div>
        </div>
      ) : showDraftHeader ? (
        <div className="flex h-full min-h-0 flex-col">
          <div className="px-4 pt-3 pb-2">
            <AgentSelector
              defaultAgentType={selectedAgent}
              onSelect={handleAgentSelect}
              onAgentsLoaded={(agents) => {
                setAgentsLoaded(true)
                setUsableAgentCount(
                  agents.filter((agent) => agent.enabled && agent.available)
                    .length
                )
              }}
              onOpenAgentsSettings={handleOpenAgentsSettings}
              disabled={isConnecting || dbConversationId != null}
            />
            {autoConnectError || agentConnectError ? (
              <button
                type="button"
                onClick={handleOpenAgentsSettings}
                className="mt-2 w-full cursor-pointer rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-center text-xs text-destructive transition-colors hover:bg-destructive/10"
              >
                <div
                  className="overflow-hidden text-ellipsis whitespace-nowrap text-center"
                  title={autoConnectError ?? agentConnectError ?? ""}
                >
                  {autoConnectError ?? agentConnectError}
                </div>
              </button>
            ) : null}
          </div>
          <div className="min-h-0 flex-1">{messageListNode}</div>
        </div>
      ) : (
        messageListNode
      )}
    </ConversationShell>
  )
})

export function ConversationDetailPanel() {
  const t = useTranslations("Folder.conversation")
  const {
    acknowledgePersistedDetail,
    getConversationIdByExternalId,
    setSyncState,
  } = useConversationRuntime()
  const { folder, newConversation, conversations, refreshConversations } =
    useFolderContext()
  const {
    tabs,
    activeTabId,
    isTileMode,
    openNewConversationTab,
    closeTab,
    switchTab,
  } = useTabContext()
  const [reloadByTabId, setReloadByTabId] = useState<Record<string, number>>({})
  const tabsRef = useRef(tabs)
  const conversationsRef = useRef(conversations)
  const pendingClosedConversationIdsRef = useRef<Set<number>>(new Set())
  const pendingRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
    null
  )

  useEffect(() => {
    tabsRef.current = tabs
  }, [tabs])

  useEffect(() => {
    conversationsRef.current = conversations
  }, [conversations])

  const flushClosedConversationRefresh = useCallback(() => {
    const conversationIds = Array.from(pendingClosedConversationIdsRef.current)
    if (conversationIds.length === 0) return
    pendingClosedConversationIdsRef.current.clear()

    void (async () => {
      await Promise.all(
        conversationIds.map(async (conversationId) => {
          const summary =
            conversationsRef.current.find(
              (item) => item.id === conversationId
            ) ?? null
          if (summary?.status === "in_progress") {
            try {
              await updateConversationStatus(conversationId, "pending_review")
            } catch (error) {
              console.error(
                "[ConversationDetailPanel] background update status failed:",
                error
              )
            }
          }

          try {
            const detail = await refreshDetailCache(conversationId)
            acknowledgePersistedDetail(conversationId, detail)
          } catch (error) {
            setSyncState(conversationId, "failed")
            console.error(
              "[ConversationDetailPanel] background detail cache refresh failed:",
              error
            )
          }
        })
      )

      refreshConversations()
    })()
  }, [acknowledgePersistedDetail, refreshConversations, setSyncState])

  const scheduleClosedConversationRefresh = useCallback(
    (conversationId: number) => {
      pendingClosedConversationIdsRef.current.add(conversationId)
      if (pendingRefreshTimerRef.current) return

      // Delay briefly so local session file writes can settle.
      pendingRefreshTimerRef.current = setTimeout(() => {
        pendingRefreshTimerRef.current = null
        flushClosedConversationRefresh()
      }, 1200)
    },
    [flushClosedConversationRefresh]
  )

  useEffect(() => {
    let cancelled = false
    let unlisten: (() => void | Promise<void>) | null = null
    const pendingClosedConversationIds = pendingClosedConversationIdsRef.current

    void import("@tauri-apps/api/event")
      .then(({ listen }) =>
        listen<AcpEvent>("acp://event", (event) => {
          const payload = event.payload
          if (payload.type !== "turn_complete") return

          const runtimeConversationId = getConversationIdByExternalId(
            payload.session_id
          )
          const summary = conversationsRef.current.find(
            (item) => item.external_id === payload.session_id
          )
          const matchedConversationId =
            runtimeConversationId ?? summary?.id ?? null
          if (!matchedConversationId) return

          const isOpenInTabs = tabsRef.current.some(
            (tab) => tab.conversationId === matchedConversationId
          )
          if (isOpenInTabs) return

          invalidateDetailCache(matchedConversationId)
          setSyncState(matchedConversationId, "reconciling")

          scheduleClosedConversationRefresh(matchedConversationId)
        })
      )
      .then((dispose) => {
        if (cancelled) {
          disposeTauriListener(
            dispose,
            "ConversationDetailPanel.backgroundRefresh"
          )
          return
        }
        unlisten = dispose
      })
      .catch(() => {
        // Ignore when non-tauri runtime.
      })

    return () => {
      cancelled = true
      if (pendingRefreshTimerRef.current) {
        clearTimeout(pendingRefreshTimerRef.current)
        pendingRefreshTimerRef.current = null
      }
      pendingClosedConversationIds.clear()
      disposeTauriListener(
        unlisten,
        "ConversationDetailPanel.backgroundRefresh"
      )
    }
  }, [
    getConversationIdByExternalId,
    acknowledgePersistedDetail,
    scheduleClosedConversationRefresh,
    setSyncState,
  ])

  const hasNoTabs = tabs.length === 0 && !activeTabId
  const activeConversationTab = useMemo(
    () =>
      tabs.find(
        (tab) => tab.id === activeTabId && tab.conversationId != null
      ) ?? null,
    [tabs, activeTabId]
  )
  const canReloadActiveConversation = activeConversationTab != null
  const handleReloadActiveConversation = useCallback(() => {
    if (!activeConversationTab) return
    setReloadByTabId((prev) => ({
      ...prev,
      [activeConversationTab.id]: (prev[activeConversationTab.id] ?? 0) + 1,
    }))
  }, [activeConversationTab])

  const handleNewConversation = useCallback(() => {
    if (!folder) return
    openNewConversationTab("codex", folder.path)
  }, [folder, openNewConversationTab])

  const handleCloseActiveTab = useCallback(() => {
    if (!activeTabId) return
    closeTab(activeTabId)
  }, [activeTabId, closeTab])

  // Ensure no-tab state is immediately bridged to a real new-conversation tab.
  useEffect(() => {
    if (!folder) return

    if (hasNoTabs) {
      openNewConversationTab(
        newConversation?.agentType ?? "codex",
        newConversation?.workingDir ?? folder.path
      )
    }
  }, [
    folder,
    hasNoTabs,
    newConversation?.agentType,
    newConversation?.workingDir,
    openNewConversationTab,
  ])

  const canTile = isTileMode && tabs.length > 1

  // Empty state: no tabs at all — show full-screen welcome
  if (hasNoTabs) {
    return null
  }

  return (
    <ContextMenu>
      <ContextMenuTrigger asChild>
        <div className="relative h-full min-h-0 overflow-hidden">
          {canTile ? (
            <ResizablePanelGroup direction="horizontal">
              {tabs.map((tab, index) => {
                const active = tab.id === activeTabId
                return (
                  <Fragment key={tab.id}>
                    {index > 0 && <ResizableHandle withHandle />}
                    <ResizablePanel
                      id={`tile-${tab.id}`}
                      order={index}
                      minSize={15}
                    >
                      <div
                        className={cn(
                          "h-full",
                          active
                            ? "bg-gradient-to-b from-muted/50 to-transparent"
                            : ""
                        )}
                        onPointerDownCapture={() => {
                          if (!active) switchTab(tab.id)
                        }}
                      >
                        <ConversationTabView
                          tabId={tab.id}
                          conversationId={tab.conversationId}
                          agentType={tab.agentType}
                          workingDir={tab.workingDir ?? folder?.path}
                          isActive={active}
                          reloadSignal={reloadByTabId[tab.id] ?? 0}
                        />
                      </div>
                    </ResizablePanel>
                  </Fragment>
                )
              })}
            </ResizablePanelGroup>
          ) : (
            tabs.map((tab) => {
              const active = tab.id === activeTabId
              return (
                <div
                  key={tab.id}
                  className={
                    active
                      ? "h-full"
                      : "absolute inset-0 invisible pointer-events-none"
                  }
                >
                  <ConversationTabView
                    tabId={tab.id}
                    conversationId={tab.conversationId}
                    agentType={tab.agentType}
                    workingDir={tab.workingDir ?? folder?.path}
                    isActive={active}
                    reloadSignal={reloadByTabId[tab.id] ?? 0}
                  />
                </div>
              )
            })
          )}
        </div>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem
          disabled={!canReloadActiveConversation}
          onSelect={handleReloadActiveConversation}
        >
          <RefreshCw className="h-4 w-4" />
          {t("reload")}
        </ContextMenuItem>
        <ContextMenuItem
          disabled={!folder?.path}
          onSelect={handleNewConversation}
        >
          <Plus className="h-4 w-4" />
          {t("newConversation")}
        </ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem
          disabled={!activeTabId}
          onSelect={handleCloseActiveTab}
        >
          <X className="h-4 w-4" />
          {t("closeConversation")}
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
