'use client'

/**
 * Dashboard
 * ----------
 * Single-file, lean dashboard implementation. Two API calls only
 * (`/stats` + `/health`, both shared with the rest of the admin), everything
 * else derived client-side via `useMemo`. No charting libraries on this view
 * (the design uses plain progress bars + colored dots), keeping the bundle
 * and TTI cost low.
 *
 * Responsive layout:
 *   <  640px (mobile):  stat cards 2-col, main grid stacks, quick actions scroll-x
 *   640-1023 (tablet):  stat cards 3-col, main grid stacks
 *   >= 1024 (desktop):  stat cards 5-col, main grid is `1fr 320px`
 */

import { useEffect, useMemo, useState } from 'react'
import {
  FileText,
  File as FileIcon,
  Image as ImageIcon,
  ClipboardList,
  Search,
  Plus,
  Upload,
  Globe,
  ExternalLink,
  Activity,
  AlertTriangle,
  Clock,
  Zap,
  Database,
  ChevronRight,
  Loader2,
  TrendingUp,
  TrendingDown,
  Gauge,
  AlertCircle,
  type LucideIcon,
} from 'lucide-react'
import { useApiData } from '../lib/useApiData.js'
import { editPathForDoc } from '../lib/collection-routes.js'

interface DashboardStats {
  totalDocuments: number
  totalMedia: number
  totalUsers: number
  formCount: number
  avgSeoScore: number
  webhookCount: number
  webhookActiveCount: number
  collectionCounts: Record<string, number>
  statusCounts: Record<string, number>
  recentDocuments: {
    id: string
    title: string
    status: string
    collection: string
    updatedAt: string
    author: string
  }[]
}

interface HealthData {
  status: 'healthy' | 'degraded'
  databaseConnected: boolean
  secretConfigured: boolean
  models: Record<string, boolean>
}

interface DeliveryStats {
  hasData: boolean
  requests24h: number
  requestsPrev24h: number
  deltaPercent: number | null
  errorRate: number
  errorCount: number
  avgLatencyMs: number
  p95LatencyMs: number
  maxLatencyMs: number
  webhookCount: number
  webhookActiveCount: number
}

interface ContentHealthData {
  score: number
  label: 'Good' | 'Fair' | 'Poor'
  counts: Record<string, number>
  totalIssues: number
  lastScanAt: string | null
}

interface CollectionMeta {
  slug: string
  type?: string
  labels?: { singular?: string; plural?: string }
}

export interface DashboardProps {
  config?: any
  session?: any
  onNavigate?: (path: string) => void
}

// ─── helpers (kept top-level so they aren't re-created on every render) ──────

function resolveCollections(config: any): CollectionMeta[] {
  if (!config?.collections) return []
  const raw = config.collections
  const list: any[] = Array.isArray(raw) ? raw : Object.values(raw)
  return list
    .filter((c) => !c?.admin?.hidden)
    .map((c) => ({ slug: c.slug, type: c.type, labels: c.labels }))
}

function collectionLabel(col: CollectionMeta, plural = true): string {
  const fallback = col.slug.charAt(0).toUpperCase() + col.slug.slice(1)
  if (plural) return col.labels?.plural ?? fallback
  return col.labels?.singular ?? fallback
}

function relativeTime(dateStr: string): string {
  const then = new Date(dateStr).getTime()
  const diff = Date.now() - then
  if (diff < 60_000) return 'just now'
  const mins = Math.floor(diff / 60_000)
  if (mins < 60) return `${mins}m ago`
  const hours = Math.floor(mins / 60)
  if (hours < 24) return `${hours}h ago`
  const days = Math.floor(hours / 24)
  if (days === 1) return 'Yesterday'
  if (days < 30) return `${days}d ago`
  return new Date(dateStr).toLocaleDateString()
}

function timeOfDayGreeting(): string {
  const h = new Date().getHours()
  if (h < 12) return 'Good morning'
  if (h < 17) return 'Good afternoon'
  return 'Good evening'
}

/**
 * Return the user's first name for the greeting. Falls back to the
 * email local-part, then a generic "there". Strips role-style names
 * like "Admin" / "Administrator" / "Editor" — a literal
 * `Good morning, Admin` reads as bot-output, not a person.
 */
