// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 SenWeaverCoding
// Licensed under the MIT License.

import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useSessionStore } from '../../stores/sessionStore'
import { useSessionRunStateStore } from '../../stores/sessionRunStateStore'
import { waitForSessionsIdle } from '../../lib/sessionLifecycle'
import { useUIStore } from '../../stores/uiStore'
import { useTranslation } from '../../i18n'
import { ProjectFilter } from './ProjectFilter'
import { ConfirmDialog } from '../shared/ConfirmDialog'
import type { SessionListItem } from '../../types/session'
import { useTabStore, SCHEDULED_TAB_ID } from '../../stores/tabStore'
import { useChatStore } from '../../stores/chatStore'
import { focusSession } from '../../lib/focusSession'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
import { useDesignerCanvasStore } from '../../stores/designerCanvasStore'
import { isPlaceholderTitle, resolveSessionTitle } from '../../utils/sessionTitle'
import { Spinner } from '../shared/Spinner'
import { useWorkspaceQueueStore } from '../../stores/workspaceQueueStore'
import { AgentMonitorPanel } from './AgentMonitorPanel'
import { searchApi } from '../../api/search'
import { useLanStore } from '../../stores/lanStore'
import { useLanGroupStore } from '../../stores/lanGroupStore'
import { useLanShareStore } from '../../stores/lanShareStore'
import { UserPanel } from '../lan/UserPanel'
import { GroupsPanel } from '../lanGroup/GroupsPanel'
import { SharePanel } from '../lanShare/SharePanel'
import { useFileDragStore } from '../../stores/fileDragStore'
import { SESSION_REF_PREFIX } from '../chat/composerRefs'

const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window)

const SIDEBAR_GROUP_PAGE_SIZE = 9

const WORKSPACE_UNKNOWN_KEY = '__unknown__'

type WorkspaceGroup = {
  workspaceKey: string
  workspaceLabel: string
  workspacePath: string
  workspaceExists: boolean
  latestModifiedAt: number
  totalCount: number
  sessions: SessionListItem[]
}

