import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ArrowsInLineHorizontal, ArrowsOutLineHorizontal, CaretDown, GearSix, Plus, SidebarSimple, X } from '@phosphor-icons/react'
import {
  DndContext,
  PointerSensor,
  closestCenter,
  type DragEndEvent,
  useSensor,
  useSensors,
} from '@dnd-kit/core'
import {
  SortableContext,
  horizontalListSortingStrategy,
  useSortable,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import {
  DEFAULT_AI_AGENT,
  getAiAgentAvailability,
  type AiAgentId,
  type AiAgentReadiness,
  type AiAgentsStatus,
} from '../lib/aiAgents'
import {
  aiTargetReady,
  targetAgent,
  type AiModelProvider,
  type AiTarget,
} from '../lib/aiTargets'
import {
  aiAgentPermissionModeLabels,
  type AiAgentPermissionMode,
} from '../lib/aiAgentPermissionMode'
import {
  type VaultAiGuidanceStatus,
} from '../lib/vaultAiGuidance'
import { translate, type AppLocale } from '../lib/i18n'
import { trackAiWorkspaceChatTitled, trackAiWorkspaceSidebarToggled } from '../lib/productAnalytics'
import type { AgentStatus, AiAgentMessage } from '../hooks/useCliAiAgent'
import type { AiWorkspaceConversationSetting } from '../types'
import type { NoteListItem } from '../utils/ai-context'
import type { VaultEntry } from '../types'
import { NEW_AI_CHAT_EVENT } from '../utils/aiPromptBridge'
import { type GenerateAiConversationTitleRequest } from '../utils/aiConversationTitle'
import { cloneAiWorkspaceSessionUntilMessage } from '../lib/aiWorkspaceSessionStore'
import { AiPanelView } from './AiPanel'
import { GuidanceWarning, WorkspaceHeader } from './AiWorkspaceChrome'
import { WorkspaceResizeHandles } from './AiWorkspaceResizeHandles'
import { AiAgentIcon } from './AiAgentIcon'
import { ConversationSidebar } from './AiWorkspaceSidebar'
import { ResizeHandle } from './ResizeHandle'
import { useAiPanelController } from './useAiPanelController'
import { buildAiWorkspaceTargetGroups, type AiWorkspaceTargetGroups } from './aiWorkspaceTargetGroups'
import {
  activeConversationForState,
  canArchiveConversation,
  firstTarget,
  flatTargets,
  resolveTarget,
  useConversations,
  type AiConversation,
} from './aiWorkspaceConversations'
import {
  useAiWorkspaceSizing,
  workspaceClassName,
  workspaceStyle,
  type AiWorkspaceMode,
  type AiWorkspaceSizing,
} from './aiWorkspaceSizing'

export type { AiConversation } from './aiWorkspaceConversations'

interface AiWorkspaceProps {
  activeEntry?: VaultEntry | null
  activeNoteContent?: string | null
  aiAgentsStatus: AiAgentsStatus
  aiModelProviders?: AiModelProvider[]
  conversationSettings?: AiWorkspaceConversationSetting[] | null
  conversationSettingsReady?: boolean
  defaultAiAgent?: AiAgentId
  defaultAiAgentReadiness?: AiAgentReadiness
  defaultAiAgentReady?: boolean
  defaultAiTarget?: AiTarget
  entries?: VaultEntry[]
  initialActiveConversationId?: string
  locale?: AppLocale
  mode?: 'docked' | 'side' | 'window'
  noteList?: NoteListItem[]
  noteListFilter?: { type: string | null; query: string }
  onActiveConversationChange?: (id: string) => void
  onClose: () => void
  onConversationSettingsChange?: (conversations: AiWorkspaceConversationSetting[]) => void
  onDock?: () => void
  onFileCreated?: (relativePath: string) => void
  onFileModified?: (relativePath: string) => void
  onOpenAiSettings?: () => void
  onOpenNote?: (path: string) => void
  onPopOut?: (context?: { activeConversationId?: string }) => void
  onRestoreVaultAiGuidance?: () => void
  onUnsupportedAiPaste?: (message: string) => void
  onVaultChanged?: () => void
  open: boolean
  openTabs?: VaultEntry[]
  vaultAiGuidanceStatus?: VaultAiGuidanceStatus
  vaultPath: string
  vaultPaths?: string[]
}

function agentReadinessForTarget(target: AiTarget, statuses: AiAgentsStatus): AiAgentReadiness {
  if (target.kind === 'api_model') return 'ready'
  const status = getAiAgentAvailability(statuses, target.agent).status
  if (status === 'checking') return 'checking'
  return status === 'installed' ? 'ready' : 'missing'
}

function TargetGroup({ label, targets }: { label: string; targets: AiTarget[] }) {
  if (targets.length === 0) return null

  return (
    <>
      <DropdownMenuLabel>{label}</DropdownMenuLabel>
      {targets.map((target) => (
        <DropdownMenuRadioItem key={target.id} value={target.id} className="gap-2">
          {target.kind === 'agent' ? <AiAgentIcon agent={target.agent} size={16} /> : null}
          <span className="truncate">{target.label}</span>
        </DropdownMenuRadioItem>
      ))}
    </>
  )
}

function TargetPickerTrigger({
  compact,
  disabled,
  hasTargets,
  locale,
  selectedTarget,
}: {
  compact: boolean
  disabled: boolean
  hasTargets: boolean
  locale: AppLocale
  selectedTarget: AiTarget
}) {
  return (
    <DropdownMenuTrigger asChild>
      <Button
        type="button"
        variant={compact ? 'ghost' : 'outline'}
        size={compact ? 'xs' : 'sm'}
        className={cn(
          'justify-between gap-1.5 text-muted-foreground hover:text-foreground',
          compact ? 'max-w-[150px] rounded-full px-2 text-[12px]' : 'max-w-[240px] gap-2',
        )}
        disabled={disabled || !hasTargets}
        aria-label={translate(locale, 'ai.workspace.targetLabel')}
        data-testid="ai-workspace-target-trigger"
      >
        {selectedTarget.kind === 'agent' ? <AiAgentIcon agent={selectedTarget.agent} size={compact ? 14 : 16} /> : null}
        <span className="truncate">{selectedTarget.shortLabel}</span>
        <CaretDown size={compact ? 12 : 13} />
      </Button>
    </DropdownMenuTrigger>
  )
}

function TargetPickerContent({
  groups,
  hasTargets,
  locale,
  onSelectTarget,
  selectedTarget,
  side,
}: {
  groups: AiWorkspaceTargetGroups
  hasTargets: boolean
  locale: AppLocale
  selectedTarget: AiTarget
  onSelectTarget: (targetId: string) => void
  side: 'bottom' | 'top'
}) {
  const hasLocalAgentsSeparator = groups.localAgents.length > 0
    && (groups.localModels.length > 0 || groups.apiModels.length > 0)
  const hasLocalModelsSeparator = groups.localModels.length > 0 && groups.apiModels.length > 0

  return (
    <DropdownMenuContent align="start" side={side} className="min-w-[280px]">
      {hasTargets ? (
        <DropdownMenuRadioGroup value={selectedTarget.id} onValueChange={onSelectTarget}>
          <TargetGroup label={translate(locale, 'ai.workspace.targetLocalAgents')} targets={groups.localAgents} />
          {hasLocalAgentsSeparator && <DropdownMenuSeparator />}
          <TargetGroup label={translate(locale, 'ai.workspace.targetLocalModels')} targets={groups.localModels} />
          {hasLocalModelsSeparator && <DropdownMenuSeparator />}
          <TargetGroup label={translate(locale, 'ai.workspace.targetApiModels')} targets={groups.apiModels} />
        </DropdownMenuRadioGroup>
      ) : (
        <DropdownMenuItem disabled>{translate(locale, 'ai.workspace.noTargets')}</DropdownMenuItem>
      )}
    </DropdownMenuContent>
  )
}

function TargetPicker({
  compact = false,
  disabled,
  groups,
  locale,
  selectedTarget,
  side = 'bottom',
  onSelectTarget,
}: {
  compact?: boolean
  disabled: boolean
  groups: AiWorkspaceTargetGroups
  locale: AppLocale
  selectedTarget: AiTarget
  side?: 'bottom' | 'top'
  onSelectTarget: (targetId: string) => void
}) {
  const hasTargets = flatTargets(groups).length > 0

  return (
    <DropdownMenu>
      <TargetPickerTrigger
        compact={compact}
        disabled={disabled}
        hasTargets={hasTargets}
        locale={locale}
        selectedTarget={selectedTarget}
      />
      <TargetPickerContent
        groups={groups}
        hasTargets={hasTargets}
        locale={locale}
        selectedTarget={selectedTarget}
        side={side}
        onSelectTarget={onSelectTarget}
      />
    </DropdownMenu>
  )
}

function PermissionPicker({
  compact = false,
  disabled,
  locale,
  permissionMode,
  side = 'bottom',
  targetKind,
  onChange,
}: {
  compact?: boolean
  disabled: boolean
  locale: AppLocale
  permissionMode: AiAgentPermissionMode
  side?: 'bottom' | 'top'
  targetKind: AiTarget['kind']
  onChange: (mode: AiAgentPermissionMode) => void
}) {
  if (targetKind === 'api_model') {
    return (
      <Button type="button" variant={compact ? 'ghost' : 'outline'} size={compact ? 'xs' : 'sm'} disabled className="rounded-full px-2 text-[12px] text-muted-foreground">
        {translate(locale, 'ai.panel.mode.chat')}
      </Button>
    )
  }

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          type="button"
          variant={compact ? 'ghost' : 'outline'}
          size={compact ? 'xs' : 'sm'}
          className={cn(
            'justify-between text-muted-foreground hover:text-foreground',
            compact ? 'rounded-full px-2 text-[12px]' : 'gap-2',
          )}
          disabled={disabled}
          aria-label={translate(locale, 'ai.workspace.permissionMode')}
          data-testid="ai-workspace-permission-trigger"
        >
          {aiAgentPermissionModeLabels(permissionMode, locale).control}
          <CaretDown size={compact ? 12 : 13} />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" side={side} className="min-w-[180px]">
        {(['safe', 'power_user'] as const).map((mode) => (
          <DropdownMenuItem key={mode} onSelect={() => onChange(mode)}>
            {aiAgentPermissionModeLabels(mode, locale).control}
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

type ConversationSessionProps = {
  active: boolean
  activeEntry?: VaultEntry | null
  activeNoteContent?: string | null
  aiAgentsStatus: AiAgentsStatus
  conversation: AiConversation
  defaultAiAgentReady: boolean
  entries?: VaultEntry[]
  groups: AiWorkspaceTargetGroups
  locale: AppLocale
  mode: AiWorkspaceMode
  noteList?: NoteListItem[]
  noteListFilter?: { type: string | null; query: string }
  onArchive: () => void
  onClose: () => void
  onDock?: () => void
  onFileCreated?: (relativePath: string) => void
  onFileModified?: (relativePath: string) => void
  onForkMessage?: (messageId: string) => void
  onMessageHistoryScrollStateChange?: (scrolled: boolean) => void
  onOpenAiSettings?: () => void
  onOpenNote?: (path: string) => void
  onPopOut?: () => void
  onRestoreVaultAiGuidance?: () => void
  onSelectTarget: (targetId: string) => void
  onStatusChange: (id: string, status: AgentStatus) => void
  onPromptSubmitted: (id: string) => void
  onTitleFromAnswer: (request: GenerateAiConversationTitleRequest & { id: string }) => void
  onUnsupportedAiPaste?: (message: string) => void
  onVaultChanged?: () => void
  openTabs?: VaultEntry[]
  target: AiTarget
  vaultAiGuidanceStatus?: VaultAiGuidanceStatus
  vaultPath: string
  vaultPaths?: string[]
}

function firstCompletedAssistantMessage(messages: AiAgentMessage[]): AiAgentMessage | undefined {
  return messages.find((message) => (
    !message.localMarker
    && !message.isStreaming
    && !!message.userMessage.trim()
    && !!message.response?.trim()
  ))
}

function useGeneratedConversationTitle({
  aiAgentsStatus,
  conversation,
  messages,
  onTitleFromAnswer,
  permissionMode,
  target,
  vaultPath,
  vaultPaths,
}: {
  aiAgentsStatus: AiAgentsStatus
  conversation: AiConversation
  messages: AiAgentMessage[]
  onTitleFromAnswer: (request: GenerateAiConversationTitleRequest & { id: string }) => void
  permissionMode: AiAgentPermissionMode
  target: AiTarget
  vaultPath: string
  vaultPaths?: string[]
}) {
  const requestedTitleKeysRef = useRef(new Set<string>())

  useEffect(() => {
    if (!conversation.usesDefaultTitle) return

    const firstMessage = firstCompletedAssistantMessage(messages)
    const prompt = firstMessage?.userMessage.trim()
    const assistantResponse = firstMessage?.response?.trim()
    if (!firstMessage || !prompt || !assistantResponse) return

    const titleKey = `${conversation.id}:${firstMessage.id ?? prompt}`
    if (requestedTitleKeysRef.current.has(titleKey)) return
    requestedTitleKeysRef.current.add(titleKey)

    onTitleFromAnswer({
      assistantResponse,
      id: conversation.id,
      permissionMode,
      prompt,
      target,
      targetReady: aiTargetReady(target, aiAgentsStatus),
      vaultPath,
      vaultPaths,
    })
  }, [
    aiAgentsStatus,
    conversation.id,
    conversation.usesDefaultTitle,
    messages,
    onTitleFromAnswer,
    permissionMode,
    target,
    vaultPath,
    vaultPaths,
  ])
}

interface ConversationSessionContext {
  activeEntry: VaultEntry | null
  activeNoteContent: string | null
  entries?: VaultEntry[]
  noteList?: NoteListItem[]
  noteListFilter?: { type: string | null; query: string }
  openTabs?: VaultEntry[]
}

function activeContextForSession({
  active,
  activeEntry,
  activeNoteContent,
  entries,
  noteList,
  noteListFilter,
  openTabs,
}: Pick<ConversationSessionProps, 'active' | 'activeEntry' | 'activeNoteContent' | 'entries' | 'noteList' | 'noteListFilter' | 'openTabs'>): ConversationSessionContext {
  if (!active) {
    return {
      activeEntry: null,
      activeNoteContent: null,
    }
  }

  return {
    activeEntry: activeEntry ?? null,
    activeNoteContent: activeNoteContent ?? null,
    entries,
    noteList,
    noteListFilter,
    openTabs,
  }
}

function ConversationComposerControls({
  disabled,
  groups,
  locale,
  onOpenAiSettings,
  onPermissionModeChange,
  onSelectTarget,
  permissionMode,
  side,
  target,
}: {
  disabled: boolean
  groups: AiWorkspaceTargetGroups
  locale: AppLocale
  onOpenAiSettings?: () => void
  onPermissionModeChange: (mode: AiAgentPermissionMode) => void
  onSelectTarget: (targetId: string) => void
  permissionMode: AiAgentPermissionMode
  side: 'bottom' | 'top'
  target: AiTarget
}) {
  return (
    <>
      <TargetPicker
        compact
        disabled={disabled}
        groups={groups}
        locale={locale}
        selectedTarget={target}
        side={side}
        onSelectTarget={onSelectTarget}
      />
      <PermissionPicker
        compact
        disabled={disabled}
        locale={locale}
        permissionMode={permissionMode}
        side={side}
        targetKind={target.kind}
        onChange={onPermissionModeChange}
      />
      {onOpenAiSettings && (
        <Button
          type="button"
          variant="ghost"
          size="icon-xs"
          className="text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground"
          aria-label={translate(locale, 'ai.workspace.settings')}
          title={translate(locale, 'ai.workspace.settings')}
          onClick={onOpenAiSettings}
          data-testid="ai-workspace-composer-settings"
        >
          <GearSix size={16} weight="regular" />
        </Button>
      )}
    </>
  )
}

function ConversationWorkspaceHeader({
  conversation,
  locale,
  mode,
  onArchive,
  onClose,
  onDock,
  onOpenAiSettings,
  onPopOut,
}: Pick<
  ConversationSessionProps,
  'conversation' | 'locale' | 'mode' | 'onArchive' | 'onClose' | 'onDock' | 'onOpenAiSettings' | 'onPopOut'
>) {
  if (mode === 'side') return null

  return (
    <WorkspaceHeader
      archiveDisabled={!canArchiveConversation(conversation)}
      conversation={conversation}
      locale={locale}
      mode={mode}
      onArchive={onArchive}
      onClose={onClose}
      onDock={onDock}
      onOpenAiSettings={onOpenAiSettings}
      onPopOut={onPopOut}
    />
  )
}

function ConversationSession({
  active,
  activeEntry,
  activeNoteContent,
  aiAgentsStatus,
  conversation,
  defaultAiAgentReady,
  entries,
  groups,
  locale,
  mode,
  noteList,
  noteListFilter,
  onArchive,
  onClose,
  onDock,
  onFileCreated,
  onFileModified,
  onForkMessage,
  onMessageHistoryScrollStateChange,
  onOpenAiSettings,
  onOpenNote,
  onPopOut,
  onRestoreVaultAiGuidance,
  onSelectTarget,
  onStatusChange,
  onPromptSubmitted,
  onTitleFromAnswer,
  onUnsupportedAiPaste,
  onVaultChanged,
  openTabs,
  target,
  vaultAiGuidanceStatus,
  vaultPath,
  vaultPaths,
}: ConversationSessionProps) {
  const context = activeContextForSession({
    active,
    activeEntry,
    activeNoteContent,
    entries,
    noteList,
    noteListFilter,
    openTabs,
  })
  const readiness = agentReadinessForTarget(target, aiAgentsStatus)
  const controller = useAiPanelController({
    vaultPath,
    vaultPaths,
    defaultAiAgent: targetAgent(target),
    defaultAiTarget: target,
    defaultAiAgentReady: target.kind === 'api_model' || defaultAiAgentReady,
    defaultAiAgentReadiness: readiness,
    activeEntry: context.activeEntry,
    activeNoteContent: context.activeNoteContent,
    entries: context.entries,
    openTabs: context.openTabs,
    noteList: context.noteList,
    noteListFilter: context.noteListFilter,
    locale,
    onOpenNote,
    onFileCreated,
    onFileModified,
    onVaultChanged,
    sessionId: conversation.id,
  })
  const running = controller.agent.status === 'thinking' || controller.agent.status === 'tool-executing'
  const composerMenuSide = mode === 'window' ? 'bottom' : 'top'
  const composerControls = (
    <ConversationComposerControls
      disabled={running}
      groups={groups}
      locale={locale}
      onOpenAiSettings={onOpenAiSettings}
      onPermissionModeChange={controller.handlePermissionModeChange}
      onSelectTarget={onSelectTarget}
      permissionMode={controller.permissionMode}
      side={composerMenuSide}
      target={target}
    />
  )

  useEffect(() => {
    onStatusChange(conversation.id, controller.agent.status)
  }, [conversation.id, controller.agent.status, onStatusChange])
  useGeneratedConversationTitle({
    aiAgentsStatus,
    conversation,
    messages: controller.agent.messages,
    onTitleFromAnswer,
    permissionMode: controller.permissionMode,
    target,
    vaultPath,
    vaultPaths,
  })

  return (
    <div className={active ? 'flex min-h-0 flex-1 flex-col' : 'hidden'} data-testid={`ai-workspace-session-${conversation.id}`}>
      <ConversationWorkspaceHeader
        conversation={conversation}
        locale={locale}
        mode={mode}
        onArchive={onArchive}
        onClose={onClose}
        onDock={onDock}
        onOpenAiSettings={onOpenAiSettings}
        onPopOut={onPopOut}
      />
      <GuidanceWarning locale={locale} onRestore={onRestoreVaultAiGuidance} status={vaultAiGuidanceStatus} />
      <div className="flex min-h-0 flex-1">
        <AiPanelView
          controller={controller}
          defaultAiAgent={targetAgent(target)}
          defaultAiAgentReadiness={readiness}
          defaultAiAgentReady={aiTargetReady(target, aiAgentsStatus)}
          defaultAiTarget={target}
          entries={context.entries}
          activeEntry={context.activeEntry}
          composerControls={composerControls}
          interactive={active}
          locale={locale}
          onClose={onClose}
          onForkMessage={onForkMessage}
          onMessageHistoryScrollStateChange={active ? onMessageHistoryScrollStateChange : undefined}
          onOpenNote={onOpenNote}
          onSendPrompt={() => onPromptSubmitted(conversation.id)}
          onUnsupportedAiPaste={onUnsupportedAiPaste}
          showHeader={false}
          showLeftBorder={false}
          surface={mode === 'side' ? 'sidebar' : 'default'}
        />
      </div>
    </div>
  )
}

type ResolvedAiWorkspaceProps = AiWorkspaceProps & {
  defaultAiAgent: AiAgentId
  defaultAiAgentReady: boolean
  entries: VaultEntry[]
  locale: AppLocale
  mode: AiWorkspaceMode
}

interface AiWorkspaceModel {
  activeConversation: AiConversation | undefined
  activeId: string
  addDefaultConversation: () => void
  archiveConversationSafely: (id: string) => void
  canArchiveConversation: (conversation: AiConversation) => boolean
  closeConversationSafely: (id: string) => void
  conversations: AiConversation[]
  fallbackTarget: AiTarget
  forkConversationUntilMessage: (sourceId: string, messageId: string) => void
  groups: AiWorkspaceTargetGroups
  handleStatusChange: (id: string, status: AgentStatus) => void
  renameConversation: (id: string, title: string) => void
  reorderConversation: (activeId: string, overId: string) => void
  restoreConversation: (id: string) => void
  sidebarCollapsed: boolean
  setActiveId: (id: string) => void
  setConversationTarget: (id: string, targetId: string) => void
  setShowArchived: (show: boolean) => void
  showArchived: boolean
  statuses: Record<string, AgentStatus>
  markConversationActivity: (id: string) => void
  titleConversationFromAnswer: (request: GenerateAiConversationTitleRequest & { id: string }) => void
  toggleSidebarCollapsed: () => void
  updateDefaultConversationTargets: (targetId: string) => void
}

function resolveAiWorkspaceProps(props: AiWorkspaceProps): ResolvedAiWorkspaceProps {
  return {
    ...props,
    defaultAiAgent: props.defaultAiAgent ?? DEFAULT_AI_AGENT,
    defaultAiAgentReady: props.defaultAiAgentReady ?? true,
    entries: props.entries ?? [],
    locale: props.locale ?? 'en',
    mode: props.mode ?? 'docked',
  }
}

function SideWorkspaceTitleEditor({
  conversation,
  locale,
  onCancel,
  onRename,
}: {
  conversation: AiConversation
  locale: AppLocale
  onCancel: () => void
  onRename: (title: string) => void
}) {
  const [draft, setDraft] = useState(conversation.title)
  const finishedRef = useRef(false)
  const submit = () => {
    if (finishedRef.current) return
    const nextTitle = draft.trim()
    if (!nextTitle) {
      finishedRef.current = true
      onCancel()
      return
    }

    finishedRef.current = true
    onRename(nextTitle)
    onCancel()
  }
  const cancel = () => {
    finishedRef.current = true
    onCancel()
  }

  return (
    <Input
      value={draft}
      onChange={(event) => setDraft(event.target.value)}
      onBlur={submit}
      onKeyDown={(event) => {
        if (event.key === 'Enter') submit()
        if (event.key === 'Escape') cancel()
      }}
      onPointerDown={(event) => event.stopPropagation()}
      aria-label={translate(locale, 'ai.workspace.renameChat')}
      className="h-9 w-[180px] rounded-lg px-3 text-[13px] font-semibold"
      autoFocus
    />
  )
}

function SideWorkspaceTab({
  active,
  conversation,
  editing,
  locale,
  onClose,
  onCancelRename,
  onRename,
  onSelect,
  onStartRename,
  status,
}: {
  active: boolean
  conversation: AiConversation
  editing: boolean
  locale: AppLocale
  onClose: (id: string) => void
  onCancelRename: () => void
  onRename: (id: string, title: string) => void
  onSelect: (id: string) => void
  onStartRename: (id: string) => void
  status: AgentStatus | undefined
}) {
  const closeLabel = translate(locale, 'ai.workspace.closeChat', { title: conversation.title })
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: conversation.id, disabled: editing })

  return (
    <div
      ref={setNodeRef}
      className="group relative shrink-0"
      style={{
        transform: CSS.Translate.toString(transform),
        transition,
        opacity: isDragging ? 0.55 : 1,
      }}
    >
      {editing ? (
        <SideWorkspaceTitleEditor
          conversation={conversation}
          locale={locale}
          onCancel={onCancelRename}
          onRename={(title) => onRename(conversation.id, title)}
        />
      ) : (
        <Button
          type="button"
          variant="ghost"
          size="sm"
          className={cn(
            'h-10 shrink-0 cursor-grab justify-start rounded-lg px-3 text-[13px] font-semibold active:cursor-grabbing',
            active
              ? 'bg-[var(--state-hover)] text-foreground'
              : 'text-muted-foreground hover:bg-[var(--state-hover)] hover:text-foreground',
          )}
          {...attributes}
          {...listeners}
          aria-pressed={active}
          onClick={() => onSelect(conversation.id)}
          onDoubleClick={(event) => {
            event.stopPropagation()
            onStartRename(conversation.id)
          }}
        >
          <span className="whitespace-nowrap">{conversation.title}</span>
          {(status === 'thinking' || status === 'tool-executing') && <span className="ml-2 h-2 w-2 shrink-0 rounded-full bg-primary" aria-hidden />}
        </Button>
      )}
      {!editing && (
        <>
          <div
            className={cn(
              'pointer-events-none absolute inset-y-1 right-0 w-9 rounded-r-lg opacity-0 transition-opacity',
              active
                ? 'bg-gradient-to-l from-[var(--state-hover)] via-[var(--state-hover)] to-transparent'
                : 'bg-gradient-to-l from-sidebar via-sidebar to-transparent group-hover:from-[var(--state-hover)] group-hover:via-[var(--state-hover)]',
              'group-hover:opacity-100 group-focus-within:opacity-100',
            )}
            aria-hidden
          />
          <Button
            type="button"
            variant="ghost"
            size="icon-xs"
            className={cn(
              'pointer-events-none absolute top-1/2 right-1.5 z-10 h-6 w-6 -translate-y-1/2 rounded-md p-0 opacity-0 shadow-none transition-opacity',
              'bg-transparent text-foreground hover:bg-transparent hover:text-foreground',
              'group-hover:pointer-events-auto group-hover:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100',
            )}
            aria-label={closeLabel}
            title={closeLabel}
            onPointerDown={(event) => event.stopPropagation()}
            onClick={(event) => {
              event.stopPropagation()
              onClose(conversation.id)
            }}
          >
            <X size={13} weight="bold" />
          </Button>
        </>
      )}
    </div>
  )
}

function useHorizontalScrollFades(dependencyKey: string) {
  const scrollRef = useRef<HTMLDivElement | null>(null)
  const [fades, setFades] = useState({ left: false, right: false })

  const updateFades = useCallback(() => {
    const element = scrollRef.current
    if (!element) return

    const maxScrollLeft = element.scrollWidth - element.clientWidth
    setFades({
      left: element.scrollLeft > 1,
      right: maxScrollLeft > 1 && element.scrollLeft < maxScrollLeft - 1,
    })
  }, [])

  useEffect(() => {
    const element = scrollRef.current
    updateFades()
    if (!element) return

    element.addEventListener('scroll', updateFades, { passive: true })
    const resizeObserver = typeof ResizeObserver === 'undefined'
      ? null
      : new ResizeObserver(updateFades)
    resizeObserver?.observe(element)
    if (element.firstElementChild) resizeObserver?.observe(element.firstElementChild)

    return () => {
      element.removeEventListener('scroll', updateFades)
      resizeObserver?.disconnect()
    }
  }, [dependencyKey, updateFades])

  return {
    scrollRef,
    showLeftFade: fades.left,
    showRightFade: fades.right,
  }
}

function SideWorkspaceTabs({
  activeId,
  conversations,
  locale,
  onCloseConversation,
  onNewChat,
  onRename,
  onReorder,
  onSelect,
  statuses,
}: {
  activeId: string
  conversations: AiConversation[]
  locale: AppLocale
  onCloseConversation: (id: string) => void
  onNewChat: () => void
  onRename: (id: string, title: string) => void
  onReorder: (activeId: string, overId: string) => void
  onSelect: (id: string) => void
  statuses: Record<string, AgentStatus>
}) {
  const [editingId, setEditingId] = useState<string | null>(null)
  const visibleConversations = conversations.filter((conversation) => !conversation.archived)
  const visibleConversationIds = visibleConversations.map((conversation) => conversation.id)
  const tabDependencyKey = visibleConversations
    .map((conversation) => `${conversation.id}:${conversation.title}`)
    .join('\0')
  const { scrollRef, showLeftFade, showRightFade } = useHorizontalScrollFades(tabDependencyKey)
  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }))
  const handleDragEnd = useCallback((event: DragEndEvent) => {
    const activeConversationId = String(event.active.id)
    const overConversationId = event.over ? String(event.over.id) : ''
    if (!overConversationId || activeConversationId === overConversationId) return

    onReorder(activeConversationId, overConversationId)
  }, [onReorder])

  return (
    <div className="relative min-w-0 flex-1">
      <div
        ref={scrollRef}
        className="overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
        data-testid="ai-workspace-side-tabs"
      >
        <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
          <div className="flex w-max items-center gap-1 py-1">
            <SortableContext items={visibleConversationIds} strategy={horizontalListSortingStrategy}>
              {visibleConversations.map((conversation) => (
                <SideWorkspaceTab
                  key={conversation.id}
                  active={conversation.id === activeId}
                  conversation={conversation}
                  editing={editingId === conversation.id}
                  locale={locale}
                  onCancelRename={() => setEditingId(null)}
                  onClose={onCloseConversation}
                  onRename={onRename}
                  onSelect={onSelect}
                  onStartRename={setEditingId}
                  status={statuses[conversation.id]}
                />
              ))}
            </SortableContext>
            <Button
              type="button"
              variant="ghost"
              size="icon-sm"
              className="shrink-0"
              aria-label={translate(locale, 'ai.workspace.newChat')}
              title={translate(locale, 'ai.workspace.newChat')}
              onClick={onNewChat}
            >
              <Plus size={17} />
            </Button>
          </div>
        </DndContext>
      </div>
      {showLeftFade && (
        <div
          className="pointer-events-none absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-sidebar to-transparent"
          data-testid="ai-workspace-side-tabs-left-fade"
        />
      )}
      {showRightFade && (
        <div
          className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-sidebar to-transparent"
          data-testid="ai-workspace-side-tabs-right-fade"
        />
      )}
    </div>
  )
}