function firstNameForGreeting(session: any): string {
  const raw = (session?.user?.name ?? session?.name ?? '').trim()
  const email = session?.user?.email ?? session?.email
  const generic = new Set(['admin', 'administrator', 'editor', 'user', 'root'])
  if (raw) {
    const first = raw.split(/\s+/)[0] ?? raw
    if (!generic.has(first.toLowerCase())) return first
  }
  if (typeof email === 'string' && email.length > 0) {
    const local = email.split('@')[0] ?? email
    return local.charAt(0).toUpperCase() + local.slice(1)
  }
  return 'there'
}

function todayDateString(): string {
  return new Date().toLocaleDateString('en-US', {
    weekday: 'long',
    month: 'long',
    day: 'numeric',
    year: 'numeric',
  })
}

// Deterministic colour per author so avatars stay stable across renders
// without storing anything in state or hitting the server.
const AVATAR_COLORS = [
  '#7C3AED', // purple
  '#E11D48', // rose
  '#059669', // emerald
  '#0891B2', // cyan
  '#D97706', // amber
  '#4F46E5', // indigo
  '#DC2626', // red
] as const

function hashString(s: string): number {
  let h = 0
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
  return Math.abs(h)
}

function authorAvatar(name: string | null | undefined): { color: string; initials: string } {
  const safe = (name ?? '').trim() || 'User'
  const parts = safe.split(/\s+/).filter(Boolean)
  const initials =
    parts.length >= 2
      ? `${parts[0]![0]}${parts[parts.length - 1]![0]}`.toUpperCase()
      : safe.slice(0, 2).toUpperCase()
  const color = AVATAR_COLORS[hashString(safe) % AVATAR_COLORS.length]!
  return { color, initials }
}

/**
 * Semantic state styling uses theme tokens (`success` / `warning` / `info`
 * / `brand`) so the dashboard tracks the design system in both light and
 * dark mode. Two deliberate exceptions remain as raw values:
 *   - Error states keep `text-red-*` / `bg-red-*`. The `--destructive`
 *     token's dark value is intentionally dark (low contrast as TEXT on a
 *     dark card), so tokenizing error text here would regress contrast.
 *     Promoting error to a token is a separate, admin-wide change.
 *   - `AVATAR_COLORS` are dynamic per-author hashes (allowed: truly
 *     dynamic values, applied via inline style).
 */
function statusBadge(status: string): {
  label: string
  cls: string
} {
  switch (status) {
    case 'PUBLISHED':
      return { label: 'Published', cls: 'bg-success/10 text-success' }
    case 'DRAFT':
      return { label: 'Draft', cls: 'bg-muted text-muted-foreground' }
    case 'SCHEDULED':
      return { label: 'Scheduled', cls: 'bg-warning/10 text-warning' }
    case 'IN_REVIEW':
      return { label: 'In Review', cls: 'bg-info/10 text-info' }
    default:
      return { label: status, cls: 'bg-muted text-muted-foreground' }
  }
}

interface StatCardData {
  label: string
  value: string
  unit?: string
  hint?: string
  hintUp?: boolean
  icon: LucideIcon
  iconBg: string
  iconColor: string
  href?: string
}

// ─── Dashboard ───────────────────────────────────────────────────────────────

