'use client'

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  ChevronUp,
  Plus,
  Search,
  Tag as TagIcon,
  Trash2,
  X,
} from 'lucide-react'
import { toast } from 'sonner'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { adminPortalContainer } from '../../lib/portal-container.js'
import {
  applyPagesQuery,
  bulkChangePageTags,
  bulkDeletePages,
  bulkUpdatePages,
  fetchPagesDataset,
  groupPages,
  type BulkResult,
  type Page,
  type PageGroupBy,
  type PagesDataset,
  type PageStatus,
  type PageTag,
} from '../../lib/pages-service.js'
import { PostStatusBadge, SeoScoreIndicator } from '../../components/posts/badges.js'
import { PageTagChip, PageTagsCell } from '../../components/pages/tag-badges.js'
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'

/**
 * Pages management surface — table with a tag-chip filter bar, group-by,
 * status filter, sortable headers, bulk actions, and pagination. Data
 * flows through pages-service so a future server-side aggregate only
 * touches the service file.
 *
 * Routing:
 *   - Row click → /pages/:id/edit (the page builder).
 *   - "+ New page" lives in the top bar (computePageActions).
 *   - "Manage tags" (in the tag bar) → /pages/tags.
 */

interface PagesListViewProps {
  onNavigate: (path: string) => void
}

type SortBy = 'title' | 'author' | 'status' | 'modified' | 'seo'
type Dir = 'asc' | 'desc'

const PAGE_SIZE = 25