function SideWorkspaceHeader({
  activeId,
  conversations,
  expanded,
  locale,
  onClose,
  onCloseConversation,
  onNewChat,
  onRename,
  onReorder,
  onSelect,
  onToggleExpanded,
  separated,
  statuses,
}: {
  activeId: string
  conversations: AiConversation[]
  expanded: boolean
  locale: AppLocale
  onClose: () => void
  onCloseConversation: (id: string) => void
  onNewChat: () => void
  onRename: (id: string, title: string) => void
  onReorder: (activeId: string, overId: string) => void
  onSelect: (id: string) => void
  onToggleExpanded: () => void
  separated: boolean
  statuses: Record<string, AgentStatus>
}) {
  const expandLabel = translate(locale, expanded ? 'ai.workspace.restorePanel' : 'ai.workspace.expandPanel')

  return (
    <div
      className={cn(
        'flex h-[52px] shrink-0 items-center gap-2 px-2',
        separated && 'border-b border-border',
      )}
      data-testid="ai-workspace-side-header"
    >
      <SideWorkspaceTabs
        activeId={activeId}
        conversations={conversations}
        locale={locale}
        onCloseConversation={onCloseConversation}
        onNewChat={onNewChat}
        onRename={onRename}
        onReorder={onReorder}
        onSelect={onSelect}
        statuses={statuses}
      />
      <Button
        type="button"
        variant="ghost"
        size="icon-sm"
        aria-label={expandLabel}
        title={expandLabel}
        onClick={onToggleExpanded}
      >
        {expanded ? <ArrowsInLineHorizontal size={17} /> : <ArrowsOutLineHorizontal size={17} />}
      </Button>
      <Button
        type="button"
        variant="ghost"
        size="icon-sm"
        aria-label={translate(locale, 'ai.workspace.close')}
        title={translate(locale, 'ai.workspace.close')}
        onClick={onClose}
      >
        <SidebarSimple size={17} weight="regular" />
      </Button>
    </div>
  )
}

