import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type React from 'react'
import {
  Archive,
  Download,
  ExternalLink,
  Eye,
  FileText,
  Film,
  Image as ImageIcon,
  Loader2,
  Presentation,
  RefreshCw,
  Search,
  Trash2,
} from 'lucide-react'

import { api } from '../api/client'
import AssetPreviewModal from '../components/AssetPreviewModal'
import Checkbox from '../components/Checkbox'
import ErrorCard from '../components/ErrorCard'
import EmptyState from '../components/EmptyState'
import { useToast } from '../store/toast'
import { useConfirm } from '../components/ConfirmDialog'

type AssetKind = 'html' | 'screenshot' | 'presentation' | 'video'
type SortKey = 'name-asc' | 'name-desc'
type FileSizes = Record<string, number>

interface Preview {
  kind: AssetKind
  filename: string
}

function kindLabel(kind: AssetKind): string {
  if (kind === 'screenshot') return 'screenshots'
  if (kind === 'html') return 'HTML files'
  if (kind === 'presentation') return 'PowerPoint files'
  return 'videos'
}

function kindIcon(kind: AssetKind): React.ReactNode {
  if (kind === 'screenshot') return <ImageIcon size={20} />
  if (kind === 'html') return <FileText size={20} />
  if (kind === 'presentation') return <Presentation size={20} />
  return <Film size={20} />
}