export function PagesListView({ onNavigate }: PagesListViewProps) {
  const [search, setSearch] = useState('')
  const [debouncedSearch, setDebouncedSearch] = useState('')
  const [status, setStatus] = useState<PageStatus | 'all'>('all')
  const [groupBy, setGroupBy] = useState<PageGroupBy>('none')
  const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set())
  const [sortBy, setSortBy] = useState<SortBy>('title')
  const [dir, setDir] = useState<Dir>('asc')
  const [page, setPage] = useState(1)
  const [selected, setSelected] = useState<Set<string>>(new Set())

  const [dataset, setDataset] = useState<PagesDataset | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [refreshToken, setRefreshToken] = useState(0)
  const [pendingDelete, setPendingDelete] = useState(false)

  const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
  useEffect(() => {
    if (debounceTimer.current) clearTimeout(debounceTimer.current)
    debounceTimer.current = setTimeout(() => {
      setDebouncedSearch(search.trim())
      setPage(1)
    }, 250)
    return () => {
      if (debounceTimer.current) clearTimeout(debounceTimer.current)
    }
  }, [search])

  const tagIds = useMemo(() => [...selectedTags], [selectedTags])

  // Single network read. Search / sort / group / paging are applied in
  // memory below, so changing any of those never re-fetches. The dataset
  // refreshes only on mount and after a mutation (refreshToken bump).
  useEffect(() => {
    let cancelled = false
    setLoading(true)
    setError(null)
    fetchPagesDataset()
      .then((res) => {
        if (cancelled) return
        setDataset(res)
        setLoading(false)
      })
      .catch((err: unknown) => {
        if (cancelled) return
        setError(err instanceof Error ? err.message : 'Failed to load pages')
        setLoading(false)
      })
    return () => {
      cancelled = true
    }
  }, [refreshToken])

  // Reset paging/selection when the active filters change shape.
  useEffect(() => {
    setPage(1)
    setSelected(new Set())
  }, [debouncedSearch, status, tagIds, groupBy])

  const allPages = useMemo(() => dataset?.pages ?? [], [dataset])
  const tags = useMemo(() => dataset?.tags ?? [], [dataset])
  const grandTotal = allPages.length
  const grouped = groupBy !== 'none'

  const filtered = useMemo(
    () =>
      applyPagesQuery(allPages, {
        status,
        tagIds,
        search: debouncedSearch,
        sortBy,
        sortDirection: dir,
      }),
    [allPages, status, tagIds, debouncedSearch, sortBy, dir],
  )
  const total = filtered.length
  const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))

  // Pagination only applies to the flat view; grouped views render every
  // match so the buckets stay complete.
  const pages = useMemo(
    () => (grouped ? filtered : filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)),
    [filtered, grouped, page],
  )

  const tagBySlug = useMemo(() => {
    const m = new Map<string, PageTag>()
    for (const t of tags) m.set(t.slug, t)
    return m
  }, [tags])

  const displayGroups = useMemo(() => groupPages(pages, groupBy, tags), [pages, groupBy, tags])

  const handleSort = useCallback(
    (field: SortBy) => {
      if (sortBy === field) {
        setDir((d) => (d === 'asc' ? 'desc' : 'asc'))
      } else {
        setSortBy(field)
        setDir(field === 'modified' || field === 'seo' ? 'desc' : 'asc')
      }
    },
    [sortBy],
  )

  // `aria-sort` belongs on the column header cell (`<th>`), not the button
  // inside it — assistive tech reads sort state from the header.
  const ariaSortFor = (field: SortBy): 'ascending' | 'descending' | 'none' =>
    sortBy === field ? (dir === 'asc' ? 'ascending' : 'descending') : 'none'

  const filtersAreActive = debouncedSearch !== '' || status !== 'all' || selectedTags.size > 0

  const clearFilters = () => {
    setSearch('')
    setStatus('all')
    setSelectedTags(new Set())
    setPage(1)
  }

  const toggleTag = (slug: string) =>
    setSelectedTags((s) => {
      const next = new Set(s)
      if (next.has(slug)) next.delete(slug)
      else next.add(slug)
      return next
    })

  // ─── Selection ─────────────────────────────────────────────────────

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

  const allOnPageSelected = pages.length > 0 && pages.every((p) => selected.has(p.id))
  const toggleAllOnPage = (checked: boolean) =>
    setSelected((s) => {
      const next = new Set(s)
      if (checked) for (const p of pages) next.add(p.id)
      else for (const p of pages) next.delete(p.id)
      return next
    })

  const selectedPages = useMemo(() => pages.filter((p) => selected.has(p.id)), [pages, selected])
  const deletablePages = useMemo(() => selectedPages.filter((p) => !p.isHomepage), [selectedPages])
  const blockedHomepage = selectedPages.length !== deletablePages.length

  // ─── Bulk actions ──────────────────────────────────────────────────

  const afterBulk = () => {
    setSelected(new Set())
    setRefreshToken((t) => t + 1)
  }

  // Bulk service calls always resolve a { ok, failed } tally (they never
  // reject), so we promote a *total* failure to a rejection — that's what
  // makes `toast.promise`'s `error` branch reachable. The `await` is
  // wrapped so a rejection can't escape as an unhandled promise, and
  // `afterBulk` always runs to refresh the (possibly partially-changed) set.
  const allFailed = (r: BulkResult) => r.ok === 0 && r.failed > 0

  const runBulkStatus = async (target: PageStatus) => {
    const ids = selectedPages.map((p) => p.id)
    if (ids.length === 0) return
    const verb = target === 'PUBLISHED' ? 'Publishing' : 'Updating'
    const promise = bulkUpdatePages(ids, { status: target }).then((r) => {
      if (allFailed(r)) throw new Error('Bulk update failed')
      return r
    })
    toast.promise(promise, {
      loading: `${verb} ${ids.length} page${ids.length === 1 ? '' : 's'}…`,
      success: (r) => `${r.ok} updated${r.failed ? `, ${r.failed} failed` : ''}`,
      error: 'Bulk update failed',
    })
    try {
      await promise
    } catch {
      /* surfaced via toast.promise */
    }
    afterBulk()
  }

  const runBulkTag = async (slug: string, mode: 'add' | 'remove') => {
    if (selectedPages.length === 0) return
    const promise = bulkChangePageTags(
      selectedPages.map((p) => ({ id: p.id, tags: p.tags, primaryTagId: p.primaryTagId })),
      mode === 'add' ? { add: [slug] } : { remove: [slug] },
    ).then((r) => {
      if (allFailed(r)) throw new Error('Bulk tag change failed')
      return r
    })
    const tagName = tagBySlug.get(slug)?.name ?? slug
    toast.promise(promise, {
      loading: `${mode === 'add' ? 'Adding' : 'Removing'} "${tagName}"…`,
      success: (r) =>
        `${r.ok} page${r.ok === 1 ? '' : 's'} updated${r.failed ? `, ${r.failed} failed` : ''}`,
      error: 'Bulk tag change failed',
    })
    try {
      await promise
    } catch {
      /* surfaced via toast.promise */
    }
    afterBulk()
  }

  const runBulkDelete = async () => {
    const ids = deletablePages.map((p) => p.id)
    if (ids.length === 0) return
    const promise = bulkDeletePages(ids).then((r) => {
      if (allFailed(r)) throw new Error('Bulk delete failed')
      return r
    })
    toast.promise(promise, {
      loading: `Deleting ${ids.length} page${ids.length === 1 ? '' : 's'}…`,
      success: (r) => `${r.ok} deleted${r.failed ? `, ${r.failed} failed` : ''}`,
      error: 'Bulk delete failed',
    })
    try {
      await promise
    } catch {
      /* surfaced via toast.promise */
    }
    afterBulk()
  }

  // ─── Render ────────────────────────────────────────────────────────

  return (
    <div className="flex h-full flex-col gap-4 p-4 sm:p-6">
      {/* Header */}
      <div className="flex flex-col gap-1">
        <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">Pages</h1>
        <p className="text-muted-foreground text-sm">
          {grandTotal} page{grandTotal === 1 ? '' : 's'} · organized by tags
        </p>
      </div>

      {/* Controls row */}
      <div className="flex flex-wrap items-center gap-2">
        <div className="relative max-w-sm flex-1">
          <Search
            className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
            aria-hidden
          />
          <input
            type="search"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            aria-label="Search pages"
            placeholder="Search pages…"
            className="border-border bg-card text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border py-2 pr-3 pl-9 text-sm shadow-sm focus:ring-2 focus:outline-none"
          />
        </div>

        <FilterSelect
          label="Group by"
          value={groupBy}
          onChange={(v) => setGroupBy(v as PageGroupBy)}
          options={[
            { value: 'none', label: 'None' },
            { value: 'tag', label: 'Tag' },
            { value: 'status', label: 'Status' },
            { value: 'author', label: 'Author' },
          ]}
        />

        <FilterSelect
          label="Status"
          value={status}
          onChange={(v) => {
            setStatus(v as PageStatus | 'all')
            setPage(1)
          }}
          options={[
            { value: 'all', label: 'All statuses' },
            { value: 'PUBLISHED', label: 'Published' },
            { value: 'DRAFT', label: 'Draft' },
            { value: 'SCHEDULED', label: 'Scheduled' },
          ]}
        />

        <span className="text-muted-foreground ml-auto text-sm tabular-nums">
          {total} of {grandTotal}
        </span>
      </div>

      {/* Tag filter bar */}
      <div className="flex flex-wrap items-center gap-2">
        <span className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
          Tags
        </span>
        {tags.map((t) => (
          <PageTagChip
            key={t.slug}
            name={t.name}
            color={t.color}
            count={t.pageCount}
            selected={selectedTags.has(t.slug)}
            onToggle={() => toggleTag(t.slug)}
          />
        ))}
        {selectedTags.size > 0 && (
          <button
            type="button"
            onClick={() => setSelectedTags(new Set())}
            className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs"
          >
            <X className="h-3 w-3" aria-hidden />
            Clear tags
          </button>
        )}
        <button
          type="button"
          onClick={() => onNavigate('/pages/tags')}
          className="text-primary ml-auto inline-flex items-center gap-1 text-xs font-medium hover:underline"
        >
          <TagIcon className="h-3 w-3" aria-hidden />
          Manage tags
        </button>
      </div>

      {/* Bulk action bar */}
      {selected.size > 0 && (
        <BulkActionBar
          count={selected.size}
          tags={tags}
          blockedHomepage={blockedHomepage}
          onClear={() => setSelected(new Set())}
          onPublish={() => runBulkStatus('PUBLISHED')}
          onDraft={() => runBulkStatus('DRAFT')}
          onAddTag={(slug) => runBulkTag(slug, 'add')}
          onRemoveTag={(slug) => runBulkTag(slug, 'remove')}
          onDelete={() => setPendingDelete(true)}
          deletableCount={deletablePages.length}
        />
      )}

      {/* Table card */}
      <div className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
        {/* Horizontal scroll on narrow viewports: the fixed columns keep their
            width and the table scrolls sideways instead of crushing content. */}
        <div className="overflow-x-auto">
          <table className="w-full min-w-[800px] table-fixed" role="table" aria-label="Pages">
            <colgroup>
              <col style={{ width: '40px' }} />
              <col />
              <col style={{ width: '200px' }} />
              <col style={{ width: '170px' }} />
              <col style={{ width: '120px' }} />
              <col style={{ width: '130px' }} />
              <col style={{ width: '80px' }} />
            </colgroup>
            <thead className="border-border bg-muted/40 border-b">
              <tr className="text-muted-foreground text-xs">
                <th scope="col" className="px-3 py-2.5 text-left">
                  <input
                    type="checkbox"
                    aria-label="Select all pages on this page"
                    checked={allOnPageSelected}
                    onChange={(e) => toggleAllOnPage(e.target.checked)}
                    className="border-border accent-primary rounded"
                  />
                </th>
                <th scope="col" aria-sort={ariaSortFor('title')} className="px-3 py-2.5 text-left">
                  <SortHeader
                    label="Title"
                    field="title"
                    current={sortBy}
                    dir={dir}
                    onSort={handleSort}
                  />
                </th>
                <th scope="col" className="px-3 py-2.5 text-left">
                  <span className="text-xs font-medium tracking-wider uppercase">Tags</span>
                </th>
                <th scope="col" aria-sort={ariaSortFor('author')} className="px-3 py-2.5 text-left">
                  <SortHeader
                    label="Author"
                    field="author"
                    current={sortBy}
                    dir={dir}
                    onSort={handleSort}
                  />
                </th>
                <th scope="col" aria-sort={ariaSortFor('status')} className="px-3 py-2.5 text-left">
                  <SortHeader
                    label="Status"
                    field="status"
                    current={sortBy}
                    dir={dir}
                    onSort={handleSort}
                  />
                </th>
                <th
                  scope="col"
                  aria-sort={ariaSortFor('modified')}
                  className="px-3 py-2.5 text-left"
                >
                  <SortHeader
                    label="Last Modified"
                    field="modified"
                    current={sortBy}
                    dir={dir}
                    onSort={handleSort}
                  />
                </th>
                <th scope="col" aria-sort={ariaSortFor('seo')} className="px-3 py-2.5 text-left">
                  <SortHeader
                    label="SEO"
                    field="seo"
                    current={sortBy}
                    dir={dir}
                    onSort={handleSort}
                  />
                </th>
              </tr>
            </thead>
            <tbody>
              {loading && pages.length === 0 && <LoadingRows />}
              {!loading && error && (
                <tr>
                  <td colSpan={7} className="px-3 py-12 text-center">
                    <div className="text-foreground mb-2 text-sm font-medium">{error}</div>
                    <button
                      onClick={() => setRefreshToken((t) => t + 1)}
                      className="text-primary text-sm hover:underline"
                    >
                      Try again
                    </button>
                  </td>
                </tr>
              )}
              {!loading && !error && pages.length === 0 && (
                <EmptyRow
                  filtersActive={filtersAreActive}
                  onClear={clearFilters}
                  onNewPage={() => onNavigate('/pages/new')}
                />
              )}
              {!loading &&
                !error &&
                displayGroups.map((group) => (
                  <GroupBlock
                    key={group.key}
                    showHeader={grouped}
                    label={group.label}
                    color={group.color}
                    count={group.pages.length}
                  >
                    {group.pages.map((p) => (
                      <PageRow
                        key={p.id}
                        page={p}
                        selected={selected.has(p.id)}
                        resolveTag={(slug) => tagBySlug.get(slug)}
                        onSelect={() => toggleOne(p.id)}
                        onOpen={() => onNavigate(`/pages/${p.id}/edit`)}
                      />
                    ))}
                  </GroupBlock>
                ))}
            </tbody>
          </table>
        </div>

        {/* Pagination only for the flat (ungrouped) view. */}
        {!grouped && (
          <PaginationFooter
            page={page}
            totalPages={totalPages}
            total={total}
            pageSize={PAGE_SIZE}
            onPage={setPage}
          />
        )}
      </div>

      <ConfirmDialog
        open={pendingDelete}
        title={`Delete ${deletablePages.length} page${deletablePages.length === 1 ? '' : 's'}?`}
        description={
          blockedHomepage
            ? 'The homepage cannot be deleted and will be skipped. The remaining pages move to trash and can be restored for 30 days.'
            : 'The selected pages move to trash and can be restored for 30 days.'
        }
        confirmLabel="Delete"
        destructive
        onClose={() => setPendingDelete(false)}
        onConfirm={() => {
          void runBulkDelete()
        }}
      />
    </div>
  )
}

