import { useEffect, useRef, useState } from 'react'
import {
  ChevronRight,
  ChevronDown,
  Folder,
  FolderPlus,
  Image,
  Star,
  Trash2,
  HardDrive,
  Copy,
  Tag as TagIcon,
  Palette,
  Layers2,
  MoreHorizontal,
  Pencil,
  PanelLeftClose,
  Settings,
} from 'lucide-react'
import clsx from 'clsx'
import { sourceFolders, photos as photosApi, 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 {
  useLibraryStatsQuery,
  LIBRARY_STATS_QUERY_KEY,
} from '../../hooks/useLibraryStatsQuery'
import { registerUndoable } from '../../store/undoStore'
import type { Photo } from '../../types/photo'
import { DeleteFolderDialog } from '../dialogs/DeleteFolderDialog'

interface TreeItem {
  id: string
  label: string
  icon?: React.ReactNode
  count?: number
  children?: TreeItem[]
  type?: 'folder' | 'heap' | 'special'
}

interface LeftSidebarProps {
  onCollapse: () => void
  onOpenSettings: () => void
}

export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
  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 [dropTargetId, setDropTargetId] = useState<string | null>(null)

  // Per-folder kebab menu open state. Stores the tree-item id ("folder-..."
  // or "folders" for the section header). Outside-click + Escape close.
  const [openMenuId, setOpenMenuId] = useState<string | null>(null)
  const menuRef = useRef<HTMLDivElement>(null)
  useEffect(() => {
    if (!openMenuId) return
    const onDown = (e: MouseEvent) => {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
        setOpenMenuId(null)
      }
    }
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setOpenMenuId(null)
    }
    document.addEventListener('mousedown', onDown)
    document.addEventListener('keydown', onKey)
    return () => {
      document.removeEventListener('mousedown', onDown)
      document.removeEventListener('keydown', onKey)
    }
  }, [openMenuId])

  // "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)

  // Bulk discard mutation for the drag-onto-Discarded interaction.
  const discardDropMutation = useMutation({
    mutationFn: (photoIds: string[]) => photosApi.bulkDiscard(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', e?.message || 'Unknown error'),
  })

  // 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', e?.response?.data?.detail || e.message || 'Unknown error'),
  })

  // 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', e?.response?.data?.detail || e.message || 'Unknown error'),
  })

  // 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 'colors':
        navigateToSection('colors', { groupBy: 'color' })
        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', e?.response?.data?.detail || e.message || 'Unknown error'),
  })

  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', e?.response?.data?.detail || e.message || 'Unknown error'),
  })

  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', e?.response?.data?.detail || e.message || 'Unknown error'),
  })

  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',
    children: node.children.length > 0
      ? node.children.map(folderNodeToTreeItem)
      : undefined,
  })

  // Total tag count for the badge on the Tags entry.
  const tagsTotalCount = allTags.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 },
        { id: 'tags', label: 'Tags', icon: <TagIcon className="h-4 w-4" />, count: tagsTotalCount },
        { id: 'colors', label: 'Colors', icon: <Palette 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: `${8 + (depth - 1) * 12}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="w-3" />
          )}

          {/* Item Icon — section headers drop their icon in favor of the
           *  uppercase eyebrow label. */}
          {item.icon && !isSectionHeader && (
            <span
              className={clsx(
                'flex-shrink-0',
                isSelected ? 'text-primary' : 'text-text-muted'
              )}
            >
              <span className="[&>svg]:h-3.5 [&>svg]:w-3.5">{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="flex-1 rounded border border-border bg-bg px-1 py-0 text-[13px] text-text focus:border-primary focus:outline-none"
            />
          ) : (
            <span className="flex-1 truncate">{item.label}</span>
          )}

          {/* 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 (
                <div
                  className={clsx(
                    'relative flex-shrink-0',
                    isMenuOpen ? 'block' : 'hidden group-hover:block'
                  )}
                >
                  <button
                    onClick={(e) => {
                      e.stopPropagation()
                      setOpenMenuId(isMenuOpen ? null : item.id)
                    }}
                    className="rounded p-0.5 text-text-muted hover:bg-surface-offset hover:text-text"
                    title="More actions"
                    aria-label="More folder actions"
                    aria-haspopup="menu"
                    aria-expanded={isMenuOpen}
                  >
                    <MoreHorizontal className="h-3.5 w-3.5" />
                  </button>

                  {isMenuOpen && (
                    <div
                      ref={menuRef}
                      role="menu"
                      onClick={(e) => e.stopPropagation()}
                      className="absolute right-0 top-full z-30 mt-1 min-w-[180px] overflow-hidden rounded-lg border border-border bg-surface py-1 text-sm shadow-xl"
                    >
                      <FolderMenuItem
                        icon={<FolderPlus className="h-3.5 w-3.5" />}
                        label="New sub-folder"
                        onClick={() => {
                          setOpenMenuId(null)
                          setCreatingUnder(folderId)
                          setCreateDraft('')
                          // Make sure the parent is expanded so the new
                          // input is visible.
                          if (!expandedItems.has(item.id)) {
                            toggleExpanded(item.id)
                          }
                        }}
                      />
                      <FolderMenuItem
                        icon={<Pencil className="h-3.5 w-3.5" />}
                        label="Rename"
                        onClick={() => {
                          setOpenMenuId(null)
                          setRenamingId(item.id)
                          setRenameDraft(item.label)
                        }}
                      />
                      <div className="my-1 h-px bg-border" />
                      <FolderMenuItem
                        icon={<Trash2 className="h-3.5 w-3.5" />}
                        label="Delete folder…"
                        destructive
                        onClick={() => {
                          setOpenMenuId(null)
                          setDeletingFolder({
                            id: folderId,
                            name: item.label,
                            photoCount: item.count,
                          })
                        }}
                      />
                    </div>
                  )}
                </div>
              )
            })()}

        </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: `${8 + (depth + 1) * 16 + 4}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>
        <button
          onClick={onCollapse}
          className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
          title="Collapse panel (Tab)"
          aria-label="Collapse panel"
        >
          <PanelLeftClose className="h-3.5 w-3.5" />
        </button>
      </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))}
        <HeapsPanel />
      </div>

      {/* Settings entry point — pinned to the bottom of the panel so it
       *  sits out of the way of the library tree but is always reachable. */}
      <div className="border-t border-border p-1.5">
        <button
          onClick={onOpenSettings}
          className="flex w-full items-center gap-2 rounded px-2 py-1 text-[12px] text-text-muted hover:bg-surface-2 hover:text-text"
          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 })
          }
        }}
      />
    </div>
  )
}

function FolderMenuItem({
  icon,
  label,
  onClick,
  destructive = false,
}: {
  icon: React.ReactNode
  label: string
  onClick: () => void
  destructive?: boolean
}) {
  return (
    <button
      role="menuitem"
      onClick={onClick}
      className={clsx(
        'flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors',
        destructive
          ? 'text-reject hover:bg-reject/10'
          : 'text-text hover:bg-surface-2'
      )}
    >
      <span className="text-text-muted">{icon}</span>
      {label}
    </button>
  )
}