function formatBytes(bytes?: number): string {
  if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes < 0) return 'Size unknown'
  if (bytes < 1024) return `${bytes} B`
  const units = ['KB', 'MB', 'GB']
  let value = bytes / 1024
  let idx = 0
  while (value >= 1024 && idx < units.length - 1) {
    value /= 1024
    idx += 1
  }
  return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${units[idx]}`
}

function classifyGeneratedFile(name: string): { className: string; subject: string } {
  const clean = name.replace(/\.[^.]+$/, '').replace(/[_-]+/g, ' ')
  const classMatch = clean.match(/\bclass\s*(\d{1,2})\b/i)
  const className = classMatch ? `Class ${classMatch[1]}` : 'Unsorted'
  const afterClass = classMatch ? clean.slice((classMatch.index ?? 0) + classMatch[0].length).trim() : clean
  const subject = afterClass.match(/^[a-zA-Z]+/)?.[0]
  return {
    className,
    subject: subject ? subject[0].toUpperCase() + subject.slice(1).toLowerCase() : 'General',
  }
}

function groupGeneratedFiles(files: string[]) {
  const map = new Map<string, Map<string, string[]>>()
  for (const file of files) {
    const { className, subject } = classifyGeneratedFile(file)
    if (!map.has(className)) map.set(className, new Map())
    const subjects = map.get(className)!
    subjects.set(subject, [...(subjects.get(subject) ?? []), file])
  }
  return [...map.entries()]
    .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
    .map(([className, subjects]) => ({
      className,
      subjects: [...subjects.entries()]
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([subject, items]) => ({ subject, items })),
    }))
}

/**
 * How many library entries to render per "page". On a full repo a section
 * can contain several hundred files — rendering the whole grid at once
 * pushes the browser into tens of thousands of DOM nodes (4 tiles per
 * card × 500+ = 2k image tags + metadata) and the scroll becomes
 * unusable. We render in pages and auto-load the next page when the
 * sentinel scrolls into view.
 */
const LIBRARY_PAGE_SIZE = 60

export default function Library() {
  const [kind, setKind] = useState<AssetKind>('screenshot')
  const [screenshots, setScreenshots] = useState<string[]>([])
  const [htmlFiles, setHtmlFiles] = useState<string[]>([])
  const [presentationFiles, setPresentationFiles] = useState<string[]>([])
  const [videoFiles, setVideoFiles] = useState<string[]>([])
  const [htmlSizes, setHtmlSizes] = useState<FileSizes>({})
  const [presentationSizes, setPresentationSizes] = useState<FileSizes>({})
  const [videoSizes, setVideoSizes] = useState<FileSizes>({})
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [query, setQuery] = useState('')
  const [sort, setSort] = useState<SortKey>('name-desc')
  const [selected, setSelected] = useState<Set<string>>(new Set())
  const [preview, setPreview] = useState<Preview | null>(null)
  const [working, setWorking] = useState(false)
  const toast = useToast()
  const confirm = useConfirm()

  const load = useCallback(async () => {
    setLoading(true)
    setError(null)
    try {
      const r = await api.list()
      setScreenshots(r.screenshots ?? [])
      setHtmlFiles(r.html_files ?? [])
      setPresentationFiles(r.presentation_files ?? [])
      setVideoFiles(r.video_files ?? [])
      setHtmlSizes(r.html_sizes ?? {})
      setPresentationSizes(r.presentation_sizes ?? {})
      setVideoSizes(r.video_sizes ?? {})
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setLoading(false)
    }
  }, [])

  // setTimeout(0) defers the first fetch past the effect body so the rule
  // `react-hooks/set-state-in-effect` is satisfied (same pattern as Processes).
  useEffect(() => {
    const t = setTimeout(() => {
      void load()
    }, 0)
    return () => clearTimeout(t)
  }, [load])

  // Selecting a tab also clears any cross-tab selection — done inline below.
  const switchKind = (next: AssetKind) => {
    setKind(next)
    setSelected(new Set())
  }

  const raw =
    kind === 'screenshot'
      ? screenshots
      : kind === 'html'
      ? htmlFiles
      : kind === 'presentation'
      ? presentationFiles
      : videoFiles
  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase()
    const list = q ? raw.filter((f) => f.toLowerCase().includes(q)) : [...raw]
    list.sort((a, b) => (sort === 'name-asc' ? a.localeCompare(b) : b.localeCompare(a)))
    return list
  }, [raw, query, sort])

  // Pagination — reset whenever the visible list identity changes (tab
  // switch, search, sort, underlying list reloaded). We defer the reset
  // with setTimeout(0) so the setState doesn't fire synchronously in an
  // effect body (matches the `load()` pattern above and satisfies
  // react-hooks/set-state-in-effect).
  const [visibleCount, setVisibleCount] = useState(LIBRARY_PAGE_SIZE)
  useEffect(() => {
    const t = setTimeout(() => setVisibleCount(LIBRARY_PAGE_SIZE), 0)
    return () => clearTimeout(t)
  }, [kind, query, sort, raw])
  const visible = useMemo(() => filtered.slice(0, visibleCount), [filtered, visibleCount])
  const hasMore = visibleCount < filtered.length
  const sentinelRef = useRef<HTMLDivElement | null>(null)
  useEffect(() => {
    if (!hasMore) return
    const el = sentinelRef.current
    if (!el) return
    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            setVisibleCount((n) => Math.min(n + LIBRARY_PAGE_SIZE, filtered.length))
          }
        }
      },
      { rootMargin: '320px' },
    )
    observer.observe(el)
    return () => observer.disconnect()
  }, [hasMore, filtered.length])

  const allSelected = filtered.length > 0 && filtered.every((f) => selected.has(f))
  const toggleAll = () => {
    if (allSelected) setSelected(new Set())
    else setSelected(new Set(filtered))
  }

  const toggleOne = (name: string) => {
    setSelected((prev) => {
      const next = new Set(prev)
      if (next.has(name)) next.delete(name)
      else next.add(name)
      return next
    })
  }

  const urlFor = (name: string) =>
    kind === 'screenshot'
      ? api.screenshotUrl(name)
      : kind === 'html'
      ? api.htmlUrl(name)
      : api.downloadUrl(kind === 'presentation' ? `output/presentations/${name}` : `output/videos/${name}`)

  const onDownloadOne = (name: string) => {
    const a = document.createElement('a')
    a.href = urlFor(name)
    a.download = name.split('/').pop() ?? name
    document.body.appendChild(a)
    a.click()
    a.remove()
  }

  const onDownloadZip = async () => {
    if (kind !== 'screenshot' || selected.size === 0) return
    setWorking(true)
    try {
      const blob = await api.downloadZip([...selected], 'library')
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = 'library.zip'
      document.body.appendChild(a)
      a.click()
      a.remove()
      URL.revokeObjectURL(url)
    } catch (e) {
      toast.push({
        variant: 'error',
        title: 'Bulk download failed',
        message: e instanceof Error ? e.message : String(e),
      })
    } finally {
      setWorking(false)
    }
  }

  const onDeleteSelected = async () => {
    if (selected.size === 0) return
    const ok = await confirm({
      title: `Delete ${selected.size} file${selected.size === 1 ? '' : 's'}?`,
      message: 'This permanently removes the files from the backend output folder.',
      confirmLabel: 'Delete',
      variant: 'danger',
    })
    if (!ok) return
    setWorking(true)
    try {
      const type =
        kind === 'screenshot'
          ? 'screenshot'
          : kind === 'html'
          ? 'html'
          : kind === 'presentation'
          ? 'presentation'
          : 'video'
      await Promise.all([...selected].map((name) => api.deleteFile(type, name)))
      const removed = selected.size
      setSelected(new Set())
      await load()
      toast.push({ variant: 'success', message: `Deleted ${removed} file${removed === 1 ? '' : 's'}.` })
    } catch (e) {
      toast.push({
        variant: 'error',
        title: 'Delete failed',
        message: e instanceof Error ? e.message : String(e),
      })
    } finally {
      setWorking(false)
    }
  }

  return (
    <div className="container-page space-y-6">
      <div className="flex flex-wrap items-end justify-between gap-4">
        <div>
          <div className="eyebrow">
            <span className="h-1 w-1 rounded-full bg-brand-500" />
            Outputs
          </div>
          <h1 className="h-page mt-2">Library</h1>
          <p className="mt-2 text-sm text-muted">
            Every screenshot, HTML file, PowerPoint deck, and video produced
            by the backend. Preview, download, or clean up.
          </p>
        </div>
        <button type="button" className="btn-secondary" onClick={load} disabled={loading}>
          <RefreshCw size={14} className={loading ? 'animate-spin' : ''} /> Refresh
        </button>
      </div>

      {/* Tabs — keyboard navigable per WAI-ARIA tablist pattern. */}
      <LibraryTabs
        kind={kind}
        screenshots={screenshots.length}
        htmlFiles={htmlFiles.length}
        presentationFiles={presentationFiles.length}
        videoFiles={videoFiles.length}
        onSwitch={switchKind}
      />

      {/* Controls row */}
      <div className="card flex flex-wrap items-center gap-3">
        <div className="relative min-w-[220px] flex-1">
          <label htmlFor="library-search" className="sr-only">
            Search {kindLabel(kind)}
          </label>
          <Search
            size={14}
            className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
          />
          <input
            id="library-search"
            type="search"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder={`Search ${kindLabel(kind)}...`}
            className="input !pl-9"
          />
        </div>
        <label htmlFor="library-sort" className="sr-only">
          Sort order
        </label>
        <select
          id="library-sort"
          value={sort}
          onChange={(e) => setSort(e.target.value as SortKey)}
          className="select max-w-[220px]"
        >
          <option value="name-desc">Sort: Name (Z → A)</option>
          <option value="name-asc">Sort: Name (A → Z)</option>
        </select>

        <div className="ml-auto flex items-center gap-2">
          <Checkbox
            checked={allSelected}
            onChange={() => toggleAll()}
            label={<span className="text-xs text-muted">Select all ({filtered.length})</span>}
          />
          {kind === 'screenshot' && (
            <button
              type="button"
              className="btn-secondary"
              disabled={selected.size === 0 || working}
              onClick={onDownloadZip}
              title="Zip selected screenshots"
            >
              <Archive size={14} /> Download ZIP
            </button>
          )}
          <button
            type="button"
            className="btn-danger"
            disabled={selected.size === 0 || working}
            onClick={onDeleteSelected}
          >
            <Trash2 size={14} /> Delete
          </button>
        </div>
      </div>

      {/* Grid / list — rendered as the tabpanel for the selected kind. */}
      <div
        role="tabpanel"
        id={`library-panel-${kind}`}
        aria-labelledby={`library-tab-${kind}`}
        tabIndex={0}
      >
      {error ? (
        <ErrorCard title="Couldn't load library" message={error} onRetry={load} />
      ) : loading ? (
        <div className="card flex items-center justify-center gap-2 py-10 text-sm text-slate-500">
          <Loader2 size={16} className="animate-spin" /> Loading…
        </div>
      ) : filtered.length === 0 ? (
        raw.length === 0 ? (
          <EmptyState
            icon={kindIcon(kind)}
            title={`No ${kindLabel(kind)} yet`}
            description={
              <>
                Run a Text→Video, HTML→Video, or Image→Video job and the
                outputs will land here automatically.
              </>
            }
          />
        ) : (
          <EmptyState
            variant="muted"
            icon={<Search size={20} />}
            title="No matches for your search"
            description="Try a shorter query, clear the filter, or switch tabs."
          />
        )
      ) : kind === 'screenshot' ? (
        <>
          <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
            {visible.map((name) => (
              <ScreenshotCard
                key={name}
                name={name}
                selected={selected.has(name)}
                onToggle={() => toggleOne(name)}
                onPreview={() => setPreview({ kind: 'screenshot', filename: name })}
                onDownload={() => onDownloadOne(name)}
              />
            ))}
          </div>
          <LibraryPaginator
            sentinelRef={sentinelRef}
            hasMore={hasMore}
            shown={visible.length}
            total={filtered.length}
            onLoadMore={() => setVisibleCount((n) => Math.min(n + LIBRARY_PAGE_SIZE, filtered.length))}
          />
        </>
      ) : kind === 'html' ? (
        <>
          <div className="card divide-y divide-slate-100 dark:divide-white/5">
            {visible.map((name) => (
              <HtmlRow
                key={name}
                name={name}
                size={htmlSizes[name]}
                selected={selected.has(name)}
                onToggle={() => toggleOne(name)}
                onPreview={() => setPreview({ kind: 'html', filename: name })}
                onDownload={() => onDownloadOne(name)}
              />
            ))}
          </div>
          <LibraryPaginator
            sentinelRef={sentinelRef}
            hasMore={hasMore}
            shown={visible.length}
            total={filtered.length}
            onLoadMore={() => setVisibleCount((n) => Math.min(n + LIBRARY_PAGE_SIZE, filtered.length))}
          />
        </>
      ) : (
        <>
          <GroupedGeneratedFiles
            kind={kind}
            files={visible}
            sizes={kind === 'presentation' ? presentationSizes : videoSizes}
            selected={selected}
            onToggle={toggleOne}
            onPreview={(name) => setPreview({ kind, filename: name })}
            onDownload={onDownloadOne}
            onOpenExternal={(name) => {
              // E3: PowerPoint is not previewable inline — opening the
              // download URL in a new tab lets the OS hand off to the
              // user's preferred editor (PowerPoint / Keynote / web).
              const url = urlFor(name)
              window.open(url, '_blank', 'noopener')
            }}
          />
          <LibraryPaginator
            sentinelRef={sentinelRef}
            hasMore={hasMore}
            shown={visible.length}
            total={filtered.length}
            onLoadMore={() => setVisibleCount((n) => Math.min(n + LIBRARY_PAGE_SIZE, filtered.length))}
          />
        </>
      )}
      </div>

      {preview && preview.kind !== 'presentation' && (
        <AssetPreviewModal
          kind={preview.kind === 'screenshot' ? 'image' : preview.kind === 'html' ? 'html' : 'video'}
          src={urlFor(preview.filename)}
          title={preview.filename.split('/').pop() ?? preview.filename}
          subtitle={
            preview.kind === 'html'
              ? 'HTML file'
              : preview.kind === 'video'
              ? 'MP4 video'
              : preview.filename
          }
          onClose={() => setPreview(null)}
        />
      )}
    </div>
  )
}

function LibraryPaginator({
  sentinelRef,
  hasMore,
  shown,
  total,
  onLoadMore,
}: {
  sentinelRef: React.MutableRefObject<HTMLDivElement | null>
  hasMore: boolean
  shown: number
  total: number
  onLoadMore: () => void
}) {
  if (total <= LIBRARY_PAGE_SIZE) return null
  return (
    <div className="mt-4 flex flex-col items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
      <div aria-live="polite">
        Showing {shown.toLocaleString()} of {total.toLocaleString()} items
      </div>
      {hasMore ? (
        <>
          <button type="button" className="btn-secondary btn-sm" onClick={onLoadMore}>
            Load more
          </button>
          <div ref={sentinelRef} aria-hidden="true" className="h-1 w-full" />
        </>
      ) : null}
    </div>
  )
}

function ScreenshotCard({
  name,
  selected,
  onToggle,
  onPreview,
  onDownload,
}: {
  name: string
  selected: boolean
  onToggle: () => void
  onPreview: () => void
  onDownload: () => void
}) {
  return (
    <div
      className={
        'group relative overflow-hidden rounded-xl border bg-white shadow-glass transition-shadow hover:shadow-glass-lg dark:bg-white/[0.03] ' +
        (selected
          ? 'border-brand-400 ring-2 ring-brand-200 dark:border-brand-500/60 dark:ring-brand-500/30'
          : 'border-slate-200 dark:border-white/10')
      }
    >
      <span className="absolute left-2 top-2 z-10 flex h-6 w-6 items-center justify-center rounded-md bg-white/90 shadow-sm backdrop-blur-sm dark:bg-slate-900/80">
        <Checkbox
          checked={selected}
          onChange={() => onToggle()}
          hideLabel
          ariaLabel={`Select ${name}`}
        />
      </span>
      <button
        type="button"
        onClick={onPreview}
        className="block aspect-[16/10] w-full overflow-hidden bg-slate-100 dark:bg-slate-900/40"
      >
        <img
          src={api.screenshotUrl(name)}
          alt={name}
          loading="lazy"
          className="h-full w-full object-cover transition-transform group-hover:scale-[1.02]"
        />
      </button>
      <div className="flex items-center gap-2 px-3 py-2">
        <div className="min-w-0 flex-1 truncate text-xs text-slate-700 dark:text-slate-200" title={name}>
          {name}
        </div>
        <button
          type="button"
          onClick={onPreview}
          className="btn-ghost !px-1.5 !py-1"
          aria-label="Preview"
        >
          <Eye size={14} />
        </button>
        <button
          type="button"
          onClick={onDownload}
          className="btn-ghost !px-1.5 !py-1"
          aria-label="Download"
        >
          <Download size={14} />
        </button>
      </div>
    </div>
  )
}

function HtmlRow({
  name,
  size,
  selected,
  onToggle,
  onPreview,
  onDownload,
}: {
  name: string
  size?: number
  selected: boolean
  onToggle: () => void
  onPreview: () => void
  onDownload: () => void
}) {
  return (
    <div className="flex items-center gap-3 py-3">
      <Checkbox
        checked={selected}
        onChange={() => onToggle()}
        hideLabel
        ariaLabel={`Select ${name}`}
      />
      <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-slate-500 dark:bg-white/[0.05] dark:text-slate-300">
        <FileText size={16} />
      </div>
      <button
        type="button"
        onClick={onPreview}
        className="min-w-0 flex-1 text-left text-sm font-medium text-slate-800 hover:text-brand-700 dark:text-slate-100 dark:hover:text-brand-300"
      >
        <span className="block truncate">{name}</span>
        <span className="text-xs font-normal text-slate-500 dark:text-slate-400">{formatBytes(size)}</span>
      </button>
      <button
        type="button"
        onClick={onPreview}
        className="btn-ghost !px-2 !py-1"
        aria-label="Preview"
      >
        <Eye size={14} />
      </button>
      <button
        type="button"
        onClick={onDownload}
        className="btn-ghost !px-2 !py-1"
        aria-label="Download"
      >
        <Download size={14} />
      </button>
    </div>
  )
}

// ─── Tabs ────────────────────────────────────────────────────────────────
// WAI-ARIA tablist pattern with roving-tabindex + arrow-key navigation, so
// Left/Right, Home/End walk between "Screenshots" and "HTML files" and the
// active tab is the one in the document tab order.
function GroupedGeneratedFiles({
  kind,
  files,
  sizes,
  selected,
  onToggle,
  onPreview,
  onDownload,
  onOpenExternal,
}: {
  kind: 'presentation' | 'video'
  files: string[]
  sizes: FileSizes
  selected: Set<string>
  onToggle: (name: string) => void
  onPreview: (name: string) => void
  onDownload: (name: string) => void
  onOpenExternal?: (name: string) => void
}) {
  const groups = groupGeneratedFiles(files)
  return (
    <div className="space-y-4">
      {groups.map((group) => (
        <section key={group.className} className="rounded-lg border border-slate-200 bg-[rgb(var(--bg-surface))] p-4 dark:border-white/10">
          <div className="mb-3 flex items-center justify-between gap-3">
            <h2 className="text-sm font-semibold text-[rgb(var(--text-strong))]">{group.className}</h2>
            <span className="text-xs text-slate-500 dark:text-slate-400">
              {group.subjects.reduce((sum, subject) => sum + subject.items.length, 0)} file(s)
            </span>
          </div>
          <div className="space-y-3">
            {group.subjects.map((subject) => (
              <div key={`${group.className}-${subject.subject}`} className="rounded-md border border-slate-200 bg-slate-50/60 dark:border-white/10 dark:bg-white/[0.03]">
                <div className="flex items-center justify-between gap-3 border-b border-slate-200 px-3 py-2 dark:border-white/10">
                  <h3 className="text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-300">{subject.subject}</h3>
                  <span className="text-[11px] text-slate-500 dark:text-slate-400">{subject.items.length} item(s)</span>
                </div>
                <div className="divide-y divide-slate-200 px-3 dark:divide-white/10">
                  {subject.items.map((name) => (
                    <GeneratedFileRow
                      key={name}
                      kind={kind}
                      name={name}
                      size={sizes[name]}
                      selected={selected.has(name)}
                      onToggle={() => onToggle(name)}
                      onPreview={kind === 'video' ? () => onPreview(name) : undefined}
                      onOpenExternal={
                        kind === 'presentation' && onOpenExternal
                          ? () => onOpenExternal(name)
                          : undefined
                      }
                      onDownload={() => onDownload(name)}
                    />
                  ))}
                </div>
              </div>
            ))}
          </div>
        </section>
      ))}
    </div>
  )
}

function GeneratedFileRow({
  kind,
  name,
  size,
  selected,
  onToggle,
  onPreview,
  onOpenExternal,
  onDownload,
}: {
  kind: 'presentation' | 'video'
  name: string
  size?: number
  selected: boolean
  onToggle: () => void
  onPreview?: () => void
  onOpenExternal?: () => void
  onDownload: () => void
}) {
  const Icon = kind === 'presentation' ? Presentation : Film
  return (
    <div className="flex items-center gap-3 py-3">
      <Checkbox
        checked={selected}
        onChange={() => onToggle()}
        hideLabel
        ariaLabel={`Select ${name}`}
      />
      <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-slate-500 dark:bg-white/[0.05] dark:text-slate-300">
        <Icon size={16} />
      </div>
      <div className="min-w-0 flex-1 text-sm font-medium text-slate-800 dark:text-slate-100">
        <span className="block truncate">{name}</span>
        <span className="text-xs font-normal text-slate-500 dark:text-slate-400">
          {kind === 'presentation' ? 'PowerPoint deck' : 'MP4 video'} · {formatBytes(size)}
        </span>
      </div>
      {onPreview && (
        <button type="button" onClick={onPreview} className="btn-ghost !px-2 !py-1" aria-label="Preview">
          <Eye size={14} />
        </button>
      )}
      {onOpenExternal && (
        <button
          type="button"
          onClick={onOpenExternal}
          className="btn-ghost !px-2 !py-1"
          aria-label="Open externally"
          title="Open in PowerPoint / web viewer"
        >
          <ExternalLink size={14} />
        </button>
      )}
      <button type="button" onClick={onDownload} className="btn-ghost !px-2 !py-1" aria-label="Download">
        <Download size={14} />
      </button>
    </div>
  )
}

function LibraryTabs({
  kind,
  screenshots,
  htmlFiles,
  presentationFiles,
  videoFiles,
  onSwitch,
}: {
  kind: AssetKind
  screenshots: number
  htmlFiles: number
  presentationFiles: number
  videoFiles: number
  onSwitch: (next: AssetKind) => void
}) {
  const tabs: { id: AssetKind; label: string; icon: typeof FileText; count: number }[] = [
    { id: 'screenshot', label: 'Screenshots', icon: ImageIcon, count: screenshots },
    { id: 'html', label: 'HTML files', icon: FileText, count: htmlFiles },
    { id: 'presentation', label: 'PowerPoint', icon: Presentation, count: presentationFiles },
    { id: 'video', label: 'Videos', icon: Film, count: videoFiles },
  ]
  const refs = useRef<Array<HTMLButtonElement | null>>([])

  const focusAt = (idx: number) => {
    const n = tabs.length
    const target = ((idx % n) + n) % n
    refs.current[target]?.focus()
    onSwitch(tabs[target].id)
  }

  const onKey = (e: React.KeyboardEvent, idx: number) => {
    switch (e.key) {
      case 'ArrowRight':
        e.preventDefault()
        focusAt(idx + 1)
        break
      case 'ArrowLeft':
        e.preventDefault()
        focusAt(idx - 1)
        break
      case 'Home':
        e.preventDefault()
        focusAt(0)
        break
      case 'End':
        e.preventDefault()
        focusAt(tabs.length - 1)
        break
    }
  }

  return (
    <div
      role="tablist"
      aria-label="Library kind"
      className="flex flex-wrap items-center gap-2 border-b border-slate-200 dark:border-white/10"
    >
      {tabs.map((t, i) => {
        const active = kind === t.id
        return (
          <button
            key={t.id}
            ref={(el) => {
              refs.current[i] = el
            }}
            type="button"
            role="tab"
            id={`library-tab-${t.id}`}
            aria-selected={active}
            aria-controls={`library-panel-${t.id}`}
            tabIndex={active ? 0 : -1}
            onClick={() => onSwitch(t.id)}
            onKeyDown={(e) => onKey(e, i)}
            className={
              'flex items-center gap-2 border-b-2 px-3 py-2 text-sm font-medium transition-colors ' +
              (active
                ? 'border-brand-500 text-brand-700 dark:text-brand-200'
                : 'border-transparent text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100')
            }
          >
            <t.icon size={14} />
            {t.label}
            <span className="ml-1 rounded-full bg-slate-100 px-1.5 text-[11px] font-medium text-slate-600 dark:bg-white/[0.05] dark:text-slate-300">
              {t.count}
            </span>
          </button>
        )
      })}
    </div>
  )
}