// ─── Sub-components ──────────────────────────────────────────────────

function SortHeader({
  label,
  field,
  current,
  dir,
  onSort,
}: {
  label: string
  field: SortBy
  current: SortBy
  dir: Dir
  onSort: (f: SortBy) => void
}) {
  const active = current === field
  return (
    <button
      type="button"
      onClick={() => onSort(field)}
      className={`hover:text-foreground inline-flex items-center gap-1 text-xs font-medium tracking-wider uppercase transition-colors ${
        active ? 'text-foreground' : 'text-muted-foreground'
      }`}
    >
      <span>{label}</span>
      {active &&
        (dir === 'asc' ? (
          <ChevronUp className="h-3 w-3" aria-hidden />
        ) : (
          <ChevronDown className="h-3 w-3" aria-hidden />
        ))}
    </button>
  )
}

function FilterSelect({
  label,
  value,
  onChange,
  options,
}: {
  label: string
  value: string
  onChange: (v: string) => void
  options: { value: string; label: string }[]
}) {
  return (
    <label className="text-foreground inline-flex items-center gap-1.5 text-sm">
      <span className="text-muted-foreground">{label}:</span>
      <select
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className="border-border bg-card text-foreground focus:border-primary focus:ring-primary/30 rounded-lg border px-2.5 py-1.5 text-sm shadow-sm focus:ring-2 focus:outline-none"
      >
        {options.map((o) => (
          <option key={o.value} value={o.value}>
            {o.label}
          </option>
        ))}
      </select>
    </label>
  )
}