export function Dashboard({ config, session, onNavigate }: DashboardProps) {
  const nav = (path: string) => onNavigate?.(path)
  // Cache responses for 30s so navigating away and back doesn't re-fire
  // all four dashboard requests on every visit. First load is unaffected
  // (cold cache) and each request still streams independently; `refetch`
  // (the Retry button) bypasses the cache.
  const DASH_CACHE_MS = 30_000
  const {
    data: stats,
    loading,
    error,
    exhausted,
    refetch,
  } = useApiData<DashboardStats>('/stats', null, {
    cacheMs: DASH_CACHE_MS,
  })
  const { data: health } = useApiData<HealthData>('/health', null, { cacheMs: DASH_CACHE_MS })
  const { data: delivery, loading: deliveryLoading } = useApiData<DeliveryStats>(
    '/stats/api-delivery',
    null,
    { cacheMs: DASH_CACHE_MS },
  )
  const { data: contentHealthData, loading: contentHealthLoading } = useApiData<ContentHealthData>(
    '/stats/content-health',
    null,
    { cacheMs: DASH_CACHE_MS },
  )

  // A slow ticker (60s) so the greeting, calendar date, and "x minutes
  // ago" relative times stay correct on a dashboard left open for hours
  // — without it, a tab opened at 11:58am would read "Good morning" all
  // afternoon and show stale activity timestamps.
  const [now, setNow] = useState(() => Date.now())
  useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 60_000)
    return () => clearInterval(id)
  }, [])

  const greeting = useMemo(() => timeOfDayGreeting(), [now])
  const dateStr = useMemo(() => todayDateString(), [now])
  const userName = useMemo(() => firstNameForGreeting(session), [session])

  const collections = useMemo(() => resolveCollections(config), [config])

  // ── Stat cards ──────────────────────────────────────────────────────────
  const statCards: StatCardData[] = useMemo(() => {
    const counts = stats?.collectionCounts ?? {}

    // Prefer two real collections (the user's primary content types) over
    // hard-coded Posts/Pages so the dashboard adapts to admin-managed types.
    const primary: CollectionMeta[] = []
    const posts = collections.find((c) => c.slug === 'posts' || c.type === 'post')
    const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page')
    if (posts) primary.push(posts)
    if (pages) primary.push(pages)
    if (primary.length < 2) {
      for (const c of collections) {
        if (primary.length >= 2) break
        if (!primary.includes(c)) primary.push(c)
      }
    }

    const cards: StatCardData[] = []

    if (primary[0]) {
      cards.push({
        label: collectionLabel(primary[0]),
        value: String(counts[primary[0].slug] ?? 0),
        icon: primary[0].type === 'page' ? FileIcon : FileText,
        iconBg: 'bg-chart-1/15',
        iconColor: 'text-chart-1',
        href: `/${primary[0].slug}`,
      })
    } else {
      cards.push({
        label: 'Documents',
        value: String(stats?.totalDocuments ?? 0),
        icon: FileText,
        iconBg: 'bg-chart-1/15',
        iconColor: 'text-chart-1',
      })
    }

    if (primary[1]) {
      cards.push({
        label: collectionLabel(primary[1]),
        value: String(counts[primary[1].slug] ?? 0),
        icon: primary[1].type === 'page' ? FileIcon : FileText,
        iconBg: 'bg-chart-2/15',
        iconColor: 'text-chart-2',
        href: `/${primary[1].slug}`,
      })
    }

    cards.push({
      label: 'Media',
      value: String(stats?.totalMedia ?? 0),
      hint:
        stats && stats.totalMedia > 0
          ? `${stats.totalMedia} file${stats.totalMedia === 1 ? '' : 's'}`
          : undefined,
      icon: ImageIcon,
      iconBg: 'bg-chart-3/15',
      iconColor: 'text-chart-3',
      href: '/media',
    })

    // `formCount` in /stats is the *submission* count; the number of
    // forms is the count of `collection: forms` documents.
    const formsCount = counts['forms'] ?? 0
    const submissions = stats?.formCount ?? 0
    cards.push({
      label: 'Forms',
      value: String(formsCount),
      hint: submissions > 0 ? `${submissions} response${submissions === 1 ? '' : 's'}` : undefined,
      icon: ClipboardList,
      iconBg: 'bg-chart-4/15',
      iconColor: 'text-chart-4',
      href: '/forms',
    })

    const seo = stats?.avgSeoScore ?? 0
    cards.push({
      label: 'SEO Score',
      value: seo > 0 ? String(seo) : '—',
      unit: seo > 0 ? '/100' : undefined,
      hint: seo > 0 ? (seo >= 70 ? 'Good' : seo >= 40 ? 'Fair' : 'Needs work') : 'No content yet',
      hintUp: seo >= 70,
      icon: Search,
      iconBg: 'bg-chart-5/15',
      iconColor: 'text-chart-5',
      href: '/seo',
    })

    return cards
  }, [stats, collections])

  // ── Quick actions ───────────────────────────────────────────────────────
  // 5-button strip matching the reference dashboard. The "+ New Post" entry
  // intentionally duplicates the top-bar primary action: the top bar
  // surface is for chrome-level navigation, the quick-action row is the
  // dashboard's content-creation toolbar. Order mirrors the natural
  // authoring flow (post → page → media → form → integration).
  const quickActions = useMemo(() => {
    const postTypes = collections.filter((c) => c.type === 'post')
    const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page')
    const pagesSlug = pages?.slug ?? 'pages'
    // "New Post": one Post Type → straight to its editor; several →
    // the picker; none → the Post Type setup instructions (avoids a
    // broken slug-based new URL).
    const newPostPath =
      postTypes.length === 1
        ? `/${postTypes[0]!.slug}/new`
        : postTypes.length > 1
          ? '/posts/new'
          : '/posts/types/new'
    const items: { label: string; icon: LucideIcon; onClick: () => void }[] = [
      {
        label: 'New Post',
        icon: Plus,
        onClick: () => nav(newPostPath),
      },
      { label: 'New Page', icon: Plus, onClick: () => nav(`/${pagesSlug}/new`) },
      { label: 'Upload Media', icon: Upload, onClick: () => nav('/media') },
      { label: 'New Form', icon: Plus, onClick: () => nav('/forms') },
      { label: 'View API', icon: Globe, onClick: () => nav('/settings?tab=api-keys') },
    ]
    return items
  }, [collections, onNavigate]) // eslint-disable-line react-hooks/exhaustive-deps

  // ── Recent activity ─────────────────────────────────────────────────────
  const [activityLimit, setActivityLimit] = useState(8)
  const activity = useMemo(() => {
    const docs = stats?.recentDocuments ?? []
    return docs.slice(0, activityLimit).map((d) => {
      const col = collections.find((c) => c.slug === d.collection)
      return {
        ...d,
        typeLabel: col ? collectionLabel(col, false) : d.collection,
        relTime: relativeTime(d.updatedAt),
        avatar: authorAvatar(d.author),
        statusInfo: statusBadge(d.status),
        editPath: editPathForDoc(col, d.collection, d.id),
      }
    })
    // `now` keeps the relative timestamps fresh on a long-open tab.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [stats, collections, activityLimit, now])

  // ── Publishing queue (scheduled docs only) ──────────────────────────────
  const publishQueue = useMemo(() => {
    const docs = stats?.recentDocuments ?? []
    return docs
      .filter((d) => d.status === 'SCHEDULED')
      .slice(0, 5)
      .map((d) => {
        const col = collections.find((c) => c.slug === d.collection)
        return {
          id: d.id,
          collection: d.collection,
          title: d.title || 'Untitled',
          type: col ? collectionLabel(col, false) : d.collection,
          date: relativeTime(d.updatedAt),
          author: d.author,
          editPath: editPathForDoc(col, d.collection, d.id),
        }
      })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [stats, collections, now])

  // ── Content health (sourced from /stats/content-health) ─────────────────
  // Persisted by the nightly content-health cron. The four issue rows
  // map 1:1 to ContentIssueType in the schema; the labels are
  // user-facing and intentionally read like the reference dashboard.
  const contentHealth = useMemo(() => {
    const counts = contentHealthData?.counts ?? {}
    const score = contentHealthData?.score ?? 100
    const label = contentHealthData?.label ?? 'Good'
    const tone: 'ok' | 'warn' | 'err' | 'muted' =
      score >= 70 ? 'ok' : score >= 40 ? 'warn' : score > 0 ? 'err' : 'muted'

    // Issue list — only render rows that have non-zero counts so the
    // card stays compact when the corpus is healthy. The empty state
    // below picks up when all four are zero.
    const issues: { kind: string; label: string; count: number; tone: 'warn' | 'err' }[] = []
    const missingMeta = counts['MISSING_META_DESCRIPTION'] ?? 0
    const brokenLinks = counts['BROKEN_INTERNAL_LINK'] ?? 0
    const missingAlt = counts['MISSING_ALT_TEXT'] ?? 0
    const outdated = counts['OUTDATED_CONTENT'] ?? 0
    if (missingMeta > 0)
      issues.push({
        kind: 'MISSING_META_DESCRIPTION',
        label: 'Missing meta descriptions',
        count: missingMeta,
        tone: 'warn',
      })
    if (brokenLinks > 0)
      issues.push({
        kind: 'BROKEN_INTERNAL_LINK',
        label: 'Broken internal links',
        count: brokenLinks,
        tone: 'err',
      })
    if (missingAlt > 0)
      issues.push({
        kind: 'MISSING_ALT_TEXT',
        label: 'Missing alt text',
        count: missingAlt,
        tone: 'warn',
      })
    if (outdated > 0)
      issues.push({
        kind: 'OUTDATED_CONTENT',
        label: 'Outdated content (90+ days)',
        count: outdated,
        tone: 'warn',
      })

    return {
      score,
      label,
      tone,
      issues,
      hasData: contentHealthData !== null,
      loading: contentHealthLoading,
    } as const
  }, [contentHealthData, contentHealthLoading])

  const totalIssues = contentHealth.issues.reduce((s, i) => s + i.count, 0)

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

  if (loading && !stats) {
    return (
      <div className="flex h-64 items-center justify-center p-4 sm:p-6">
        <Loader2 className="text-brand h-6 w-6 animate-spin" />
      </div>
    )
  }

  return (
    <div className="w-full space-y-5 p-4 sm:p-6 lg:px-8">
      {/* Health banners ────────────────────────────────────────────── */}
      {health && health.status === 'degraded' && (
        <div className="border-info/30 bg-info/10 flex items-start gap-3 rounded-lg border p-3">
          <Database className="text-info mt-0.5 h-5 w-5 shrink-0" />
          <div className="min-w-0 flex-1">
            <span className="text-info text-sm font-medium">Database setup required</span>
            <p className="text-muted-foreground mt-0.5 text-xs">
              {!health.databaseConnected
                ? 'Cannot connect to the database. Check your DATABASE_URL environment variable.'
                : !health.secretConfigured
                  ? 'CMS secret not configured. Set CMS_SECRET or CMS_SESSION_SECRET (min 32 characters).'
                  : `Some CMS models are missing: ${Object.entries(health.models)
                      .filter(([, v]) => !v)
                      .map(([k]) => k)
                      .join(', ')}. Run your database migrations.`}
            </p>
          </div>
        </div>
      )}

      {error && exhausted && (
        <div className="border-warning/30 bg-warning/10 flex items-center gap-3 rounded-lg border p-3">
          <AlertTriangle className="text-warning h-5 w-5 shrink-0" />
          <span className="text-warning flex-1 text-sm">
            Some dashboard data may be unavailable.
          </span>
          <button
            onClick={refetch}
            className="border-warning/40 text-warning hover:bg-warning/10 rounded-lg border px-3 py-1 text-sm transition-colors"
          >
            Retry
          </button>
        </div>
      )}

      {/* Hero greeting ─────────────────────────────────────────────────
          Primary "+ New Post" and "View Site" actions live in the top bar
          (Header.tsx page-action slot), not here, so the hero is purely
          informational and the dashboard's chrome-level affordances stay
          consistent across navigation. The on-page secondary action row
          below covers the same creation paths plus authoring shortcuts. */}
      <div className="flex flex-col gap-1 sm:gap-0.5">
        <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
          {greeting}, {userName} <span aria-hidden>👋</span>
        </h1>
        <p className="text-muted-foreground text-sm">
          {dateStr}
          {totalIssues > 0 && (
            <span className="hidden sm:inline">
              {' · '}
              {totalIssues} content item{totalIssues === 1 ? '' : 's'} need attention
            </span>
          )}
        </p>
      </div>

      {/* Quick actions ──────────────────────────────────────────────── */}
      <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 [scrollbar-width:thin]">
        {quickActions.map((a) => (
          <button
            key={a.label}
            onClick={a.onClick}
            className="border-border bg-card hover:border-brand/50 hover:bg-brand/10 hover:text-brand inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-3.5 py-2 text-sm shadow-sm transition-colors"
          >
            <a.icon className="h-3.5 w-3.5" />
            {a.label}
          </button>
        ))}
      </div>

      {/* Stat cards ─────────────────────────────────────────────────── */}
      <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
        {statCards.map((card) => {
          const Inner = (
            <>
              <div
                className={`mb-3 flex h-8 w-8 items-center justify-center rounded-lg ${card.iconBg}`}
              >
                <card.icon className={`h-4 w-4 ${card.iconColor}`} />
              </div>
              <div className="flex items-baseline gap-1">
                <span className="text-foreground text-2xl leading-none font-medium tracking-tight">
                  {card.value}
                </span>
                {card.unit && (
                  <span className="text-muted-foreground text-sm font-medium">{card.unit}</span>
                )}
              </div>
              <p className="text-muted-foreground mt-1 text-xs">{card.label}</p>
              {card.hint && (
                <p
                  className={`mt-1.5 text-[11px] ${card.hintUp ? 'text-success' : 'text-muted-foreground'}`}
                >
                  {card.hint}
                </p>
              )}
            </>
          )

          return card.href ? (
            <button
              key={card.label}
              type="button"
              onClick={() => nav(card.href!)}
              className="bg-card border-border rounded-xl border p-4 text-left shadow-sm transition-shadow hover:shadow-md"
            >
              {Inner}
            </button>
          ) : (
            <div key={card.label} className="bg-card border-border rounded-xl border p-4 shadow-sm">
              {Inner}
            </div>
          )
        })}
      </div>

      {/* Main grid ──────────────────────────────────────────────────── */}
      <div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
        {/* Recent Activity ───────────────────────────────────────── */}
        <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
          <header className="border-border flex items-center justify-between border-b px-4 py-3">
            <div className="min-w-0">
              <h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
              <p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
            </div>
            {(stats?.recentDocuments?.length ?? 0) > 8 && (
              <button
                className="text-brand text-xs font-medium hover:underline"
                onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
              >
                {activityLimit >= 20 ? 'Show less' : 'Show more'}
              </button>
            )}
          </header>
          {activity.length === 0 ? (
            <EmptyState
              icon={Activity}
              title="No activity yet"
              subtitle="Content changes will appear here as your team works."
            />
          ) : (
            <ul role="list" className="divide-border divide-y">
              {activity.map((it) => (
                <li key={it.id}>
                  <button
                    type="button"
                    onClick={() => nav(it.editPath)}
                    className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
                  >
                    <div className="flex min-w-0 items-start gap-3">
                      <div
                        className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-medium text-white"
                        style={{ background: it.avatar.color }}
                        aria-hidden
                      >
                        {it.avatar.initials}
                      </div>
                      <div className="min-w-0 flex-1">
                        <p className="text-foreground truncate text-sm leading-snug">
                          <span className="font-medium">
                            &ldquo;{it.title || 'Untitled'}&rdquo;
                          </span>{' '}
                          <span className="text-muted-foreground">— {it.typeLabel}</span>
                        </p>
                        <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
                          <span
                            className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
                          >
                            {it.statusInfo.label}
                          </span>
                          <span className="max-w-[120px] truncate">{it.author}</span>
                          <span aria-hidden>·</span>
                          <span>{it.relTime}</span>
                        </div>
                      </div>
                    </div>
                  </button>
                </li>
              ))}
            </ul>
          )}
        </section>

        {/* Right column ─────────────────────────────────────────── */}
        <aside className="flex min-w-0 flex-col gap-4">
          {/* Publishing Queue */}
          <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
            <header className="border-border flex items-center justify-between border-b px-4 py-3">
              <div className="min-w-0">
                <h2 className="text-foreground text-sm font-medium">Publishing Queue</h2>
                <p className="text-muted-foreground mt-0.5 text-xs">
                  {publishQueue.length} item{publishQueue.length === 1 ? '' : 's'} scheduled
                </p>
              </div>
              <button
                className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
                disabled={publishQueue.length === 0}
                onClick={() => {
                  // We don't have a dedicated "scheduled" admin page, so deep-
                  // link to the first scheduled item — that's where authors
                  // typically need to land to reschedule or cancel.
                  const first = publishQueue[0]
                  if (first) nav(first.editPath)
                }}
              >
                Manage
              </button>
            </header>
            {publishQueue.length === 0 ? (
              <EmptyState
                icon={Clock}
                title="No scheduled content"
                subtitle="Scheduled posts and pages appear here."
                compact
              />
            ) : (
              <ul role="list" className="divide-border divide-y">
                {publishQueue.map((q) => (
                  <li key={q.id}>
                    <button
                      type="button"
                      onClick={() => nav(q.editPath)}
                      className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 text-left transition-colors"
                    >
                      <span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
                      <div className="min-w-0 flex-1">
                        <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
                        <p className="text-muted-foreground truncate text-[11px]">
                          {q.date} · {q.author}
                        </p>
                      </div>
                      <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
                        {q.type}
                      </span>
                    </button>
                  </li>
                ))}
              </ul>
            )}
          </section>

          {/* Content Health */}
          <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
            <header className="border-border flex items-center justify-between border-b px-4 py-3">
              <div className="min-w-0">
                <h2 className="text-foreground text-sm font-medium">Content Health</h2>
                <p className="text-muted-foreground mt-0.5 text-xs">SEO &amp; quality issues</p>
              </div>
              <button
                className="text-brand text-xs font-medium hover:underline"
                onClick={() => nav('/seo')}
              >
                Fix issues
              </button>
            </header>
            <div className="px-4 pt-3.5 pb-2.5">
              <div className="mb-1.5 flex items-baseline justify-between">
                <p className="text-foreground text-2xl leading-none font-medium tracking-tight">
                  {contentHealth.score > 0 ? contentHealth.score : '—'}
                  {contentHealth.score > 0 && (
                    <span className="text-muted-foreground ml-0.5 text-sm font-normal">/100</span>
                  )}
                </p>
                <span
                  className={`text-xs font-medium ${
                    contentHealth.tone === 'ok'
                      ? 'text-success'
                      : contentHealth.tone === 'warn'
                        ? 'text-warning'
                        : contentHealth.tone === 'err'
                          ? 'text-destructive dark:text-red-400'
                          : 'text-muted-foreground'
                  }`}
                >
                  {contentHealth.label}
                </span>
              </div>
              <div className="bg-muted h-1.5 overflow-hidden rounded-full">
                <div
                  className={`h-full rounded-full transition-all ${
                    contentHealth.tone === 'ok'
                      ? 'bg-success'
                      : contentHealth.tone === 'warn'
                        ? 'bg-warning'
                        : contentHealth.tone === 'err'
                          ? 'bg-destructive'
                          : 'bg-muted-foreground/40'
                  }`}
                  style={{ width: `${Math.max(0, Math.min(100, contentHealth.score))}%` }}
                />
              </div>
            </div>
            {contentHealth.issues.length === 0 ? (
              <div className="text-muted-foreground px-4 pt-1 pb-4 text-xs">
                No outstanding issues. Nice work.
              </div>
            ) : (
              <ul role="list" className="divide-border divide-y">
                {contentHealth.issues.map((iss, i) => (
                  <li key={i}>
                    <button
                      type="button"
                      onClick={() => nav('/seo')}
                      className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full items-center gap-2.5 px-4 py-2 text-left transition-colors"
                    >
                      <span
                        className={`h-1.5 w-1.5 shrink-0 rounded-full ${
                          iss.tone === 'err'
                            ? 'bg-destructive'
                            : iss.tone === 'warn'
                              ? 'bg-warning'
                              : 'bg-muted-foreground'
                        }`}
                        aria-hidden
                      />
                      <span className="text-muted-foreground flex-1 truncate text-xs">
                        {iss.label}
                      </span>
                      <span
                        className={`shrink-0 text-sm font-medium ${
                          iss.tone === 'err'
                            ? 'text-destructive dark:text-red-400'
                            : iss.tone === 'warn'
                              ? 'text-warning'
                              : 'text-muted-foreground'
                        }`}
                      >
                        {iss.count}
                      </span>
                      <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
                    </button>
                  </li>
                ))}
              </ul>
            )}
          </section>
        </aside>
      </div>

      {/* Delivery API ──────────────────────────────────────────────────
          Headless content delivery metrics aggregated over the last
          24h from `actuate_api_request_metrics`. Populated in real-time
          by the request logger in handler-factory.ts — no cron
          required. Empty-state messaging differentiates "we haven't
          collected enough data yet" from "we have data and it shows
          0" so editors don't think the dashboard is broken on a
          quiet weekend. */}
      <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
        <header className="border-border flex items-center justify-between border-b px-4 py-3">
          <div className="min-w-0">
            <h2 className="text-foreground text-sm font-medium">Delivery API</h2>
            <p className="text-muted-foreground mt-0.5 text-xs">
              Headless content delivery · last 24 hours
            </p>
          </div>
          <button
            className="text-brand inline-flex items-center gap-1 text-xs font-medium hover:underline"
            onClick={() => nav('/settings?tab=api-keys')}
          >
            API docs <ExternalLink className="h-3 w-3" />
          </button>
        </header>
        <DeliveryGrid delivery={delivery} loading={deliveryLoading} />
      </section>
    </div>
  )
}

// ─── Delivery API grid ──────────────────────────────────────────────────────

function DeliveryGrid({ delivery, loading }: { delivery: DeliveryStats | null; loading: boolean }) {
  if (loading && !delivery) {
    return (
      <div
        className="divide-border grid grid-cols-2 divide-x divide-y lg:grid-cols-4 lg:divide-y-0"
        aria-busy
      >
        {[0, 1, 2, 3].map((i) => (
          <div key={i} className="px-4 py-3.5">
            <div className="bg-muted mb-2 h-3 w-24 rounded" />
            <div className="bg-muted mb-1.5 h-7 w-20 rounded" />
            <div className="bg-muted h-3 w-28 rounded" />
          </div>
        ))}
      </div>
    )
  }

  const hasData = delivery?.hasData ?? false
  const requests = delivery?.requests24h ?? 0
  const delta: number | null = delivery?.deltaPercent ?? null
  const errorRate = delivery?.errorRate ?? 0
  const avgLatency = delivery?.avgLatencyMs ?? 0
  const p95Latency = delivery?.p95LatencyMs ?? 0
  const webhookTotal = delivery?.webhookCount ?? 0
  const webhookActive = delivery?.webhookActiveCount ?? 0

  // Pretty-print large numbers (14_823 → "14.8K") so the tile stays
  // readable at the dashboard's compact density.
  const requestsLabel = formatLargeNumber(requests)
  const errorRatePct = (errorRate * 100).toFixed(2)

  return (
    <div className="divide-border grid grid-cols-2 divide-x divide-y lg:grid-cols-4 lg:divide-y-0">
      <DeliveryTile
        icon={Activity}
        label="API Requests (24h)"
        value={hasData ? requestsLabel : '—'}
        sub={
          !hasData
            ? 'no traffic recorded yet'
            : delta === null
              ? 'no prior baseline'
              : `${delta >= 0 ? '+' : ''}${delta.toFixed(1)}% vs yesterday`
        }
        subIcon={
          hasData && typeof delta === 'number'
            ? delta >= 0
              ? TrendingUp
              : TrendingDown
            : undefined
        }
        tone={hasData ? (delta && delta >= 0 ? 'ok' : 'muted') : 'muted'}
      />
      <DeliveryTile
        icon={AlertCircle}
        label="Error Rate"
        value={hasData ? `${errorRatePct}%` : '—'}
        sub={
          !hasData
            ? 'no traffic recorded yet'
            : errorRate < 0.005
              ? 'healthy'
              : errorRate < 0.05
                ? 'elevated'
                : 'investigate'
        }
        tone={!hasData ? 'muted' : errorRate < 0.005 ? 'ok' : errorRate < 0.05 ? 'warn' : 'err'}
      />
      <DeliveryTile
        icon={Gauge}
        label="Avg Response Time"
        value={hasData ? `${Math.round(avgLatency)}ms` : '—'}
        sub={hasData ? `p95: ${p95Latency}ms` : 'no traffic recorded yet'}
        tone={!hasData ? 'muted' : avgLatency < 200 ? 'ok' : avgLatency < 600 ? 'warn' : 'err'}
      />
      <DeliveryTile
        icon={Zap}
        label="Active Webhooks"
        value={`${webhookActive} / ${webhookTotal}`}
        sub={
          webhookTotal === 0
            ? 'none configured'
            : webhookActive === webhookTotal
              ? 'All active'
              : `${webhookTotal - webhookActive} paused`
        }
        tone={webhookTotal === 0 ? 'muted' : webhookActive === webhookTotal ? 'ok' : 'warn'}
      />
    </div>
  )
}

function formatLargeNumber(n: number): string {
  if (n < 1000) return n.toLocaleString()
  if (n < 10_000) return `${(n / 1000).toFixed(1)}K`
  if (n < 1_000_000) return `${(n / 1000).toFixed(0)}K`
  return `${(n / 1_000_000).toFixed(1)}M`
}

// ─── Sub-components (kept in-file: smaller bundle, no extra module hops) ────

function EmptyState({
  icon: Icon,
  title,
  subtitle,
  compact = false,
}: {
  icon: LucideIcon
  title: string
  subtitle: string
  compact?: boolean
}) {
  return (
    <div
      className={`flex flex-col items-center justify-center gap-1.5 text-center ${compact ? 'px-4 py-6' : 'px-6 py-10'}`}
    >
      <Icon className="text-muted-foreground/50 mb-1 h-7 w-7" aria-hidden />
      <p className="text-foreground text-sm font-medium">{title}</p>
      <p className="text-muted-foreground max-w-xs text-xs">{subtitle}</p>
    </div>
  )
}

function DeliveryTile({
  icon: Icon,
  label,
  value,
  sub,
  subIcon: SubIcon,
  tone = 'muted',
}: {
  icon: LucideIcon
  label: string
  value: string
  sub?: string
  subIcon?: LucideIcon
  tone?: 'ok' | 'warn' | 'err' | 'muted'
}) {
  const subTone =
    tone === 'ok'
      ? 'text-success'
      : tone === 'warn'
        ? 'text-warning'
        : tone === 'err'
          ? 'text-destructive dark:text-red-400'
          : 'text-muted-foreground'
  return (
    <div className="px-4 py-3.5">
      <div className="text-muted-foreground mb-1 flex items-center gap-1.5">
        <Icon className="h-3.5 w-3.5" aria-hidden />
        <span className="text-[11px] font-medium">{label}</span>
      </div>
      <p className="text-foreground text-xl leading-tight font-medium tracking-tight">{value}</p>
      {sub && (
        <p className={`mt-0.5 inline-flex items-center gap-1 text-[11px] ${subTone}`}>
          {SubIcon && <SubIcon className="h-3 w-3 shrink-0" aria-hidden />}
          {sub}
        </p>
      )}
    </div>
  )
}