export function Sidebar() {
  const sessions = useSessionStore((s) => s.sessions)
  const runningSessions = useSessionRunStateStore((s) => s.running)
  const queueState = useWorkspaceQueueStore((s) => s.queues)
  const cancelAllQueuedForSession = useWorkspaceQueueStore((s) => s.cancelAllForSession)
  const queuedCounts = useMemo(() => {
    const counts: Record<string, number> = {}
    for (const list of Object.values(queueState)) {
      for (const item of list) {
        counts[item.sessionId] = (counts[item.sessionId] ?? 0) + 1
      }
    }
    return counts
  }, [queueState])
  const selectedProjects = useSessionStore((s) => s.selectedProjects)
  const error = useSessionStore((s) => s.error)
  const lanNickname = useLanStore((s) => s.identity?.nickname ?? s.identity?.userId ?? '')
  const lanRunning = useLanStore((s) => s.identity?.running ?? false)
  const lanUnread = useLanStore((s) => s.unread)
  const lanPanelOpen = useLanStore((s) => s.panelOpen)
  const lanInit = useLanStore((s) => s.init)
  const toggleLanPanel = useLanStore((s) => s.togglePanel)
  const lanGroupInit = useLanGroupStore((s) => s.init)
  const lanGroupUnread = useLanGroupStore((s) => s.unread)
  const lanGroupPanelOpen = useLanGroupStore((s) => s.panelOpen)
  const toggleLanGroupPanel = useLanGroupStore((s) => s.togglePanel)
  const lanShareInit = useLanShareStore((s) => s.init)
  const lanSharePanelOpen = useLanShareStore((s) => s.panelOpen)
  const toggleLanSharePanel = useLanShareStore((s) => s.togglePanel)
  useEffect(() => {
    void lanInit()
    void lanGroupInit()
    void lanShareInit()
  }, [lanInit, lanGroupInit, lanShareInit])
  const fetchSessions = useSessionStore((s) => s.fetchSessions)
  const deleteSession = useSessionStore((s) => s.deleteSession)
  const deleteSessions = useSessionStore((s) => s.deleteSessions)
  const clearWorkDirSelectionIfMatches = useSessionStore((s) => s.clearWorkDirSelectionIfMatches)
  const renameSession = useSessionStore((s) => s.renameSession)
  const addToast = useUIStore((s) => s.addToast)
  const sidebarOpen = useUIStore((s) => s.sidebarOpen)
  const settingsOverlayOpen = useUIStore((s) => s.settingsOverlayOpen)
  const templateLibraryOpen = useUIStore((s) => s.templateLibraryOpen)
  const activeTabId = useTabStore((s) => s.activeTabId)
  const designerCanvasOpen = useDesignerCanvasStore((s) =>
    activeTabId ? s.panels[activeTabId]?.visible ?? false : false,
  )
  const toggleDesignerCanvas = useDesignerCanvasStore((s) => s.toggle)
  const closeTab = useTabStore((s) => s.closeTab)
  const disconnectSession = useChatStore((s) => s.disconnectSession)
  const stopGeneration = useChatStore((s) => s.stopGeneration)
  const [searchQuery, setSearchQuery] = useState('')
  const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null)
  const [pendingDeleteSessionId, setPendingDeleteSessionId] = useState<string | null>(null)
  const [pendingDeleteWorkspace, setPendingDeleteWorkspace] = useState<WorkspaceGroup | null>(null)
  const [workspaceDeleting, setWorkspaceDeleting] = useState(false)
  const [renamingId, setRenamingId] = useState<string | null>(null)
  const [renameValue, setRenameValue] = useState('')

  const [expandedWorkspaces, setExpandedWorkspaces] = useState<Set<string>>(new Set())
  const [workspacesShowMore, setWorkspacesShowMore] = useState(false)
  const [sessionsShowMore, setSessionsShowMore] = useState<Set<string>>(new Set())

  useEffect(() => {
    fetchSessions()
  }, [fetchSessions])

  useEffect(() => {
    if (!contextMenu || sidebarOpen) return
    setContextMenu(null)
  }, [contextMenu, sidebarOpen])

  useEffect(() => {
    if (!contextMenu) return
    const close = () => setContextMenu(null)
    document.addEventListener('click', close)
    return () => document.removeEventListener('click', close)
  }, [contextMenu])

  const t = useTranslation()

  const [contentMatches, setContentMatches] = useState<Map<string, number>>(new Map())

  useEffect(() => {
    const q = searchQuery.trim()
    if (q.length < 2) {
      setContentMatches((prev) => (prev.size === 0 ? prev : new Map()))
      return
    }
    let cancelled = false
    const handle = window.setTimeout(() => {
      void searchApi
        .searchSessions(q)
        .then((res) => {
          if (cancelled) return
          const map = new Map<string, number>()
          for (const hit of res.results) {
            map.set(hit.sessionId, hit.matchCount)
          }
          setContentMatches(map)
        })
        .catch(() => {
          if (!cancelled) setContentMatches(new Map())
        })
    }, 250)
    return () => {
      cancelled = true
      window.clearTimeout(handle)
    }
  }, [searchQuery])

  const filteredSessions = useMemo(() => {
    let result = sessions
    if (selectedProjects.length > 0) {
      result = result.filter((s) => selectedProjects.includes(s.projectPath))
    }
    if (searchQuery) {
      const q = searchQuery.toLowerCase()
      result = result.filter(
        (s) => s.title.toLowerCase().includes(q) || contentMatches.has(s.id),
      )
    }
    return result
  }, [sessions, selectedProjects, searchQuery, contentMatches])

  const inSearchMode = searchQuery.trim().length > 0

  const unknownWorkspaceLabel = t('sidebar.other')
  const workspaceGroups = useMemo(
    () => groupByWorkspace(filteredSessions, unknownWorkspaceLabel),
    [filteredSessions, unknownWorkspaceLabel],
  )

  const currentWorkspaceKey = useMemo(() => {
    if (!activeTabId) return null
    const s = sessions.find((x) => x.id === activeTabId)
    if (!s) return null
    const workDir = s.workDir?.trim() ?? ''
    return workDir || WORKSPACE_UNKNOWN_KEY
  }, [sessions, activeTabId])

  const initializedWorkspacesRef = useRef<Set<string>>(new Set())
  useEffect(() => {
    if (!currentWorkspaceKey) return
    if (initializedWorkspacesRef.current.has(currentWorkspaceKey)) return
    initializedWorkspacesRef.current.add(currentWorkspaceKey)
    setExpandedWorkspaces((prev) => {
      if (prev.has(currentWorkspaceKey)) return prev
      const next = new Set(prev)
      next.add(currentWorkspaceKey)
      return next
    })
  }, [currentWorkspaceKey])

  const toggleWorkspace = useCallback((workspaceKey: string) => {
    setExpandedWorkspaces((prev) => {
      const next = new Set(prev)
      if (next.has(workspaceKey)) next.delete(workspaceKey)
      else next.add(workspaceKey)
      return next
    })
  }, [])

  const revealMoreSessions = useCallback((workspaceKey: string) => {
    setSessionsShowMore((prev) => {
      if (prev.has(workspaceKey)) return prev
      const next = new Set(prev)
      next.add(workspaceKey)
      return next
    })
  }, [])

  const handleContextMenu = useCallback((e: React.MouseEvent, id: string) => {
    e.preventDefault()
    setContextMenu({ id, x: e.clientX, y: e.clientY })
  }, [])

  const handleDelete = useCallback((id: string) => {
    setContextMenu(null)
    setPendingDeleteSessionId(id)
  }, [])

  const confirmDelete = useCallback(async () => {
    if (!pendingDeleteSessionId) return
    await deleteSession(pendingDeleteSessionId)
    disconnectSession(pendingDeleteSessionId)
    closeTab(pendingDeleteSessionId)
    setPendingDeleteSessionId(null)
  }, [closeTab, deleteSession, disconnectSession, pendingDeleteSessionId])

  const handleDeleteWorkspace = useCallback((ws: WorkspaceGroup) => {
    setContextMenu(null)
    setPendingDeleteWorkspace(ws)
  }, [])

  const pendingWorkspaceInfo = useMemo(() => {
    if (!pendingDeleteWorkspace) return null
    const ids = sessionIdsForWorkspaceKey(sessions, pendingDeleteWorkspace.workspaceKey)
    const runningCount = ids.reduce(
      (acc, id) => acc + (runningSessions.has(id) ? 1 : 0),
      0,
    )
    return { ids, runningCount, label: pendingDeleteWorkspace.workspaceLabel }
  }, [pendingDeleteWorkspace, runningSessions, sessions])

  const confirmDeleteWorkspace = useCallback(async () => {
    const ws = pendingDeleteWorkspace
    if (!ws || workspaceDeleting) return
    const ids = sessionIdsForWorkspaceKey(
      useSessionStore.getState().sessions,
      ws.workspaceKey,
    )
    if (ids.length === 0) {
      setPendingDeleteWorkspace(null)
      return
    }
    setWorkspaceDeleting(true)
    try {
      const running = useSessionRunStateStore.getState().running
      const runningIds = ids.filter((id) => running.has(id))
      for (const id of runningIds) {
        stopGeneration(id)
      }
      if (runningIds.length > 0) {
        await waitForSessionsIdle(runningIds, 2500)
      }
      await deleteSessions(ids)
      for (const id of ids) {
        cancelAllQueuedForSession(id)
        disconnectSession(id)
        closeTab(id)
      }
      if (ws.workspacePath) {
        clearWorkDirSelectionIfMatches(ws.workspacePath)
      }
      setPendingDeleteWorkspace(null)
    } catch (err) {
      addToast({
        type: 'error',
        message: err instanceof Error ? err.message : t('sidebar.sessionListFailed'),
      })
    } finally {
      setWorkspaceDeleting(false)
    }
  }, [
    addToast,
    cancelAllQueuedForSession,
    clearWorkDirSelectionIfMatches,
    closeTab,
    deleteSessions,
    disconnectSession,
    pendingDeleteWorkspace,
    stopGeneration,
    t,
    workspaceDeleting,
  ])

  const handleStartRename = useCallback((id: string, currentTitle: string) => {
    setContextMenu(null)
    setRenamingId(id)
    setRenameValue(currentTitle)
  }, [])

  const handleFinishRename = useCallback(async () => {
    if (renamingId && renameValue.trim()) {
      await renameSession(renamingId, renameValue.trim())
    }
    setRenamingId(null)
    setRenameValue('')
  }, [renamingId, renameValue, renameSession])

  const startDraggingRef = useRef<(() => Promise<void>) | null>(null)

  useEffect(() => {
    if (!isTauri) return
    import(/* @vite-ignore */ '@tauri-apps/api/window')
      .then(({ getCurrentWindow }) => {
        const win = getCurrentWindow()
        startDraggingRef.current = () => win.startDragging()
      })
      .catch(() => {})
  }, [])

  const handleSidebarDrag = useCallback((e: React.MouseEvent) => {
    if ((e.target as HTMLElement).closest('button, input, textarea, select, a, [role="button"]')) return
    startDraggingRef.current?.()
  }, [])

  const sessionDragSuppressRef = useRef(false)

  const handleSessionPointerDown = useCallback(
    (event: React.PointerEvent, sessionId: string, displayTitle: string) => {
      if (event.button !== 0) return
      if (renamingId === sessionId) return
      if (sessionId === activeTabId) return
      sessionDragSuppressRef.current = false
      const startX = event.clientX
      const startY = event.clientY
      const payload = {
        relPath: `${SESSION_REF_PREFIX}${sessionId}`,
        name: displayTitle,
        isDir: false,
      }
      const store = useFileDragStore.getState()
      let started = false
      const onMove = (ev: PointerEvent) => {
        if (!started) {
          if (Math.hypot(ev.clientX - startX, ev.clientY - startY) < 6) return
          started = true
          sessionDragSuppressRef.current = true
          document.body.classList.add('sen-file-dragging')
          store.begin(payload, ev.clientX, ev.clientY)
        } else {
          useFileDragStore.getState().move(ev.clientX, ev.clientY)
        }
      }
      const cleanup = (commit: boolean) => {
        window.removeEventListener('pointermove', onMove)
        window.removeEventListener('pointerup', onUp)
        window.removeEventListener('pointercancel', onCancel)
        document.body.classList.remove('sen-file-dragging')
        if (started) {
          if (commit) useFileDragStore.getState().finish()
          else useFileDragStore.getState().cancel()
        }
      }
      const onUp = () => cleanup(true)
      const onCancel = () => cleanup(false)
      window.addEventListener('pointermove', onMove)
      window.addEventListener('pointerup', onUp)
      window.addEventListener('pointercancel', onCancel)
    },
    [renamingId, activeTabId],
  )

  const renderSessionRow = (session: SessionListItem) => {
    const displayTitle = resolveSessionTitle(session.title, t('sidebar.untitled'))
    const isRunning = runningSessions.has(session.id)
    const queuedCount = queuedCounts[session.id] ?? 0
    return (
      <div key={session.id} className="group/row relative">
        {renamingId === session.id ? (
          <input
            autoFocus
            value={renameValue}
            onChange={(e) => setRenameValue(e.target.value)}
            onBlur={handleFinishRename}
            onKeyDown={(e) => {
              if (e.key === 'Enter') handleFinishRename()
              if (e.key === 'Escape') {
                setRenamingId(null)
                setRenameValue('')
              }
            }}
            className="ml-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border-focus)] bg-[var(--color-surface)] px-3 py-2 text-xs text-[var(--color-text-primary)] outline-none"
          />
        ) : (
          <>
            <button
              onClick={() => {
                if (sessionDragSuppressRef.current) {
                  sessionDragSuppressRef.current = false
                  return
                }
                useTabStore.getState().openTab(session.id, displayTitle)
                focusSession(session.id)
              }}
              onPointerDown={(e) => handleSessionPointerDown(e, session.id, displayTitle)}
              onContextMenu={(e) => handleContextMenu(e, session.id)}
              title={displayTitle}
              className={`
                w-full rounded-[12px] px-3 py-2 pr-9 text-left text-xs transition-colors duration-200
                ${session.id === activeTabId
                  ? 'bg-[var(--color-sidebar-item-active)] text-[var(--color-text-primary)]'
                  : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-sidebar-item-hover)]'
                }
              `}
            >
              <span className="flex items-center gap-2.5">
                {isRunning ? (
                  <span
                    aria-label={t('common.running')}
                    className="inline-flex items-center flex-shrink-0 text-[var(--color-brand)]"
                  >
                    <Spinner size={8} />
                  </span>
                ) : (
                  <span
                    className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
                    style={{
                      backgroundColor: session.id === activeTabId ? 'var(--color-brand)' : 'var(--color-text-tertiary)',
                      opacity: session.id === activeTabId ? 1 : 0.5,
                    }}
                  />
                )}
                <span className="flex-1 truncate font-medium tracking-[-0.01em]">{displayTitle}</span>
                {!isRunning && queuedCount > 0 && (
                  <span
                    aria-label={t('tabs.queuedBadge', { count: queuedCount })}
                    title={t('tabs.queuedBadge', { count: queuedCount })}
                    className="flex-shrink-0 inline-flex items-center text-[10px] tabular-nums text-[var(--color-text-tertiary)]"
                  >
                    ·{queuedCount}
                  </span>
                )}
                {!session.workDirExists && (
                  <span
                    className="flex-shrink-0 text-xs text-[var(--color-warning)]"
                    title={session.workDir ?? ''}
                  >
                    {t('sidebar.missingDir')}
                  </span>
                )}
                <span className="flex-shrink-0 text-xs text-[var(--color-text-tertiary)] opacity-50 transition-opacity duration-150 group-hover/row:opacity-0">
                  {formatRelativeTime(session.modifiedAt)}
                </span>
              </span>
            </button>
            <button
              type="button"
              onClick={(e) => {
                e.stopPropagation()
                handleDelete(session.id)
              }}
              aria-label={t('sidebar.deleteAria')}
              title={t('sidebar.deleteAria')}
              data-testid={`sidebar-session-delete-${session.id}`}
              className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-[var(--color-text-tertiary)] opacity-0 transition-opacity duration-150 hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-error)] focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)] group-hover/row:opacity-100"
            >
              <TrashIcon />
            </button>
          </>
        )}
      </div>
    )
  }

  const visibleWorkspaces =
    workspacesShowMore || workspaceGroups.length <= SIDEBAR_GROUP_PAGE_SIZE
      ? workspaceGroups
      : workspaceGroups.slice(0, SIDEBAR_GROUP_PAGE_SIZE)
  const hiddenWorkspaceCount = workspaceGroups.length - visibleWorkspaces.length

  return (
    <aside
      onMouseDown={handleSidebarDrag}
      className="sidebar-panel relative h-full flex flex-col bg-[var(--color-surface-sidebar)] border-r border-[var(--color-border)] select-none"
      data-state={sidebarOpen ? 'open' : 'closed'}
      aria-label="Sidebar"
    >
      <div className="flex items-center justify-end gap-1 px-3 pb-2 pt-3">
        <button
          type="button"
          data-lan-group-toggle
          onClick={() => toggleLanGroupPanel()}
          title={t('lanGroup.title')}
          aria-label={t('lanGroup.title')}
          aria-pressed={lanGroupPanelOpen}
          className={
            `relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
              lanGroupPanelOpen
                ? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
                : 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
            }`
          }
        >
          <span className="material-symbols-outlined text-[16px]">groups</span>
          {lanGroupUnread > 0 && (
            <span className="absolute -right-0.5 -top-0.5 inline-flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-[var(--color-error)] px-1 text-[9px] font-semibold text-white">
              {lanGroupUnread > 99 ? '99+' : lanGroupUnread}
            </span>
          )}
        </button>
        <button
          type="button"
          data-lan-share-toggle
          onClick={() => toggleLanSharePanel()}
          title={t('lanShare.title')}
          aria-label={t('lanShare.title')}
          aria-pressed={lanSharePanelOpen}
          className={
            `relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
              lanSharePanelOpen
                ? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
                : 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
            }`
          }
        >
          <span className="material-symbols-outlined text-[16px]">share</span>
        </button>
        <button
          type="button"
          data-template-library-toggle
          onClick={() => useUIStore.getState().toggleTemplateLibrary()}
          title={t('templateLibrary.open')}
          aria-label={t('templateLibrary.open')}
          aria-pressed={templateLibraryOpen}
          className={
            `relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
              templateLibraryOpen
                ? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
                : 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
            }`
          }
        >
          <span className="material-symbols-outlined text-[16px]">dashboard_customize</span>
        </button>
        <a
          href="https://github.com/senweaver/SenWeaverCoding"
          target="_blank"
          rel="noopener noreferrer"
          className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
          title="GitHub"
          data-tauri-drag-region
        >
          <GitHubIcon />
        </a>
      </div>

      <div className={`px-3 pb-3 flex flex-col ${sidebarOpen ? 'gap-0.5' : 'items-center gap-2'}`}>
        <NavItem
          active={false}
          collapsed={!sidebarOpen}
          label={t('sidebar.newSession')}
          onClick={async () => {
            try {
              const currentTabId = useTabStore.getState().activeTabId
              const workDir = useSessionStore.getState().resolveWorkDirForNewSessionTab(currentTabId)
              const sessionId = await useSessionStore.getState().createSession(workDir)
              useTabStore.getState().openTab(sessionId, t('sidebar.newSession'))
              focusSession(sessionId)
            } catch (error) {
              addToast({
                type: 'error',
                message: error instanceof Error ? error.message : t('sidebar.sessionListFailed'),
              })
            }
          }}
          icon={<PlusIcon />}
        >
          {t('sidebar.newSession')}
        </NavItem>
        <NavItem
          active={activeTabId === SCHEDULED_TAB_ID}
          collapsed={!sidebarOpen}
          label={t('sidebar.automations')}
          onClick={() => useTabStore.getState().openTab(SCHEDULED_TAB_ID, t('sidebar.automations'), 'scheduled')}
          icon={<ClockIcon />}
        >
          {t('sidebar.automations')}
        </NavItem>
      </div>

      <AgentMonitorPanel />

      {sidebarOpen ? (
        <>
          <div
            data-testid="sidebar-project-filter-section"
            className="sidebar-section sidebar-section--visible relative z-20 flex-none px-3 pb-2"
            style={{ overflow: 'visible' }}
          >
            <div className="flex h-9 items-center rounded-[14px] border border-[var(--color-sidebar-search-border)] bg-[var(--color-sidebar-search-bg)] pl-1.5 pr-3 transition-colors focus-within:border-[var(--color-border-focus)]">
              <ProjectFilter variant="embedded" />
              <span className="mx-2 h-4 w-px bg-[var(--color-border)]/80" aria-hidden="true" />
              <span className="pointer-events-none flex shrink-0 items-center text-[var(--color-text-tertiary)]">
                <SearchIcon />
              </span>
              <input
                id="sidebar-search"
                type="text"
                placeholder={t('sidebar.searchPlaceholder')}
                value={searchQuery}
                onChange={(e) => setSearchQuery(e.target.value)}
                className="min-w-0 flex-1 bg-transparent pl-2 pr-0 text-[13px] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none"
              />
            </div>
          </div>

          <div
            data-testid="sidebar-session-list-section"
            className="sidebar-section sidebar-section--visible flex flex-1 min-h-0 flex-col"
          >
            <div className="sidebar-scroll-area min-h-0 flex-1 overflow-y-auto px-3">
              {error && (
                <div className="mx-1 mt-2 rounded-[var(--radius-md)] border border-[var(--color-error)]/20 bg-[var(--color-error)]/5 px-3 py-2">
                  <div className="text-xs font-medium text-[var(--color-error)]">{t('sidebar.sessionListFailed')}</div>
                  <div className="mt-1 text-[11px] text-[var(--color-text-secondary)] break-words">{error}</div>
                  <button
                    onClick={() => fetchSessions()}
                    className="mt-2 text-[11px] font-medium text-[var(--color-brand)] hover:underline"
                  >
                    {t('common.retry')}
                  </button>
                </div>
              )}
              {filteredSessions.length === 0 && (
                <div className="px-3 py-4 text-center text-xs text-[var(--color-text-tertiary)]">
                  {searchQuery ? t('sidebar.noMatching') : t('sidebar.noSessions')}
                </div>
              )}
              {inSearchMode ? (

                <div className="mb-1 pt-2">
                  {filteredSessions.map(renderSessionRow)}
                </div>
              ) : (
                <>
                  {visibleWorkspaces.map((ws) => {
                    const wsOpen = expandedWorkspaces.has(ws.workspaceKey)
                    return (
                      <div key={ws.workspaceKey} className="mb-1 mt-2">
                        <div className="group/ws relative">
                          <button
                            type="button"
                            onClick={() => toggleWorkspace(ws.workspaceKey)}
                            aria-expanded={wsOpen}
                            aria-label={wsOpen ? t('sidebar.collapseWorkspace') : t('sidebar.expandWorkspace')}
                            title={ws.workspacePath || ws.workspaceLabel}
                            className="flex w-full items-center gap-1.5 rounded-[10px] px-2 py-1 pr-14 text-left text-xs font-semibold tracking-wide text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-sidebar-item-hover)]"
                          >
                            <ChevronRightIcon open={wsOpen} />
                            <FolderIcon existing={ws.workspaceExists} />
                            <span className="flex-1 truncate">{ws.workspaceLabel}</span>
                            {!ws.workspaceExists && ws.workspaceKey !== WORKSPACE_UNKNOWN_KEY && (
                              <span
                                className="flex-shrink-0 text-xs font-normal text-[var(--color-warning)]"
                                title={ws.workspacePath}
                              >
                                {t('sidebar.missingDir')}
                              </span>
                            )}
                            <span className="flex-shrink-0 text-xs font-normal tabular-nums text-[var(--color-text-tertiary)] transition-opacity duration-150 group-hover/ws:opacity-0">
                              {ws.totalCount}
                            </span>
                          </button>
                          {ws.workspaceExists && ws.workspaceKey !== WORKSPACE_UNKNOWN_KEY && (
                            <button
                              type="button"
                              onClick={async (e) => {
                                e.stopPropagation()
                                try {
                                  const sessionId = await useSessionStore
                                    .getState()
                                    .createSession(ws.workspacePath)
                                  useTabStore.getState().openTab(sessionId, t('sidebar.newSession'))
                                  focusSession(sessionId)
                                } catch (error) {
                                  addToast({
                                    type: 'error',
                                    message:
                                      error instanceof Error
                                        ? error.message
                                        : t('sidebar.sessionListFailed'),
                                  })
                                }
                              }}
                              aria-label={t('sidebar.newSessionInWorkspaceAria')}
                              title={t('sidebar.newSessionInWorkspaceAria')}
                              data-testid={`sidebar-workspace-new-session-${ws.workspaceKey}`}
                              className="absolute right-8 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-[var(--color-text-tertiary)] opacity-0 transition-opacity duration-150 hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)] group-hover/ws:opacity-100"
                            >
                              <span className="material-symbols-outlined text-[16px]" aria-hidden="true">add</span>
                            </button>
                          )}
                          <button
                            type="button"
                            onClick={(e) => {
                              e.stopPropagation()
                              handleDeleteWorkspace(ws)
                            }}
                            aria-label={t('sidebar.deleteWorkspaceAria')}
                            title={t('sidebar.deleteWorkspaceAria')}
                            data-testid={`sidebar-workspace-delete-${ws.workspaceKey}`}
                            className="absolute right-1.5 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-[var(--color-text-tertiary)] opacity-0 transition-opacity duration-150 hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-error)] focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)] group-hover/ws:opacity-100"
                          >
                            <TrashIcon />
                          </button>
                        </div>
                        {wsOpen && (
                          <div className="ml-3 mt-0.5">
                            {(() => {
                              const items = ws.sessions
                              const showAll = sessionsShowMore.has(ws.workspaceKey)
                              const visibleItems =
                                showAll || items.length <= SIDEBAR_GROUP_PAGE_SIZE
                                  ? items
                                  : items.slice(0, SIDEBAR_GROUP_PAGE_SIZE)
                              const hiddenCount = items.length - visibleItems.length
                              return (
                                <>
                                  {visibleItems.map(renderSessionRow)}
                                  {hiddenCount > 0 && (
                                    <button
                                      type="button"
                                      onClick={() => revealMoreSessions(ws.workspaceKey)}
                                      className="ml-4 mt-0.5 flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-[var(--color-brand)] transition-colors hover:bg-[var(--color-sidebar-item-hover)]"
                                    >
                                      <span className="material-symbols-outlined text-[14px]" aria-hidden="true">expand_more</span>
                                      {t('sidebar.showMore', { count: hiddenCount })}
                                    </button>
                                  )}
                                </>
                              )
                            })()}
                          </div>
                        )}
                      </div>
                    )
                  })}
                  {!workspacesShowMore && hiddenWorkspaceCount > 0 && (
                    <button
                      type="button"
                      onClick={() => setWorkspacesShowMore(true)}
                      className="mt-2 flex w-full items-center justify-center gap-1 rounded-md px-2 py-1.5 text-[11px] font-medium text-[var(--color-brand)] transition-colors hover:bg-[var(--color-sidebar-item-hover)]"
                    >
                      <span className="material-symbols-outlined text-[14px]" aria-hidden="true">expand_more</span>
                      {t('sidebar.showMoreWorkspaces', { count: hiddenWorkspaceCount })}
                    </button>
                  )}
                </>
              )}
            </div>
          </div>
        </>
      ) : (
        <div className="flex-1" aria-hidden="true" />
      )}

      <div className="flex min-w-0 items-center gap-1 border-t border-[var(--color-border)] p-2">
        <button
          type="button"
          data-lan-panel-toggle
          onClick={() => toggleLanPanel()}
          title={lanNickname || t('lan.title')}
          aria-label={lanNickname || t('lan.title')}
          aria-pressed={lanPanelOpen}
          className={
            `mr-auto flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-1.5 py-1 transition-colors ${
              lanPanelOpen
                ? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
                : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
            }`
          }
        >
          <span className="relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[var(--color-brand)] text-[10px] font-semibold text-white">
            {(lanNickname || '?').trim().slice(0, 1).toUpperCase()}
            <span
              className={`absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border border-[var(--color-surface)] ${
                lanRunning ? 'bg-[var(--color-success,#16a34a)]' : 'bg-[var(--color-text-tertiary)]'
              }`}
            />
          </span>
          <span className="min-w-0 flex-1 truncate text-left text-xs font-medium">
            {lanNickname || t('lan.title')}
          </span>
          {lanUnread > 0 && (
            <span className="ml-0.5 inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-[var(--color-error)] px-1 text-[10px] font-semibold text-white">
              {lanUnread > 99 ? '99+' : lanUnread}
            </span>
          )}
        </button>
        <button
          type="button"
          onClick={() => {
            const sessionId = useTabStore.getState().activeTabId
            if (!sessionId) return
            toggleDesignerCanvas(sessionId)
          }}
          title={t('designer.canvas.toggle')}
          aria-label={t('designer.canvas.toggle')}
          aria-pressed={designerCanvasOpen}
          className={
            designerCanvasOpen
              ? 'inline-flex shrink-0 items-center justify-center rounded-md p-1.5 bg-[var(--color-surface-selected)] text-[var(--color-text-primary)] transition-colors'
              : 'inline-flex shrink-0 items-center justify-center rounded-md p-1.5 text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
          }
        >
          <span className="material-symbols-outlined text-[16px]">palette</span>
        </button>
        {isTauri && (
          <button
            type="button"
            onClick={() => {
              const sessionId = useTabStore.getState().activeTabId
              if (!sessionId) return
              const store = useBrowserPanelStore.getState()
              const panel = store.panels[sessionId]
              if (panel?.visible) {
                void store.closeForSession(sessionId)
              } else {
                void store.toggle(sessionId, { source: 'manual' })
              }
            }}
            title={t('sidebar.browserPanel')}
            aria-label={t('sidebar.browserPanel')}
            className="inline-flex shrink-0 items-center justify-center rounded-md p-1.5 text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
          >
            <span className="material-symbols-outlined text-[16px]">public</span>
          </button>
        )}
        <button
          type="button"
          onClick={() => useUIStore.getState().toggleSettingsOverlay()}
          title={t('sidebar.settings')}
          aria-label={t('sidebar.settings')}
          aria-pressed={settingsOverlayOpen}
          className={
            settingsOverlayOpen
              ? 'inline-flex shrink-0 items-center justify-center rounded-md p-1.5 bg-[var(--color-surface-selected)] text-[var(--color-text-primary)] transition-colors'
              : 'inline-flex shrink-0 items-center justify-center rounded-md p-1.5 text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
          }
        >
          <span className="material-symbols-outlined text-[16px]">settings</span>
        </button>
      </div>

      <UserPanel />
      <GroupsPanel />
      <SharePanel />

      {contextMenu && sidebarOpen && (
        <div
          className="fixed z-50 min-w-[140px] rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] py-1"
          style={{ left: contextMenu.x, top: contextMenu.y, boxShadow: 'var(--shadow-dropdown)' }}
        >
          <button
            onClick={() => {
              const session = sessions.find((s) => s.id === contextMenu.id)
              const seed = isPlaceholderTitle(session?.title) ? '' : (session?.title ?? '')
              handleStartRename(contextMenu.id, seed)
            }}
            className="w-full px-3 py-1.5 text-left text-xs text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-surface-hover)]"
          >
            {t('common.rename')}
          </button>
          <button
            onClick={() => handleDelete(contextMenu.id)}
            className="w-full px-3 py-1.5 text-left text-xs text-[var(--color-error)] transition-colors hover:bg-[var(--color-surface-hover)]"
          >
            {t('common.delete')}
          </button>
        </div>
      )}

      <ConfirmDialog
        open={pendingDeleteSessionId !== null}
        onClose={() => setPendingDeleteSessionId(null)}
        onConfirm={confirmDelete}
        title={t('common.delete')}
        body={pendingDeleteSessionId ? t('sidebar.confirmDelete') : ''}
        confirmLabel={t('common.delete')}
        cancelLabel={t('common.cancel')}
        confirmVariant="danger"
      />

      <ConfirmDialog
        open={pendingDeleteWorkspace !== null}
        onClose={() => {
          if (workspaceDeleting) return
          setPendingDeleteWorkspace(null)
        }}
        onConfirm={confirmDeleteWorkspace}
        title={t('sidebar.deleteWorkspaceTitle')}
        body={
          pendingWorkspaceInfo
            ? pendingWorkspaceInfo.runningCount > 0
              ? t('sidebar.confirmDeleteWorkspaceRunning', {
                  dir: pendingWorkspaceInfo.label,
                  count: pendingWorkspaceInfo.ids.length,
                  running: pendingWorkspaceInfo.runningCount,
                })
              : t('sidebar.confirmDeleteWorkspace', {
                  dir: pendingWorkspaceInfo.label,
                  count: pendingWorkspaceInfo.ids.length,
                })
            : ''
        }
        confirmLabel={t('common.delete')}
        cancelLabel={t('common.cancel')}
        confirmVariant="danger"
        loading={workspaceDeleting}
      />
    </aside>
  )
}