function SideAiWorkspaceLayout({
  model,
  sizing,
  workspace,
}: {
  model: AiWorkspaceModel
  sizing: AiWorkspaceSizing
  workspace: ResolvedAiWorkspaceProps
}) {
  const [expanded, setExpanded] = useState(false)
  const [headerSeparated, setHeaderSeparated] = useState(false)

  return (
    <section
      className={workspaceClassName('side', expanded)}
      style={workspaceStyle('side', sizing.workspaceSize, expanded)}
      data-testid="ai-workspace"
      data-ai-workspace-mode="side"
      data-ai-workspace-expanded={expanded ? 'true' : 'false'}
      role="complementary"
      aria-label={translate(workspace.locale, 'ai.workspace.title')}
    >
      {!expanded && <WorkspaceResizeHandles mode="side" onResize={sizing.onWorkspaceResize} />}
      <div className="flex min-w-0 flex-1 flex-col">
        <SideWorkspaceHeader
          activeId={model.activeId}
          conversations={model.conversations}
          expanded={expanded}
          locale={workspace.locale}
          onClose={workspace.onClose}
          onCloseConversation={model.closeConversationSafely}
          onNewChat={model.addDefaultConversation}
          onRename={model.renameConversation}
          onReorder={model.reorderConversation}
          onSelect={model.setActiveId}
          onToggleExpanded={() => setExpanded((current) => !current)}
          separated={headerSeparated}
          statuses={model.statuses}
        />
        <ConversationSessions model={model} workspace={workspace} onMessageHistoryScrollStateChange={setHeaderSeparated} />
      </div>
    </section>
  )
}