function GroupBlock({
  showHeader,
  label,
  color,
  count,
  children,
}: {
  showHeader: boolean
  label: string
  color: string | null
  count: number
  children: React.ReactNode
}) {
  return (
    <>
      {showHeader && (
        <tr className="bg-muted/30 border-border border-b">
          <td colSpan={7} className="px-3 py-2">
            <span className="text-foreground inline-flex items-center gap-2 text-xs font-medium tracking-wider uppercase">
              {color && <span aria-hidden className={`h-2 w-2 rounded-full ${dotFor(color)}`} />}
              {label}
              <span className="text-muted-foreground tabular-nums">{count}</span>
            </span>
          </td>
        </tr>
      )}
      {children}
    </>
  )
}

function dotFor(color: string): string {
  const map: Record<string, string> = {
    purple: 'bg-brand',
    green: 'bg-success',
    orange: 'bg-warning',
    red: 'bg-destructive',
    blue: 'bg-info',
    pink: 'bg-chart-4',
  }
  return map[color] ?? 'bg-muted-foreground'
}

function BulkActionBar({
  count,
  tags,
  blockedHomepage,
  deletableCount,
  onClear,
  onPublish,
  onDraft,
  onAddTag,
  onRemoveTag,
  onDelete,
}: {
  count: number
  tags: PageTag[]
  blockedHomepage: boolean
  deletableCount: number
  onClear: () => void
  onPublish: () => void
  onDraft: () => void
  onAddTag: (slug: string) => void
  onRemoveTag: (slug: string) => void
  onDelete: () => void
}) {
  return (
    <div className="border-primary/30 bg-primary/5 flex flex-wrap items-center gap-2 rounded-lg border px-4 py-2">
      <span className="text-foreground text-sm font-medium">{count} selected</span>
      <div className="bg-border h-4 w-px" aria-hidden />
      <button
        type="button"
        onClick={onPublish}
        className="text-foreground hover:bg-accent rounded-md px-2 py-1 text-sm transition-colors"
      >
        Publish
      </button>
      <button
        type="button"
        onClick={onDraft}
        className="text-foreground hover:bg-accent rounded-md px-2 py-1 text-sm transition-colors"
      >
        Move to draft
      </button>
      <TagMenu label="Add tag" tags={tags} onPick={onAddTag} />
      <TagMenu label="Remove tag" tags={tags} onPick={onRemoveTag} />
      <button
        type="button"
        onClick={onDelete}
        disabled={deletableCount === 0}
        title={blockedHomepage ? 'The homepage is excluded from deletion' : undefined}
        className="text-destructive hover:bg-destructive/10 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40"
      >
        <Trash2 className="h-3.5 w-3.5" aria-hidden />
        Delete
      </button>
      <button
        type="button"
        onClick={onClear}
        className="text-muted-foreground hover:text-foreground ml-auto inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors"
      >
        <X className="h-3.5 w-3.5" aria-hidden />
        Clear
      </button>
    </div>
  )
}

