import { useState } from 'react'
import {
  ChevronRight,
  ChevronDown,
  Folder,
  FolderPlus,
  Image,
  Star,
  Trash2,
  HardDrive,
  Copy,
  Tag as TagIcon,
  Palette,
  MapPin,
  Layers2,
  MoreHorizontal,
  Pencil,
  Settings,
  Users,
  Eye,
  EyeOff,
  User as UserIcon,
  LogOut,
  Shield,
  Clock,
  Upload as UploadIcon,
  Download as DownloadIcon,
} from 'lucide-react'
import clsx from 'clsx'
import { sourceFolders, photos as photosApi, downloads, type FolderTreeNode } from '../../services/api'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from '../ToastContainer'
import { useFilterStore } from '../../store/filterStore'
import { HeapsPanel } from '../heaps/HeapsPanel'
import { ActiveHeapCard } from '../heaps/ActiveHeapCard'
import { PHOTO_DRAG_MIME } from '../timeline/PhotoThumbnail'
import { useFolderTreeQuery } from '../../hooks/useFolderTreeQuery'
import { useTagsQuery } from '../../hooks/useTagsQuery'
import { usePhotoStore } from '../../store/photoStore'
import { stripPhotosFromCache } from '../../hooks/usePhotosQuery'
import {
  useLibraryStatsQuery,
  LIBRARY_STATS_QUERY_KEY,
} from '../../hooks/useLibraryStatsQuery'
import { registerUndoable } from '../../store/undoStore'
import { formatApiError } from '../../lib/apiError'
import type { Photo } from '../../types/photo'
import { DeleteFolderDialog } from '../dialogs/DeleteFolderDialog'
import { ShareDialog } from '../sharing/ShareDialog'
import { UploadModal } from '../upload/UploadModal'
import { useSharedFoldersQuery } from '../../hooks/useSharingQueries'
import { useAuth } from '../../contexts/AuthContext'
import { useFeaturesQuery } from '../../hooks/useFeaturesQuery'
import { useScanActivity } from '../../hooks/useScanActivity'
import { Input } from '@/components/ui/input'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

interface TreeItem {
  id: string
  label: string
  icon?: React.ReactNode
  count?: number
  children?: TreeItem[]
  type?: 'folder' | 'heap' | 'special'
  /** For folder rows only: the user-set "hide from views" flag. Drives
   *  the muted styling + eye-off badge + menu item label. */
  isHidden?: boolean
  /** For folder rows only: filesystem path. Used to match against the
   *  scan-status `current_folder` so we can show an inline spinner on
   *  the row that's actively being scanned. */
  path?: string
}