function useActiveConversationSync(
  activeConversation: AiConversation | undefined,
  activeId: string,
  setActiveId: (id: string) => void,
) {
  useEffect(() => {
    if (activeConversation && activeConversation.id !== activeId) setActiveId(activeConversation.id)
  }, [activeConversation, activeId, setActiveId])
}

function useArchiveConversationSafely({
  addConversation,
  archiveConversation,
  conversations,
  fallbackTarget,
}: {
  addConversation: (target: AiTarget) => void
  archiveConversation: (id: string) => void
  conversations: AiConversation[]
  fallbackTarget: AiTarget
}) {
  return useCallback((id: string) => {
    const conversation = conversations.find((candidate) => candidate.id === id)
    if (!conversation || !canArchiveConversation(conversation)) return

    const activeCount = conversations.filter((conversation) => !conversation.archived).length
    archiveConversation(id)
    if (activeCount <= 1) addConversation(fallbackTarget)
  }, [addConversation, archiveConversation, conversations, fallbackTarget])
}

function useTrackedConversationActions({
  conversations,
  renameConversation,
  titleConversationFromAnswer,
}: {
  conversations: AiConversation[]
  renameConversation: (id: string, title: string) => void
  titleConversationFromAnswer: (request: GenerateAiConversationTitleRequest & { id: string }) => void
}) {
  const trackedRenameConversation = useCallback((id: string, title: string) => {
    if (!title.trim()) return
    renameConversation(id, title)
    trackAiWorkspaceChatTitled('manual')
  }, [renameConversation])
  const trackedTitleConversationFromAnswer = useCallback((request: GenerateAiConversationTitleRequest & { id: string }) => {
    const conversation = conversations.find((candidate) => candidate.id === request.id)
    titleConversationFromAnswer(request)
    if (conversation?.usesDefaultTitle) trackAiWorkspaceChatTitled('generated')
  }, [conversations, titleConversationFromAnswer])

  return { trackedRenameConversation, trackedTitleConversationFromAnswer }
}