function TagMenu({
  label,
  tags,
  onPick,
}: {
  label: string
  tags: PageTag[]
  onPick: (slug: string) => void
}) {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button
          type="button"
          className="text-foreground hover:bg-accent inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors"
        >
          {label}
          <ChevronDown className="h-3 w-3" aria-hidden />
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal container={adminPortalContainer()}>
        <DropdownMenu.Content
          align="start"
          sideOffset={4}
          className="border-border bg-popover z-50 max-h-64 w-44 overflow-auto rounded-lg border p-1 shadow-md"
        >
          {tags.length === 0 ? (
            <div className="text-muted-foreground px-2 py-1.5 text-xs">No tags yet</div>
          ) : (
            tags.map((t) => (
              <DropdownMenu.Item
                key={t.slug}
                onSelect={() => onPick(t.slug)}
                className="text-foreground focus:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none"
              >
                <span aria-hidden className={`h-2 w-2 rounded-full ${dotFor(t.color ?? 'gray')}`} />
                {t.name}
              </DropdownMenu.Item>
            ))
          )}
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

function PageRow({
  page,
  selected,
  resolveTag,
  onSelect,
  onOpen,
}: {
  page: Page
  selected: boolean
  resolveTag: (slug: string) => { name: string; color: string | null } | undefined
  onSelect: () => void
  onOpen: () => void
}) {
  return (
    <tr
      className={`border-border hover:bg-muted/30 group border-b transition-colors last:border-b-0 ${
        selected ? 'bg-primary/5' : ''
      }`}
    >
      <td className="px-3 py-3" onClick={(e) => e.stopPropagation()}>
        <input
          type="checkbox"
          aria-label={`Select "${page.title}"`}
          checked={selected}
          onChange={onSelect}
          className="border-border accent-primary rounded"
        />
      </td>
      <td className="px-3 py-3">
        <button type="button" onClick={onOpen} className="flex w-full items-center gap-3 text-left">
          <span aria-hidden className="bg-muted block h-9 w-9 shrink-0 rounded-md" />
          <span className="min-w-0">
            <span className="text-foreground group-hover:text-primary block truncate text-sm font-medium">
              {page.title}
            </span>
            <span className="text-muted-foreground block truncate text-xs">{page.path}</span>
          </span>
        </button>
      </td>
      <td className="px-3 py-3">
        <PageTagsCell tagSlugs={page.tags} primaryTagId={page.primaryTagId} resolve={resolveTag} />
      </td>
      <td className="px-3 py-3">
        <div className="flex items-center gap-2">
          <span
            aria-hidden
            className="bg-muted text-muted-foreground inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-medium"
          >
            {page.authorInitials}
          </span>
          <span className="text-foreground truncate text-sm">{page.authorName}</span>
        </div>
      </td>
      <td className="px-3 py-3">
        <PostStatusBadge status={page.status} />
      </td>
      <td className="text-muted-foreground px-3 py-3 text-sm tabular-nums">
        {formatDateShort(page.lastModified)}
      </td>
      <td className="px-3 py-3">
        <SeoScoreIndicator score={page.seoScore} />
      </td>
    </tr>
  )
}

function LoadingRows() {
  return (
    <>
      {Array.from({ length: 8 }).map((_, i) => (
        <tr key={i} className="border-border border-b last:border-b-0">
          {Array.from({ length: 7 }).map((__, j) => (
            <td key={j} className="px-3 py-3">
              <div className="bg-muted h-4 w-full animate-pulse rounded" />
            </td>
          ))}
        </tr>
      ))}
    </>
  )
}

function EmptyRow({
  filtersActive,
  onClear,
  onNewPage,
}: {
  filtersActive: boolean
  onClear: () => void
  onNewPage: () => void
}) {
  return (
    <tr>
      <td colSpan={7} className="px-3 py-16 text-center">
        <div className="text-foreground mb-2 text-sm font-medium">
          {filtersActive ? 'No pages match your filters' : 'No pages yet'}
        </div>
        <div className="text-muted-foreground mb-4 text-sm">
          {filtersActive
            ? 'Try clearing the search, tags, or status filter above.'
            : 'Create your first page to get started.'}
        </div>
        {filtersActive ? (
          <button type="button" onClick={onClear} className="text-primary text-sm hover:underline">
            Clear filters
          </button>
        ) : (
          <button
            type="button"
            onClick={onNewPage}
            className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors"
          >
            <Plus className="h-3.5 w-3.5" aria-hidden />
            New page
          </button>
        )}
      </td>
    </tr>
  )
}

function PaginationFooter({
  page,
  totalPages,
  total,
  pageSize,
  onPage,
}: {
  page: number
  totalPages: number
  total: number
  pageSize: number
  onPage: (p: number) => void
}) {
  const start = total === 0 ? 0 : (page - 1) * pageSize + 1
  const end = Math.min(total, page * pageSize)
  return (
    <div className="border-border flex items-center justify-between border-t px-4 py-3 text-sm">
      <span className="text-muted-foreground tabular-nums">
        {start}–{end} of {total} page{total === 1 ? '' : 's'}
      </span>
      <nav className="flex items-center gap-1" aria-label="Pages pagination">
        <button
          type="button"
          onClick={() => onPage(Math.max(1, page - 1))}
          disabled={page === 1}
          aria-label="Previous page"
          className="border-border bg-background text-foreground hover:bg-accent inline-flex h-8 w-8 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-40"
        >
          <ChevronLeft className="h-4 w-4" aria-hidden />
        </button>
        {pageNumbers(page, totalPages).map((p, idx) =>
          p === '…' ? (
            <span
              key={`gap-${idx}`}
              aria-hidden
              className="text-muted-foreground inline-flex h-8 w-8 items-center justify-center text-sm"
            >
              …
            </span>
          ) : (
            <button
              key={p}
              type="button"
              onClick={() => onPage(p)}
              aria-current={p === page ? 'page' : undefined}
              className={`inline-flex h-8 w-8 items-center justify-center rounded-md text-sm tabular-nums transition-colors ${
                p === page
                  ? 'bg-primary text-primary-foreground'
                  : 'border-border bg-background text-foreground hover:bg-accent border'
              }`}
            >
              {p}
            </button>
          ),
        )}
        <button
          type="button"
          onClick={() => onPage(Math.min(totalPages, page + 1))}
          disabled={page === totalPages}
          aria-label="Next page"
          className="border-border bg-background text-foreground hover:bg-accent inline-flex h-8 w-8 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-40"
        >
          <ChevronRight className="h-4 w-4" aria-hidden />
        </button>
      </nav>
    </div>
  )
}

// ─── Helpers ─────────────────────────────────────────────────────────

function formatDateShort(input: string): string {
  const d = new Date(input)
  if (Number.isNaN(d.getTime())) return '—'
  return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
}

export function pageNumbers(current: number, total: number): Array<number | '…'> {
  if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1)
  const out: Array<number | '…'> = [1]
  const start = Math.max(2, current - 1)
  const end = Math.min(total - 1, current + 1)
  if (start > 2) out.push('…')
  for (let i = start; i <= end; i++) out.push(i)
  if (end < total - 1) out.push('…')
  out.push(total)
  return out
}