export function LeftSidebar() {
  const { user, isAdmin, logout } = useAuth()
  const scanActivity = useScanActivity()
  const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['library', 'folders', 'heaps']))
  // Inline rename state for source-root rows. Stores the id being edited
  // and the draft name. Double-click a folder row to start.
  const [renamingId, setRenamingId] = useState<string | null>(null)
  const [renameDraft, setRenameDraft] = useState('')

  const queryClient = useQueryClient()
  const navigateToSection = useFilterStore((s) => s.navigateToSection)
  const currentSection = useFilterStore((s) => s.currentSection)
  const { data: allTags = [] } = useTagsQuery()
  const { data: stats } = useLibraryStatsQuery()
  const { data: featuresMap } = useFeaturesQuery()
  const visionOn = featuresMap ? featuresMap['vision.enabled'] !== false : true
  const tagsOn = true
  const [dropTargetId, setDropTargetId] = useState<string | null>(null)

  // Per-folder kebab menu open state. Stores the tree-item id ("folder-..."
  // or "folders" for the section header). Radix DropdownMenu handles
  // outside-click + Escape dismissal for us — we only track which row's
  // menu is open so the trigger stays visible while it's open (the
  // trigger is hover-hidden by default on non-hovered rows).
  const [openMenuId, setOpenMenuId] = useState<string | null>(null)

  // "Create new folder under {parent}" inline state. parentId is the
  // Folder.id (no "folder-" prefix).
  const [creatingUnder, setCreatingUnder] = useState<string | null>(null)
  const [createDraft, setCreateDraft] = useState('')

  // Folder being deleted, drives the DeleteFolderDialog mounted below.
  const [deletingFolder, setDeletingFolder] = useState<{
    id: string
    name: string
    photoCount?: number
  } | null>(null)
  const [sharingFolder, setSharingFolder] = useState<{
    id: string
    name: string
  } | null>(null)
  // Upload modal state. `uploadTarget` stores the pre-selected
  // destination Folder/SourceRoot id so "Upload here…" on a folder row
  // drops files straight into that folder; null means the general
  // Library-level button (defaults to the first source root).
  const [uploadTarget, setUploadTarget] = useState<{ open: boolean; folderId: string | null }>({
    open: false,
    folderId: null,
  })
  const { data: sharedFolders = [] } = useSharedFoldersQuery()

  // Bulk discard mutation for the drag-onto-Discarded interaction.
  const discardDropMutation = useMutation({
    mutationFn: (photoIds: string[]) => photosApi.bulkDiscard(photoIds),
    // Optimistically pull the dropped photos out of the timeline so the
    // grid reflows the moment the drop lands, instead of waiting for
    // the network round-trip + invalidation refetch.
    onMutate: (photoIds) => {
      usePhotoStore.getState().removePhotosFromTimeline(photoIds)
      stripPhotosFromCache(queryClient, photoIds)
    },
    onSuccess: (_data, photoIds) => {
      registerUndoable(
        `Discarded ${photoIds.length} photo${photoIds.length === 1 ? '' : 's'}`,
        async () => {
          await photosApi.bulkRestore(photoIds)
          queryClient.invalidateQueries({ queryKey: ['photos'] })
          queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
        }
      )
      queryClient.invalidateQueries({ queryKey: ['photos'] })
      queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
    },
    onError: (e: any) =>
      toast.error('Discard failed', formatApiError(e)),
  })

  // Bulk move mutation for the drag-onto-folder interaction. The mutation
  // captures each photo's source folder before issuing the move so the
  // undo path can put them back exactly where they came from (different
  // sources end up in different undo subgroups).
  const moveDropMutation = useMutation({
    mutationFn: async ({
      targetId,
      photoIds,
    }: {
      targetId: string
      photoIds: string[]
    }) => {
      // Snapshot per-photo source folder ids from the photos cache. We
      // walk every cached ['photos', ...] entry because the user could
      // be in any section / filter combination, and we don't know the
      // exact key offhand.
      const sourceMap = new Map<string, string>()
      const photoCaches = queryClient.getQueriesData<Photo[]>({ queryKey: ['photos'] })
      for (const [, list] of photoCaches) {
        if (!list) continue
        for (const p of list) {
          if (photoIds.includes(p.id) && p.folder_id && !sourceMap.has(p.id)) {
            sourceMap.set(p.id, p.folder_id)
          }
        }
      }
      const result = await photosApi.move(photoIds, targetId)
      return { result, sourceMap }
    },
    onSuccess: ({ result, sourceMap }) => {
      const moved = result?.moved ?? 0
      const errCount = result?.errors?.length ?? 0
      if (moved > 0) {
        // Group photos by their source folder so we can issue one move
        // call per group when undoing. Photos whose source folder we
        // couldn't recover get dropped from the undo (they'll just stay
        // where the move put them).
        const groups = new Map<string, string[]>()
        for (const [photoId, src] of sourceMap.entries()) {
          const arr = groups.get(src) ?? []
          arr.push(photoId)
          groups.set(src, arr)
        }
        if (groups.size > 0) {
          registerUndoable(
            `Moved ${moved} photo${moved === 1 ? '' : 's'}`,
            async () => {
              for (const [src, ids] of groups.entries()) {
                await photosApi.move(ids, src)
              }
              queryClient.invalidateQueries({ queryKey: ['photos'] })
              queryClient.invalidateQueries({ queryKey: ['folders'] })
            }
          )
        } else {
          toast.success(
            'Moved',
            `${moved} photo${moved > 1 ? 's' : ''}${errCount ? ` (${errCount} skipped)` : ''}`
          )
        }
      } else if (errCount > 0) {
        toast.error(
          'Move failed',
          `${errCount} file${errCount > 1 ? 's' : ''} could not be moved`
        )
      }
      queryClient.invalidateQueries({ queryKey: ['photos'] })
      queryClient.invalidateQueries({ queryKey: ['folders'] })
    },
    onError: (e: any) =>
      toast.error('Move failed', formatApiError(e)),
  })

  // Bulk copy mutation — Alt-drag uses this instead of move.
  const copyDropMutation = useMutation({
    mutationFn: ({ targetId, photoIds }: { targetId: string; photoIds: string[] }) =>
      photosApi.copy(photoIds, targetId),
    onSuccess: (data) => {
      const copied = data?.copied ?? 0
      const errCount = data?.errors?.length ?? 0
      if (copied > 0) {
        toast.success(
          'Copied',
          `${copied} photo${copied > 1 ? 's' : ''}${errCount ? ` (${errCount} skipped)` : ''}`
        )
      } else if (errCount > 0) {
        toast.error('Copy failed', `${errCount} file${errCount > 1 ? 's' : ''} could not be copied`)
      }
      queryClient.invalidateQueries({ queryKey: ['photos'] })
      queryClient.invalidateQueries({ queryKey: ['folders'] })
    },
    onError: (e: any) =>
      toast.error('Copy failed', formatApiError(e)),
  })

  // Reads the dragged ids out of a drop event payload.
  const readDragIds = (e: React.DragEvent): string[] | null => {
    const raw = e.dataTransfer.getData(PHOTO_DRAG_MIME)
    if (!raw) return null
    try {
      const parsed = JSON.parse(raw) as string[]
      return Array.isArray(parsed) && parsed.length > 0 ? parsed : null
    } catch {
      return null
    }
  }

  // Map a library tree id to a section navigation. Each "virtual node" in
  // the library tree is its own section, with its own remembered filter
  // state. The preset is the section's intrinsic filter (the thing that
  // makes it that section); user-added filters from the FilterBar layer
  // on top and are saved when the user navigates away.
  const applyLibraryNode = (id: string) => {
    switch (id) {
      case 'all-photos':
        navigateToSection('all-photos', {})
        break
      case 'rated':
        navigateToSection('rated', { ratingMin: 1, groupBy: 'rating' })
        break
      case 'discarded':
        navigateToSection('discarded', { flag: 'discarded' })
        break
      case 'duplicates':
        navigateToSection('duplicates', { duplicates: true })
        break
      case 'tags':
        navigateToSection('tags', { groupBy: 'tag' })
        break
      case 'needs-review':
        navigateToSection('needs-review', { needsReview: true })
        break
      case 'colors':
        navigateToSection('colors', { groupBy: 'color' })
        break
      case 'map':
        navigateToSection('map', {})
        break
      case 'memories':
        navigateToSection('memories', {})
        break
      default:
        if (id.startsWith('folder-')) {
          const folderId = id.slice('folder-'.length)
          navigateToSection(`folder-${folderId}`, { folderId })
        }
    }
  }

  // Fetch the recursive folder tree (one root per active source root).
  const { data: folderTree = [] } = useFolderTreeQuery()

  const renameMutation = useMutation({
    mutationFn: ({ id, name }: { id: string; name: string }) =>
      sourceFolders.rename(id, name),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['folders'] })
      queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
      queryClient.invalidateQueries({ queryKey: ['photos'] })
    },
    onError: (e: any) =>
      toast.error('Rename failed', formatApiError(e)),
  })

  const createFolderMutation = useMutation({
    mutationFn: ({ parentId, name }: { parentId: string; name: string }) =>
      sourceFolders.create(parentId, name),
    onSuccess: (data) => {
      toast.success('Folder created', data.name)
      queryClient.invalidateQueries({ queryKey: ['folders'] })
      queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
      setCreatingUnder(null)
      setCreateDraft('')
    },
    onError: (e: any) =>
      toast.error('Create failed', formatApiError(e)),
  })

  // Toggle folder hide-from-views. Invalidates every query that could
  // include photos from the affected folder subtree — the sidebar
  // tree (for the counts + badge), the photos timeline, library
  // stats (sidebar badges), tags (count subquery), and duplicates
  // (source for dup groups). All of these respect the new flag on
  // the server side; the invalidation is just cache bust.
  const toggleHiddenMutation = useMutation({
    mutationFn: ({ id, hidden }: { id: string; hidden: boolean }) =>
      sourceFolders.setHidden(id, hidden),
    onSuccess: (data) => {
      toast.success(
        data.is_hidden ? 'Folder hidden' : 'Folder visible',
        data.is_hidden
          ? `${data.name} is now excluded from cross-cutting views`
          : `${data.name} is back in cross-cutting views`
      )
      queryClient.invalidateQueries({ queryKey: ['folders'] })
      queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
      queryClient.invalidateQueries({ queryKey: ['photos'] })
      queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
      queryClient.invalidateQueries({ queryKey: ['tags'] })
    },
    onError: (e: any) =>
      toast.error('Toggle failed', formatApiError(e)),
  })

  const deleteFolderMutation = useMutation({
    mutationFn: ({ id, mode }: { id: string; mode: 'discard' | 'permanent' }) =>
      sourceFolders.delete(id, mode),
    onSuccess: (data) => {
      if (data.mode === 'discard') {
        toast.success(
          'Folder photos discarded',
          `${data.discarded ?? 0} moved to discard pile`
        )
      } else {
        toast.success(
          'Folder deleted',
          `${data.deleted_photos ?? 0} photos removed from disk`
        )
      }
      queryClient.invalidateQueries({ queryKey: ['folders'] })
      queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
      queryClient.invalidateQueries({ queryKey: ['photos'] })
      queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
      // If we were viewing the deleted folder, snap back to all-photos.
      if (deletingFolder && currentSection === `folder-${deletingFolder.id}`) {
        navigateToSection('all-photos', {})
      }
      setDeletingFolder(null)
    },
    onError: (e: any) =>
      toast.error('Delete failed', formatApiError(e)),
  })

  const toggleExpanded = (id: string) => {
    const newExpanded = new Set(expandedItems)
    if (newExpanded.has(id)) {
      newExpanded.delete(id)
    } else {
      newExpanded.add(id)
    }
    setExpandedItems(newExpanded)
  }

  // Recursively map a backend FolderTreeNode into our generic TreeItem.
  const folderNodeToTreeItem = (node: FolderTreeNode): TreeItem => ({
    id: `folder-${node.id}`,
    label: node.name,
    icon: <Folder className="h-4 w-4" />,
    count: node.photo_count,
    type: 'folder',
    isHidden: node.is_hidden,
    path: node.path,
    children: node.children.length > 0
      ? node.children.map(folderNodeToTreeItem)
      : undefined,
  })

  // Total tag count for the badge on the Tags entry (user tags only).
  const userTags = allTags.filter((t) => t.kind === 'user')
  const tagsTotalCount = userTags.reduce((sum, t) => sum + (t.photo_count || 0), 0)

  const libraryTree: TreeItem[] = [
    {
      id: 'library',
      label: 'Views',
      icon: <Layers2 className="h-4 w-4" />,
      children: [
        { id: 'all-photos', label: 'All Photos', icon: <Image className="h-4 w-4" />, count: stats?.all_photos ?? 0 },
        { id: 'rated', label: 'Rated', icon: <Star className="h-4 w-4" />, count: stats?.rated ?? 0 },
        ...(tagsOn ? [{ id: 'tags', label: 'Tags', icon: <TagIcon className="h-4 w-4" />, count: tagsTotalCount }] : []),
        ...(visionOn ? [{ id: 'needs-review', label: 'Needs Review', icon: <Users className="h-4 w-4" />, count: stats?.needs_review ?? 0 }] : []),
        { id: 'colors', label: 'Colors', icon: <Palette className="h-4 w-4" />, count: stats?.colored ?? 0 },
        { id: 'map', label: 'Map', icon: <MapPin className="h-4 w-4" />, count: stats?.with_gps ?? 0 },
        { id: 'memories', label: 'Memories', icon: <Clock className="h-4 w-4" /> },
        { id: 'duplicates', label: 'Duplicates', icon: <Copy className="h-4 w-4" />, count: stats?.duplicates ?? 0 },
        { id: 'discarded', label: 'Discarded', icon: <Trash2 className="h-4 w-4" />, count: stats?.discarded ?? 0 },
      ],
    },
    {
      id: 'folders',
      label: 'Folders',
      icon: <HardDrive className="h-4 w-4" />,
      children: folderTree.map(folderNodeToTreeItem),
    },
  ]

  // Derive whether a tree item is currently the "active" filter target.
  // Folder rows are selected when the filter store's folderId matches; the
  // library "All Photos" virtual node is selected when no folder/heap filter
  // is set.
  // Active highlight is now driven entirely by currentSection. Each
  // library node and folder row maps 1:1 to a section id.
  const isItemActive = (id: string): boolean => {
    if (id.startsWith('folder-')) {
      return currentSection === id
    }
    return currentSection === id
  }

  // Which tree items accept photo drops, and what each does on drop.
  const isDropTarget = (id: string): boolean => {
    return id === 'discarded' || id.startsWith('folder-')
  }

  const handleDrop = (id: string, ids: string[], copy: boolean) => {
    if (id === 'discarded') {
      discardDropMutation.mutate(ids)
      return
    }
    if (id.startsWith('folder-')) {
      const targetId = id.slice('folder-'.length)
      if (copy) {
        copyDropMutation.mutate({ targetId, photoIds: ids })
      } else {
        moveDropMutation.mutate({ targetId, photoIds: ids })
      }
    }
  }

  const renderTreeItem = (item: TreeItem, depth: number = 0) => {
    const hasChildren = item.children && item.children.length > 0
    const isExpanded = expandedItems.has(item.id)
    const isSelected = isItemActive(item.id)
    const acceptsDrop = isDropTarget(item.id)
    const isDropHover = dropTargetId === item.id
    // Top-level entries (Views, Folders) render as small uppercase
    // section eyebrows rather than another tree row, so the panel reads
    // as distinct sections with the actual items beneath them.
    const isSectionHeader = depth === 0

    return (
      <div key={item.id}>
        <div
          className={clsx(
            'group flex cursor-pointer items-center gap-1',
            isSectionHeader
              ? 'mt-2 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:text-text'
              : clsx(
                  // Fixed h-[24px] (not min-h) locks the row height so the
                  // hover-only kebab button can't grow the row vertically.
                  'h-[24px] rounded px-2 text-[12px] leading-none',
                  isSelected ? 'bg-primary/20 text-primary' : 'text-text hover:bg-surface-2'
                ),
            isDropHover && (item.id === 'discarded'
              ? 'ring-2 ring-reject bg-reject/10'
              : 'ring-2 ring-primary bg-primary/10')
          )}
          style={
            isSectionHeader
              ? undefined
              : { paddingLeft: `${depth * 20}px` }
          }
          onClick={() => {
            if (renamingId === item.id) return
            // Folder rows are always filterable, parent or leaf — clicking
            // anywhere on the row applies the filter and the chevron
            // (separate button below) handles expansion. Other group
            // headers (Library, Folders) just toggle expansion since
            // they have no associated section.
            if (item.id.startsWith('folder-')) {
              applyLibraryNode(item.id)
            } else if (hasChildren) {
              toggleExpanded(item.id)
            } else {
              applyLibraryNode(item.id)
            }
          }}
          onDoubleClick={
            item.id.startsWith('folder-')
              ? (e) => {
                e.stopPropagation()
                setRenamingId(item.id)
                setRenameDraft(item.label)
              }
              : undefined
          }
          onDragOver={acceptsDrop ? (e) => {
            if (e.dataTransfer.types.includes(PHOTO_DRAG_MIME)) {
              e.preventDefault()
              // Alt held → copy (only meaningful for folder targets;
              // discarding doesn't copy).
              const wantCopy = e.altKey && item.id.startsWith('folder-')
              e.dataTransfer.dropEffect = wantCopy ? 'copy' : 'move'
              if (dropTargetId !== item.id) setDropTargetId(item.id)
            }
          } : undefined}
          onDragLeave={acceptsDrop ? (e) => {
            if (!e.currentTarget.contains(e.relatedTarget as Node)) {
              if (dropTargetId === item.id) setDropTargetId(null)
            }
          } : undefined}
          onDrop={acceptsDrop ? (e) => {
            e.preventDefault()
            setDropTargetId(null)
            const ids = readDragIds(e)
            if (ids) handleDrop(item.id, ids, e.altKey)
          } : undefined}
        >
          {/* Expand/Collapse Icon */}
          {hasChildren ? (
            <button
              onClick={(e) => {
                e.stopPropagation()
                toggleExpanded(item.id)
              }}
              className="rounded p-0.5 hover:bg-surface-offset"
            >
              {isExpanded ? (
                <ChevronDown className="h-3 w-3" />
              ) : (
                <ChevronRight className="h-3 w-3" />
              )}
            </button>
          ) : (
            !isSectionHeader && <div className="h-4 w-4 flex-shrink-0" />
          )}

          {/* Item Icon — section headers drop their icon in favor of the
           *  uppercase eyebrow label. Hidden folders swap the folder
           *  icon for an EyeOff so the user sees the state at a glance
           *  without hunting through the kebab menu. */}
          {item.icon && !isSectionHeader && (
            <span
              className={clsx(
                'flex-shrink-0',
                isSelected
                  ? 'text-primary'
                  : item.isHidden
                    ? 'text-text-muted/60'
                    : 'text-text-muted'
              )}
            >
              <span className="[&>svg]:h-3.5 [&>svg]:w-3.5">
                {item.isHidden ? <EyeOff className="h-4 w-4" /> : item.icon}
              </span>
            </span>
          )}

          {/* Label (or inline rename input for folder rows) */}
          {renamingId === item.id ? (
            <Input
              autoFocus
              type="text"
              value={renameDraft}
              onChange={(e) => setRenameDraft(e.target.value)}
              onClick={(e) => e.stopPropagation()}
              onBlur={() => {
                const next = renameDraft.trim()
                const id = item.id.slice('folder-'.length)
                if (next && next !== item.label) {
                  renameMutation.mutate({ id, name: next })
                }
                setRenamingId(null)
              }}
              onKeyDown={(e) => {
                if (e.key === 'Enter') {
                  e.currentTarget.blur()
                } else if (e.key === 'Escape') {
                  setRenamingId(null)
                }
              }}
              className="h-6 flex-1 bg-bg px-1 text-[13px]"
            />
          ) : (
            <span
              className={clsx(
                'flex-1 truncate',
                item.isHidden && !isSelected && 'italic text-text-muted/80'
              )}
              title={item.isHidden ? `${item.label} — hidden from views` : undefined}
            >
              {item.label}
            </span>
          )}

          {/* Activity spinner — shown on the FOLDERS section header
           *  whenever any background scan/processing is happening, and
           *  on a folder row whose path is the one currently being
           *  scanned. Replaces the old bottom-right ScanProgress popup. */}
          {(() => {
            const showOnFolders =
              isSectionHeader && item.id === 'folders' && scanActivity.active
            const showOnFolderRow =
              !!item.path &&
              !!scanActivity.currentFolder &&
              scanActivity.currentFolder.startsWith(item.path)
            if (!showOnFolders && !showOnFolderRow) return null
            return (
              <span
                className="ml-1 inline-block h-2.5 w-2.5 flex-shrink-0 animate-spin rounded-full border border-primary/30 border-t-primary"
                aria-label="Background activity in progress"
              />
            )
          })()}

          {/* Count Badge — fixed-width slot so counts line up in a column
           *  across rows regardless of digit count. Section headers skip
           *  the badge entirely (they're labels, not navigable rows). */}
          {!isSectionHeader && (
            item.count !== undefined && item.count > 0 ? (
              <span className="flex h-4 min-w-[20px] flex-shrink-0 items-center justify-center rounded bg-surface-offset px-1 text-[10px] tabular-nums text-text-muted">
                {item.count}
              </span>
            ) : (
              <span className="h-4 min-w-[20px] flex-shrink-0" aria-hidden="true" />
            )
          )}

          {/* Folder kebab menu — only on folder rows. Hidden (display:none)
           *  until hover so it reserves NO width in the resting state and
           *  the count column stays aligned across folder + non-folder
           *  rows. On hover it appears to the right, pushing the count
           *  left to make room. */}
          {item.id.startsWith('folder-') &&
            (() => {
              const folderId = item.id.slice('folder-'.length)
              const isMenuOpen = openMenuId === item.id
              return (
                <DropdownMenu
                  open={isMenuOpen}
                  onOpenChange={(o) => setOpenMenuId(o ? item.id : null)}
                >
                  <div
                    className={clsx(
                      'relative flex-shrink-0',
                      isMenuOpen ? 'block' : 'hidden group-hover:block'
                    )}
                  >
                    <DropdownMenuTrigger asChild>
                      <button
                        onClick={(e) => e.stopPropagation()}
                        className="rounded p-0.5 text-text-muted hover:bg-surface-offset hover:text-text"
                        title="More actions"
                        aria-label="More folder actions"
                      >
                        <MoreHorizontal className="h-3.5 w-3.5" />
                      </button>
                    </DropdownMenuTrigger>
                  </div>
                  <DropdownMenuContent
                    align="end"
                    className="min-w-[180px]"
                    onClick={(e) => e.stopPropagation()}
                  >
                    <DropdownMenuItem
                      onClick={() =>
                        setUploadTarget({ open: true, folderId })
                      }
                    >
                      <UploadIcon className="h-3.5 w-3.5 text-text-muted" />
                      Upload here…
                    </DropdownMenuItem>
                    <DropdownMenuItem
                      onClick={() => {
                        setCreatingUnder(folderId)
                        setCreateDraft('')
                        if (!expandedItems.has(item.id)) {
                          toggleExpanded(item.id)
                        }
                      }}
                    >
                      <FolderPlus className="h-3.5 w-3.5 text-text-muted" />
                      New sub-folder
                    </DropdownMenuItem>
                    <DropdownMenuItem
                      onClick={() => {
                        setRenamingId(item.id)
                        setRenameDraft(item.label)
                      }}
                    >
                      <Pencil className="h-3.5 w-3.5 text-text-muted" />
                      Rename
                    </DropdownMenuItem>
                    <DropdownMenuItem
                      onClick={() =>
                        toggleHiddenMutation.mutate({
                          id: folderId,
                          hidden: !item.isHidden,
                        })
                      }
                    >
                      {item.isHidden ? (
                        <Eye className="h-3.5 w-3.5 text-text-muted" />
                      ) : (
                        <EyeOff className="h-3.5 w-3.5 text-text-muted" />
                      )}
                      {item.isHidden ? 'Show in views' : 'Hide from views'}
                    </DropdownMenuItem>
                    <DropdownMenuItem
                      onClick={() =>
                        setSharingFolder({ id: folderId, name: item.label })
                      }
                    >
                      <Users className="h-3.5 w-3.5 text-text-muted" />
                      Share…
                    </DropdownMenuItem>
                    <DropdownMenuItem
                      onClick={() =>
                        downloads.trigger(downloads.folderUrl(folderId))
                      }
                    >
                      <DownloadIcon className="h-3.5 w-3.5 text-text-muted" />
                      Download as zip
                    </DropdownMenuItem>
                    <DropdownMenuSeparator />
                    <DropdownMenuItem
                      onClick={() =>
                        setDeletingFolder({
                          id: folderId,
                          name: item.label,
                          photoCount: item.count,
                        })
                      }
                      className="text-reject focus:bg-reject/10 focus:text-reject"
                    >
                      <Trash2 className="h-3.5 w-3.5" />
                      Delete folder…
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
              )
            })()}

        </div>

        {/* Inline "create new sub-folder" input. Renders just below the
         *  parent row when its create state is active. */}
        {item.id.startsWith('folder-') &&
          creatingUnder === item.id.slice('folder-'.length) && (
            <div
              className="flex items-center gap-1 px-2 py-1"
              style={{ paddingLeft: `${(depth + 1) * 20}px` }}
            >
              <FolderPlus className="h-3 w-3 flex-shrink-0 text-text-muted" />
              <input
                autoFocus
                type="text"
                value={createDraft}
                placeholder="New folder name"
                onChange={(e) => setCreateDraft(e.target.value)}
                onClick={(e) => e.stopPropagation()}
                onKeyDown={(e) => {
                  if (e.key === 'Enter') {
                    const name = createDraft.trim()
                    if (name) {
                      createFolderMutation.mutate({
                        parentId: item.id.slice('folder-'.length),
                        name,
                      })
                    }
                  } else if (e.key === 'Escape') {
                    setCreatingUnder(null)
                    setCreateDraft('')
                  }
                }}
                onBlur={() => {
                  // Don't auto-commit on blur — empty/escaped renames
                  // close the input but don't fire the request.
                  if (!createFolderMutation.isPending) {
                    setCreatingUnder(null)
                    setCreateDraft('')
                  }
                }}
                className="flex-1 rounded border border-border bg-bg px-1 py-0 text-[13px] text-text focus:border-primary focus:outline-none"
              />
            </div>
          )}

        {/* Render Children */}
        {hasChildren && isExpanded && (
          <div>
            {item.children!.map((child) => renderTreeItem(child, depth + 1))}
          </div>
        )}
      </div>
    )
  }

  return (
    <div className="flex h-full flex-col bg-surface">
      {/* Header with collapse button. Matches the right sidebar header
       *  so both panels have symmetric affordances. */}
      <div className="flex h-9 flex-shrink-0 items-center justify-between border-b border-border px-3">
        <h2 className="text-[11px] font-semibold uppercase tracking-[0.14em] text-text-muted">
          Library
        </h2>
        <div className="flex items-center gap-1">
          <button
            onClick={() => setUploadTarget({ open: true, folderId: null })}
            className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
            title="Upload photos"
            aria-label="Upload photos"
          >
            <UploadIcon className="h-3.5 w-3.5" />
          </button>
        </div>
      </div>
      {/* Active heap card — pinned just below the Library header so
       *  toasts (bottom-left fixed) can't cover it. Returns null when
       *  no heap is active, so the layout collapses cleanly. */}
      <ActiveHeapCard />

      {/* Tree View */}
      <div className="flex-1 overflow-y-auto pb-2">
        {libraryTree.map((item) => renderTreeItem(item))}

        {/* Shared with me — folders shared by other users */}
        {sharedFolders.length > 0 && (
          <div className="mt-1">
            <div className="px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted">
              Shared with me
            </div>
            {sharedFolders.map((sf) => {
              const isSelected = currentSection === `folder-${sf.id}`
              return (
                <div
                  key={sf.id}
                  className={clsx(
                    'flex h-[24px] cursor-pointer items-center gap-1 rounded px-2 text-[12px] leading-none',
                    isSelected ? 'bg-primary/20 text-primary' : 'text-text hover:bg-surface-2',
                  )}
                  style={{ paddingLeft: '20px' }}
                  onClick={() =>
                    navigateToSection(`folder-${sf.id}`, { folderId: sf.id })
                  }
                >
                  <Users
                    className={clsx(
                      'h-3.5 w-3.5 flex-shrink-0',
                      isSelected ? 'text-primary' : 'text-text-muted'
                    )}
                  />
                  <span className="truncate" title={sf.name}>
                    {sf.name}
                  </span>
                  <span className="ml-0.5 truncate text-[10px] text-text-faint">
                    {sf.owner_username}
                  </span>
                  <span className="ml-auto rounded bg-surface-offset px-1 text-[9px] uppercase text-text-faint">
                    {sf.permission}
                  </span>
                  {sf.photo_count > 0 && (
                    <span className="flex h-4 min-w-[20px] flex-shrink-0 items-center justify-center rounded bg-surface-offset px-1 text-[10px] tabular-nums text-text-muted">
                      {sf.photo_count}
                    </span>
                  )}
                </div>
              )
            })}
          </div>
        )}

        <HeapsPanel />
      </div>

      {/* Bottom panel — user identity + settings, pinned below the tree. */}
      <div className="border-t border-border p-1.5 space-y-0.5">
        {/* User row */}
        <div className="flex items-center gap-2 rounded px-2 py-1 text-[12px] text-text-muted">
          <UserIcon className="h-3.5 w-3.5 flex-shrink-0" />
          <span className="flex-1 truncate text-text">{user?.username}</span>
          {isAdmin && (
            <span className="rounded bg-accent/20 px-1 py-px text-[10px] leading-none text-accent flex-shrink-0">
              <Shield className="inline h-2.5 w-2.5" />
            </span>
          )}
          <button
            onClick={logout}
            className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-red-400 flex-shrink-0"
            title="Sign out"
          >
            <LogOut className="h-3 w-3" />
          </button>
        </div>

        {/* Settings — admin only, navigates to the settings section */}
        {isAdmin && (
          <button
            onClick={() => navigateToSection('settings', {})}
            className={clsx(
              'flex w-full items-center gap-2 rounded px-2 py-1 text-[12px] hover:bg-surface-2 hover:text-text',
              currentSection === 'settings' ? 'text-primary' : 'text-text-muted',
            )}
            title="Settings"
          >
            <Settings className="h-3.5 w-3.5" />
            Settings
          </button>
        )}
      </div>

      <DeleteFolderDialog
        isOpen={!!deletingFolder}
        folderName={deletingFolder?.name ?? ''}
        photoCount={deletingFolder?.photoCount}
        onClose={() => setDeletingFolder(null)}
        onConfirm={(mode) => {
          if (deletingFolder) {
            deleteFolderMutation.mutate({ id: deletingFolder.id, mode })
          }
        }}
      />
      <ShareDialog
        isOpen={!!sharingFolder}
        type="folder"
        targetId={sharingFolder?.id ?? ''}
        targetName={sharingFolder?.name ?? ''}
        onClose={() => setSharingFolder(null)}
      />
      <UploadModal
        isOpen={uploadTarget.open}
        initialFolderId={uploadTarget.folderId}
        onClose={() => setUploadTarget({ open: false, folderId: null })}
      />
    </div>
  )
}