function useAiWorkspaceNewChatEvent(open: boolean, addDefaultConversation: () => void) {
  useEffect(() => {
    if (!open) return
    const handleNewChat = () => addDefaultConversation()
    window.addEventListener(NEW_AI_CHAT_EVENT, handleNewChat)
    return () => window.removeEventListener(NEW_AI_CHAT_EVENT, handleNewChat)
  }, [addDefaultConversation, open])
}

function useAiWorkspaceModel(workspace: ResolvedAiWorkspaceProps): AiWorkspaceModel {
  const groups = useMemo(
    () => buildAiWorkspaceTargetGroups(workspace.aiAgentsStatus, workspace.aiModelProviders),
    [workspace.aiAgentsStatus, workspace.aiModelProviders],
  )
  const fallbackTarget = useMemo(
    () => firstTarget(groups, workspace.defaultAiTarget, workspace.defaultAiAgent),
    [groups, workspace.defaultAiAgent, workspace.defaultAiTarget],
  )
  const {
    activeId,
    addConversation,
    archiveConversation,
    closeConversation,
    conversations,
    forkConversation,
    renameConversation,
    reorderConversation,
    restoreConversation,
    setActiveId,
    setConversationTarget,
    setShowArchived,
    showArchived,
    markConversationActivity,
    titleConversationFromAnswer,
    updateDefaultConversationTargets,
  } = useConversations({
    fallbackTarget,
    initialActiveConversationId: workspace.initialActiveConversationId,
    locale: workspace.locale,
    onSettingsChange: workspace.onConversationSettingsChange,
    settings: workspace.conversationSettings,
    settingsReady: workspace.conversationSettingsReady ?? true,
  })
  const [statuses, setStatuses] = useState<Record<string, AgentStatus>>({})
  const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
  const activeConversation = activeConversationForState(conversations, activeId, showArchived)

  const addDefaultConversation = useCallback(() => {
    addConversation(fallbackTarget)
  }, [addConversation, fallbackTarget])
  const archiveConversationSafely = useArchiveConversationSafely({
    addConversation,
    archiveConversation,
    conversations,
    fallbackTarget,
  })
  useActiveConversationSync(activeConversation, activeId, setActiveId)
  const handleStatusChange = useCallback((id: string, status: AgentStatus) => {
    setStatuses((current) => current[id] === status ? current : { ...current, [id]: status })
  }, [])
  const forkConversationUntilMessage = useCallback((sourceId: string, messageId: string) => {
    const targetId = forkConversation(sourceId)
    if (!targetId) return

    cloneAiWorkspaceSessionUntilMessage(sourceId, targetId, messageId)
  }, [forkConversation])
  const { trackedRenameConversation, trackedTitleConversationFromAnswer } = useTrackedConversationActions({
    conversations,
    renameConversation,
    titleConversationFromAnswer,
  })
  const toggleSidebarCollapsed = useCallback(() => {
    setSidebarCollapsed((current) => {
      const next = !current
      trackAiWorkspaceSidebarToggled(next, workspace.mode)
      return next
    })
  }, [workspace.mode])

  useAiWorkspaceNewChatEvent(workspace.open, addDefaultConversation)
  useEffect(() => {
    updateDefaultConversationTargets(fallbackTarget.id)
  }, [fallbackTarget.id, updateDefaultConversationTargets])

  return {
    activeConversation,
    activeId,
    addDefaultConversation,
    archiveConversationSafely,
    canArchiveConversation,
    closeConversationSafely: closeConversation,
    conversations,
    fallbackTarget,
    forkConversationUntilMessage,
    groups,
    handleStatusChange,
    renameConversation: trackedRenameConversation,
    reorderConversation,
    restoreConversation,
    sidebarCollapsed,
    setActiveId,
    setConversationTarget,
    setShowArchived,
    showArchived,
    statuses,
    markConversationActivity,
    titleConversationFromAnswer: trackedTitleConversationFromAnswer,
    toggleSidebarCollapsed,
    updateDefaultConversationTargets,
  }
}