function workspaceBasename(p: string): string {
  const trimmed = p.trim().replace(/[\\/]+$/, '')
  if (!trimmed) return ''
  const parts = trimmed.split(/[\\/]/)
  return parts[parts.length - 1] || trimmed
}

function groupByWorkspace(
  sessions: SessionListItem[],
  unknownLabel: string,
): WorkspaceGroup[] {
  const buckets = new Map<string, SessionListItem[]>()
  for (const s of sessions) {
    const workDir = s.workDir?.trim() ?? ''
    const key = workDir || WORKSPACE_UNKNOWN_KEY
    const list = buckets.get(key)
    if (list) {
      list.push(s)
    } else {
      buckets.set(key, [s])
    }
  }

  const out: WorkspaceGroup[] = []
  for (const [key, items] of buckets) {
    const first = items[0]!
    const isUnknown = key === WORKSPACE_UNKNOWN_KEY
    const workspacePath = isUnknown ? '' : key
    const basename = isUnknown ? '' : workspaceBasename(key)
    const workspaceLabel = isUnknown ? unknownLabel : basename || key

    const workspaceExists = isUnknown ? true : first.workDirExists
    let latest = 0
    for (const s of items) {
      const ts = new Date(s.modifiedAt).getTime()
      if (ts > latest) latest = ts
    }
    const sortedSessions = [...items].sort(
      (a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime(),
    )
    out.push({
      workspaceKey: key,
      workspaceLabel,
      workspacePath,
      workspaceExists,
      latestModifiedAt: latest,
      totalCount: items.length,
      sessions: sortedSessions,
    })
  }

  out.sort((a, b) => b.latestModifiedAt - a.latestModifiedAt)
  return out
}

function sessionWorkspaceKey(s: SessionListItem): string {
  return (s.workDir?.trim() ?? '') || WORKSPACE_UNKNOWN_KEY
}

function sessionIdsForWorkspaceKey(
  sessions: SessionListItem[],
  workspaceKey: string,
): string[] {
  return sessions
    .filter((s) => sessionWorkspaceKey(s) === workspaceKey)
    .map((s) => s.id)
}

function ChevronRightIcon({ open, size = 'md' }: { open: boolean; size?: 'sm' | 'md' }) {
  const px = size === 'sm' ? 'text-[14px]' : 'text-[15px]'
  return (
    <span
      className={`material-symbols-outlined ${px} flex-shrink-0 transition-transform duration-150`}
      style={{ transform: open ? 'rotate(90deg)' : 'rotate(0deg)' }}
      aria-hidden="true"
    >
      chevron_right
    </span>
  )
}

function FolderIcon({ existing }: { existing: boolean }) {
  return (
    <span
      className="material-symbols-outlined text-[15px] flex-shrink-0"
      style={{
        color: existing ? 'var(--color-text-tertiary)' : 'var(--color-warning)',
        fontVariationSettings: "'FILL' 1",
      }}
      aria-hidden="true"
    >
      folder
    </span>
  )
}

function NavItem({
  active,
  collapsed,
  label,
  onClick,
  icon,
  children,
}: {
  active: boolean
  collapsed: boolean
  label: string
  onClick: () => void
  icon: React.ReactNode
  children: React.ReactNode
}) {
  return (
    <button
      onClick={onClick}
      aria-label={label}
      title={collapsed ? label : undefined}
      className={`
        flex items-center transition-colors duration-200
        ${collapsed ? 'h-10 w-10 justify-center rounded-[var(--radius-md)] px-0 py-0' : 'w-full gap-2.5 rounded-[12px] px-3 py-2.5 text-xs'}
        ${active
          ? 'bg-[var(--color-sidebar-item-active)] font-medium text-[var(--color-text-primary)]'
          : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-sidebar-item-hover)] hover:text-[var(--color-text-primary)]'
        }
      `}
    >
      <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
        {icon}
      </span>
      <span className={`sidebar-copy ${collapsed ? 'sidebar-copy--hidden' : 'sidebar-copy--visible'}`}>
        {children}
      </span>
    </button>
  )
}

function formatRelativeTime(dateStr: string): string {
  const diff = Date.now() - new Date(dateStr).getTime()
  const min = Math.floor(diff / 60000)
  if (min < 1) return 'now'
  if (min < 60) return `${min}m`
  const hr = Math.floor(min / 60)
  if (hr < 24) return `${hr}h`
  const day = Math.floor(hr / 24)
  if (day < 30) return `${day}d`
  return `${Math.floor(day / 30)}mo`
}

function PlusIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <line x1="12" y1="5" x2="12" y2="19" />
      <line x1="5" y1="12" x2="19" y2="12" />
    </svg>
  )
}

function ClockIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="12" r="10" />
      <polyline points="12 6 12 12 16 14" />
    </svg>
  )
}

function SearchIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="11" cy="11" r="7" />
      <line x1="21" y1="21" x2="16.65" y2="16.65" />
    </svg>
  )
}

function TrashIcon() {
  return (
    <svg
      width="14"
      height="14"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <polyline points="3 6 5 6 21 6" />
      <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
      <path d="M10 11v6" />
      <path d="M14 11v6" />
      <path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />
    </svg>
  )
}

function GitHubIcon() {
  return (
    <svg width="19" height="19" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"
      />
    </svg>
  )
}