function AiWorkspaceLayout({ model, workspace }: { model: AiWorkspaceModel; workspace: ResolvedAiWorkspaceProps }) {
  const sizing = useAiWorkspaceSizing(workspace.mode)
  if (workspace.mode === 'side') {
    return <SideAiWorkspaceLayout model={model} sizing={sizing} workspace={workspace} />
  }

  return (
    <section
      className={workspaceClassName(workspace.mode)}
      style={workspaceStyle(workspace.mode, sizing.workspaceSize)}
      data-testid="ai-workspace"
      data-ai-workspace-mode={workspace.mode}
      role="dialog"
      aria-label={translate(workspace.locale, 'ai.workspace.title')}
    >
      <WorkspaceResizeHandles mode={workspace.mode} onResize={sizing.onWorkspaceResize} />
      <ConversationSidebar
        activeId={model.activeId}
        collapsed={model.sidebarCollapsed}
        conversations={model.conversations}
        locale={workspace.locale}
        onCanArchive={model.canArchiveConversation}
        onArchive={model.archiveConversationSafely}
        onNewChat={model.addDefaultConversation}
        onRename={model.renameConversation}
        onRestore={model.restoreConversation}
        onSelect={model.setActiveId}
        onToggleCollapsed={model.toggleSidebarCollapsed}
        setShowArchived={model.setShowArchived}
        showArchived={model.showArchived}
        sidebarWidth={sizing.sidebarWidth}
        statuses={model.statuses}
      />
      {!model.sidebarCollapsed && (
        <ResizeHandle onResize={sizing.onSidebarResize} />
      )}
      <div className="flex min-w-0 flex-1 flex-col">
        <ConversationSessions model={model} workspace={workspace} />
      </div>
    </section>
  )
}

function ConversationSessions({
  model,
  onMessageHistoryScrollStateChange,
  workspace,
}: {
  model: AiWorkspaceModel
  onMessageHistoryScrollStateChange?: (scrolled: boolean) => void
  workspace: ResolvedAiWorkspaceProps
}) {
  return (
    <div className="flex min-h-0 flex-1">
      {model.conversations.map((conversation) => {
        const target = resolveTarget(conversation, model.groups, model.fallbackTarget)

        return (
          <ConversationSession
            key={conversation.id}
            active={conversation.id === model.activeConversation?.id}
            activeEntry={workspace.activeEntry}
            activeNoteContent={workspace.activeNoteContent}
            aiAgentsStatus={workspace.aiAgentsStatus}
            conversation={conversation}
            defaultAiAgentReady={workspace.defaultAiAgentReady}
            entries={workspace.entries}
            groups={model.groups}
            locale={workspace.locale}
            mode={workspace.mode}
            noteList={workspace.noteList}
            noteListFilter={workspace.noteListFilter}
            onArchive={() => model.archiveConversationSafely(conversation.id)}
            onClose={workspace.onClose}
            onDock={workspace.onDock}
            onFileCreated={workspace.onFileCreated}
            onFileModified={workspace.onFileModified}
            onForkMessage={(messageId) => model.forkConversationUntilMessage(conversation.id, messageId)}
            onMessageHistoryScrollStateChange={onMessageHistoryScrollStateChange}
            onOpenAiSettings={workspace.onOpenAiSettings}
            onOpenNote={workspace.onOpenNote}
            onPopOut={workspace.onPopOut}
            onRestoreVaultAiGuidance={workspace.onRestoreVaultAiGuidance}
            onSelectTarget={(targetId) => model.setConversationTarget(conversation.id, targetId)}
            onStatusChange={model.handleStatusChange}
            onPromptSubmitted={model.markConversationActivity}
            onTitleFromAnswer={model.titleConversationFromAnswer}
            onUnsupportedAiPaste={workspace.onUnsupportedAiPaste}
            onVaultChanged={workspace.onVaultChanged}
            openTabs={workspace.openTabs}
            target={target}
            vaultAiGuidanceStatus={workspace.vaultAiGuidanceStatus}
            vaultPath={workspace.vaultPath}
            vaultPaths={workspace.vaultPaths}
          />
        )
      })}
    </div>
  )
}

export function AiWorkspace(props: AiWorkspaceProps) {
  const workspace = resolveAiWorkspaceProps(props)
  const model = useAiWorkspaceModel(workspace)
  const { onActiveConversationChange } = workspace

  useEffect(() => {
    if (!workspace.open || !model.activeId) return
    onActiveConversationChange?.(model.activeId)
  }, [model.activeId, onActiveConversationChange, workspace.open])

  if (!workspace.open || !model.activeConversation) return null

  return <AiWorkspaceLayout model={model} workspace={workspace} />
}
