'use client'

import { useEffect, useRef, useState, type ReactNode } from 'react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { adminPortalContainer } from '../lib/portal-container.js'
import { useTheme } from '../components/ThemeProvider.js'
import { fetchFormsSidebarCounts, type FormsSidebarCount } from '../lib/forms-service.js'
import { onFormsChanged } from '../lib/forms-events.js'
import {
  LayoutDashboard,
  FileText,
  File,
  Image,
  Settings,
  ChevronLeft,
  ChevronDown,
  ClipboardList,
  Inbox,
  Search as SearchIcon,
  Briefcase,
  FolderOpen,
  BookOpen,
  HelpCircle,
  Newspaper,
  PanelTop,
  PanelBottom,
  Layers,
  LayoutGrid,
  Code2,
  Library,
  ListTree,
  Menu as MenuIcon,
  PanelLeftClose,
  PanelLeftOpen,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { ActuateBrandLogo } from '../assets/actuate-logo.js'
import { LogoPlaceholder } from '../components/LogoPlaceholder.js'

/**
 * Compact mark used in the collapsed sidebar — just the "C" symbol from the
 * full lockup, drawn as its own simple SVG so it stays crisp at 32×32.
 */
function ActuateMark({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 40 44"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className={className}
      aria-hidden="true"
    >
      <polygon points="20,2 6,18 12,18 20,8 28,18 34,18" fill="#F05E65" />
      <rect x="11" y="20" width="4" height="22" rx="1" fill="#F05E65" />
      <rect x="18" y="20" width="4" height="22" rx="1" fill="#F05E65" />
      <rect x="25" y="20" width="4" height="22" rx="1" fill="#F05E65" />
    </svg>
  )
}

function ActuateWordmark({ className }: { className?: string }) {
  return <ActuateBrandLogo className={className} />
}

const ICON_MAP: Record<string, LucideIcon> = {
  file: File,
  'file-text': FileText,
  briefcase: Briefcase,
  folder: FolderOpen,
  book: BookOpen,
  help: HelpCircle,
  newspaper: Newspaper,
  navigation: ListTree,
  menu: MenuIcon,
  'list-tree': ListTree,
  PanelTop: PanelTop,
  PanelBottom: PanelBottom,
  Layers: Layers,
}

/**
 * Sidebar brand lockup.
 *
 * Resolution order follows the Appearance spec, sourcing from the live
 * appearance context (DB-backed brand, cached in localStorage) and falling
 * back to legacy `config.admin.branding`, then a neutral logo placeholder for
 * branded-but-logo-less installs, then the bundled Actuate mark/wordmark for
 * un-branded installs. The placeholder replaces the old initials badge, which
 * read as a user avatar (the real account avatar already sits top-right).
 * Dark-mode uses the dark logo when one is configured. Logos are never
 * stretched/cropped (object-contain).
 */
function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean }) {
  const { adminBrand, resolvedTheme } = useTheme()
  const branding = config?.admin?.branding
  const configLogo = branding?.logo
  const brandName = branding?.name
  const initials = adminBrand.initials
  const hasInitials = Boolean(initials) && initials !== 'A'

  // Track image URLs that fail to load (broken/expired/deleted asset) so the
  // resolution falls through to the next option instead of showing the browser's
  // broken-image icon in the sidebar header.
  const [failedSrcs, setFailedSrcs] = useState<Set<string>>(new Set())
  const markFailed = (src: string) =>
    setFailedSrcs((prev) => (prev.has(src) ? prev : new Set(prev).add(src)))
  const usable = (url: string | null | undefined): string | null =>
    url && !failedSrcs.has(url) ? url : null
  const onErr = (src: string) => () => markFailed(src)

  // The admin brand is ALWAYS the logo — the favicon is only for the browser tab
  // and is never used here (see resolveAdminBrand). Prefer the dark logo on dark
  // backgrounds when supplied, falling back to the primary if the dark asset is
  // missing or broken.
  const darkPref = resolvedTheme === 'dark' ? adminBrand.darkUrl : null
  const primary = usable(darkPref) ?? usable(adminBrand.primaryUrl)
  const mark = usable(adminBrand.markUrl)
  const cfg = usable(configLogo)
  const alt = brandName ?? 'Admin'

  // Wide horizontal logos must scale down to fit the narrow rail / sidebar
  // header instead of overflowing — height-capped, width auto, never wider than
  // the available space.
  const logoClass = 'h-8 w-auto max-w-full object-contain'

  if (collapsed) {
    // Collapsed rail prefers a dedicated square mark; otherwise it shows the
    // logo (scaled to fit) — never the favicon.
    if (mark)
      return <img src={mark} alt={alt} className="h-8 w-8 object-contain" onError={onErr(mark)} />
    if (primary)
      return <img src={primary} alt={alt} className={logoClass} onError={onErr(primary)} />
    if (cfg) return <img src={cfg} alt={alt} className={logoClass} onError={onErr(cfg)} />
    if (hasInitials) return <LogoPlaceholder tone="sidebar" />
    return <ActuateMark className="h-8 w-8" />
  }

  if (primary) {
    return (
      <div className="flex min-w-0 items-center gap-2.5">
        <img src={primary} alt={alt} className={logoClass} onError={onErr(primary)} />
        {brandName && (
          <span className="text-sidebar-foreground truncate text-sm font-medium">{brandName}</span>
        )}
      </div>
    )
  }

  if (cfg) {
    return (
      <div className="flex min-w-0 items-center gap-2.5">
        <img src={cfg} alt={alt} className={logoClass} onError={onErr(cfg)} />
        {brandName && (
          <span className="text-sidebar-foreground truncate text-sm font-medium">{brandName}</span>
        )}
      </div>
    )
  }

  if (mark) {
    return (
      <div className="flex min-w-0 items-center gap-2.5">
        <img
          src={mark}
          alt={alt}
          className="h-8 w-8 shrink-0 object-contain"
          onError={onErr(mark)}
        />
        <span className="text-sidebar-foreground truncate text-sm font-medium">
          {brandName ?? alt}
        </span>
      </div>
    )
  }

  if (hasInitials) {
    return <LogoPlaceholder tone="sidebar" label={brandName ?? alt} />
  }

  if (brandName) {
    return (
      <div className="flex min-w-0 items-center gap-2.5">
        <ActuateMark className="h-7 w-7 shrink-0" />
        <span className="text-sidebar-foreground truncate text-sm font-medium">{brandName}</span>
      </div>
    )
  }

  return <ActuateWordmark className="h-9" />
}

/**
 * Children of the Posts parent. Same shape regardless of how many
 * post-type collections are registered — Post Types are accessed via
 * the `/posts/types` cards page, not as individual sidebar entries.
 */
const POSTS_CHILDREN: NavItem[] = [
  { path: '/posts', label: 'All Posts', icon: FileText },
  { path: '/posts/types', label: 'Post Types', icon: Layers },
]

/**
 * Children of the Templates parent. Templates is its own top-level item
 * (page building tools) — Saved Sections are reusable content blocks and
 * Widgets are embeddable components. These are deliberately NOT children
 * of Pages: Pages is content, Templates/Sections/Widgets are structure.
 */
const TEMPLATES_CHILDREN: NavItem[] = [
  { path: '/saved-sections', label: 'Saved Sections', icon: Library },
  { path: '/widgets', label: 'Widgets', icon: LayoutGrid },
]

/**
 * Nav tree for the default (config-less) install. Dashboard sits on its
 * own at the top, then a CONTENT group holding Pages (no children) above
 * Posts (with All Posts + Post Types) and Templates (with Saved Sections
 * + Widgets), followed by Media / Forms / SEO / Script Tags, then a
 * SETTINGS group (Users / API Keys / Settings). The group ordering is
 * meaningful — the renderer emits a section header the first time it
 * sees a new group, so "CONTENT" renders once, above Pages.
 */
const defaultNavItems: NavItem[] = [
  { path: '/', label: 'Dashboard', icon: LayoutDashboard },
  { path: '/pages', label: 'Pages', icon: File, group: 'Content' },
  { path: '/posts', label: 'Posts', icon: FileText, group: 'Content', children: POSTS_CHILDREN },
  {
    path: '/page-templates',
    label: 'Templates',
    icon: Layers,
    group: 'Content',
    children: TEMPLATES_CHILDREN,
  },
  { path: '/media', label: 'Media', icon: Image, group: 'Content' },
  { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
  { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
  { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
  { path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
]

export interface SidebarProps {
  collapsed: boolean
  onToggleCollapse: () => void
  currentPath: string
  onNavigate: (path: string) => void
  config?: any
}

export function Sidebar({
  collapsed,
  onToggleCollapse,
  currentPath,
  onNavigate,
  config,
}: SidebarProps) {
  const formsSidebar = useFormsSidebar()
  const navItems = applyFormsNav(buildNavItems(config), formsSidebar)
  const { density } = useTheme()
  const compact = density === 'compact'

  return (
    <aside
      className={`bg-sidebar border-sidebar-border flex h-full flex-col border-r transition-all duration-200 ${
        collapsed ? 'w-20' : 'w-64'
      }`}
    >
      {/* Brand header — no toggle button here anymore; it now lives in the footer. */}
      <div
        className={`border-sidebar-border flex h-14 shrink-0 items-center border-b px-4 ${
          collapsed ? 'justify-center' : ''
        }`}
      >
        <BrandLogo config={config} collapsed={collapsed} />
      </div>

      <nav
        className={`flex-1 overflow-y-auto ${compact ? 'space-y-0.5 p-2' : 'space-y-1 p-3'}`}
        aria-label="Primary"
      >
        {renderNavTree(navItems, { collapsed, currentPath, onNavigate, compact })}
      </nav>

      {/* Footer collapse control. Sits at the bottom of the sidebar so it
          mirrors the reference design and stays out of the way during
          everyday nav. Hidden below md: the mobile sheet has its own
          dismiss affordance in <AdminShell>. */}
      <div className="border-sidebar-border hidden shrink-0 border-t p-3 md:block">
        <button
          onClick={onToggleCollapse}
          className={`text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors ${
            collapsed ? 'justify-center' : ''
          }`}
          aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
          title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
        >
          {collapsed ? (
            <PanelLeftOpen className="h-4 w-4 shrink-0" aria-hidden />
          ) : (
            <>
              <PanelLeftClose className="h-4 w-4 shrink-0" aria-hidden />
              <span className="font-medium">Collapse sidebar</span>
            </>
          )}
        </button>
      </div>
    </aside>
  )
}

// ─── Dynamic Forms unread badge ───────────────────────────────────────────

interface FormsSidebarState {
  /** Total unread submissions across active forms (parent badge). */
  totalUnread: number
  /** Per-form unread counts, used to build the Forms submenu children. */
  forms: FormsSidebarCount[]
}

/**
 * Live unread counts for the Forms submenu: a total surfaced on the Forms nav
 * item plus per-form counts that drive the "All Entries" + per-form children.
 * Refreshes on mount and whenever a forms/entries mutation fires the shared
 * event (mark read, archive, notify toggle, …). Best-effort: a failed fetch
 * keeps the last known value rather than surfacing an error in nav.
 */
function useFormsSidebar(): FormsSidebarState {
  const [state, setState] = useState<FormsSidebarState>({ totalUnread: 0, forms: [] })

  useEffect(() => {
    let active = true
    const load = () => {
      fetchFormsSidebarCounts()
        .then((counts) => {
          if (!active) return
          setState({
            totalUnread: counts.reduce((sum, c) => sum + (c.unread ?? 0), 0),
            forms: counts,
          })
        })
        .catch(() => {
          /* keep the last known value; nav must never break on a fetch error */
        })
    }
    load()
    const off = onFormsChanged(load)
    return () => {
      active = false
      off()
    }
  }, [])

  return state
}

/**
 * Return a copy of the nav tree with the Forms item expanded into a submenu:
 * a total-unread badge on the parent, an "All Entries" child, and one child
 * per active form carrying its own unread badge.
 */
function applyFormsNav(items: NavItem[], state: FormsSidebarState): NavItem[] {
  return items.map((item) => {
    if (item.path !== '/forms') return item
    const children: NavItem[] = [
      { path: '/forms/entries', label: 'All Entries', icon: Inbox },
      ...state.forms.map((f) => ({
        path: `/forms/${f.formId}/submissions`,
        label: f.name,
        icon: ClipboardList,
        badge: f.unread,
      })),
    ]
    return {
      ...item,
      badge: state.totalUnread > 0 ? state.totalUnread : undefined,
      children,
    }
  })
}

// ─── Rendering ──────────────────────────────────────────────────────────────

interface NavRenderContext {
  collapsed: boolean
  currentPath: string
  onNavigate: (path: string) => void
  /** Compact density tightens nav row vertical padding. */
  compact?: boolean
}

function isPathActive(currentPath: string, itemPath: string): boolean {
  // Ignore query string / hash so state-bearing routes (e.g. `/seo?tab=audit`)
  // still highlight their nav item.
  currentPath = currentPath.split('?')[0]!.split('#')[0]!
  if (itemPath === '/') return currentPath === '/'

  // Special case: /posts is a prefix of /posts/types — without this
  // guard, "All Posts" (/posts) would stay highlighted while the user
  // is on /posts/types or /posts/types/new. Treat /posts as exact-only
  // when a sibling /posts/* route exists.
  if (itemPath === '/posts') {
    return currentPath === '/posts' || currentPath === '/posts/new'
  }

  // Use boundary match so /pages doesn't match /pages-something. Exact, or
  // followed by a `/` separator.
  return currentPath === itemPath || currentPath.startsWith(itemPath + '/')
}

function renderNavTree(items: NavItem[], ctx: NavRenderContext): ReactNode {
  const nodes: ReactNode[] = []
  let lastGroup: string | undefined = undefined

  for (const item of items) {
    if (item.group && item.group !== lastGroup) {
      nodes.push(
        ctx.collapsed ? (
          <div key={`g-${item.group}`} className="flex justify-center pt-3 pb-1">
            <span className="border-sidebar-foreground/20 w-4 border-t" aria-hidden />
          </div>
        ) : (
          <div key={`g-${item.group}`} className="px-3 pt-4 pb-1">
            <span className="text-sidebar-foreground/55 text-[10px] font-medium tracking-wider uppercase">
              {item.group}
            </span>
          </div>
        ),
      )
      lastGroup = item.group
    } else if (!item.group) {
      lastGroup = undefined
    }

    const hasChildren = !!item.children && item.children.length > 0

    // Collapsed parents reveal their children in a hover/focus flyout instead
    // of inline (there's no room for indented rows in the 80px rail).
    if (hasChildren && ctx.collapsed) {
      nodes.push(<CollapsedFlyout key={`flyout:${item.path}`} item={item} ctx={ctx} />)
      continue
    }

    // Expanded parents with children render as a collapsible section
    // (chevron toggle to contract/expand the submenu).
    if (hasChildren) {
      nodes.push(<NavSection key={`section:${item.path}`} item={item} ctx={ctx} />)
      continue
    }

    // Keys are namespaced because a parent and its first child can share
    // the same path (e.g. the Posts parent and its "All Posts" child are
    // both `/posts`). Using the raw path as the key collided and made
    // React warn about — and risk dropping — duplicate siblings.
    nodes.push(<NavLink key={`item:${item.path}`} item={item} ctx={ctx} />)
  }

  return nodes
}

/**
 * Expanded parent with a collapsible submenu. The label/icon navigates to the
 * parent destination; a trailing chevron toggles the child list open/closed.
 * Defaults to open, and auto-opens when the parent or one of its children is
 * the active route so the current page is never hidden behind a collapsed
 * section.
 */
function NavSection({ item, ctx }: { item: NavItem; ctx: NavRenderContext }) {
  const Icon = item.icon
  const isActive = isPathActive(ctx.currentPath, item.path)
  const childActive = item.children!.some((c) => isPathActive(ctx.currentPath, c.path))
  const [open, setOpen] = useState(true)
  const childListId = `nav-section-${item.path.replace(/[^a-z0-9]+/gi, '-')}`

  // Keep the active section visible: if the user is on this section's route
  // (parent or child) it stays expanded regardless of the manual toggle.
  const expanded = open || isActive || childActive

  return (
    <div>
      <div
        className={`flex items-center rounded-lg transition-colors ${
          isActive
            ? 'bg-sidebar-accent text-sidebar-primary'
            : 'text-sidebar-foreground hover:bg-sidebar-accent'
        }`}
      >
        <button
          onClick={() => ctx.onNavigate(item.path)}
          className="flex min-w-0 flex-1 items-center gap-3 rounded-lg py-2.5 pr-1 pl-3 text-left"
          aria-current={isActive ? 'page' : undefined}
        >
          <Icon className="h-5 w-5 shrink-0" aria-hidden />
          <span className="truncate text-sm font-medium">{item.label}</span>
        </button>
        {!!item.badge && item.badge > 0 && (
          <span
            className="bg-destructive text-destructive-foreground mr-1 inline-flex min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
            aria-label={`${item.badge} unread`}
          >
            {item.badge > 99 ? '99+' : item.badge}
          </span>
        )}
        <button
          onClick={() => setOpen((v) => !v)}
          className="text-sidebar-foreground/55 hover:text-sidebar-foreground mr-1 flex shrink-0 items-center rounded-md p-1.5 transition-colors"
          aria-label={`${expanded ? 'Collapse' : 'Expand'} ${item.label}`}
          aria-expanded={expanded}
          aria-controls={childListId}
        >
          <ChevronDown
            className={`h-4 w-4 transition-transform duration-200 ${expanded ? '' : '-rotate-90'}`}
            aria-hidden
          />
        </button>
      </div>

      {/* Tight (gap-less) connector container so the vertical tree guides of
          adjacent rows touch and read as one continuous line with curved
          elbows into each child. */}
      {expanded && (
        <div id={childListId} className="relative">
          {item.children!.map((child, i) => (
            <NavLink
              key={`child:${item.path}:${child.path}`}
              item={child}
              ctx={ctx}
              nested
              isLast={i === item.children!.length - 1}
            />
          ))}
        </div>
      )}
    </div>
  )
}

function NavLink({
  item,
  ctx,
  nested = false,
  isLast = false,
}: {
  item: NavItem
  ctx: NavRenderContext
  nested?: boolean
  /** Last child in its group — the elbow stops here (no continuation line). */
  isLast?: boolean
}) {
  const Icon = item.icon
  const isActive = isPathActive(ctx.currentPath, item.path)

  const badge = item.badge && item.badge > 0 ? item.badge : 0

  const button = (
    <button
      onClick={() => ctx.onNavigate(item.path)}
      className={`flex w-full items-center gap-3 rounded-lg text-left transition-colors ${
        nested
          ? ctx.compact
            ? 'py-1 pr-3 pl-9'
            : 'py-1.5 pr-3 pl-9'
          : ctx.compact
            ? 'px-3 py-1.5'
            : 'px-3 py-2.5'
      } ${
        isActive
          ? 'bg-sidebar-accent text-sidebar-primary'
          : 'text-sidebar-foreground hover:bg-sidebar-accent'
      } ${ctx.collapsed ? 'justify-center px-3' : ''}`}
      title={ctx.collapsed ? item.label : ''}
      aria-current={isActive ? 'page' : undefined}
    >
      <span className="relative flex shrink-0 items-center">
        {!nested && <Icon className="h-5 w-5 shrink-0" aria-hidden />}
        {nested && !ctx.collapsed && (
          <Icon className="text-sidebar-foreground/60 h-4 w-4 shrink-0" aria-hidden />
        )}
        {/* Collapsed rail: a dot signals unread without room for a number. */}
        {ctx.collapsed && badge > 0 && (
          <span
            className="bg-destructive absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full"
            aria-hidden
          />
        )}
      </span>
      {!ctx.collapsed && (
        <span className={`truncate font-medium ${nested ? 'text-[13px]' : 'text-sm'}`}>
          {item.label}
        </span>
      )}
      {!ctx.collapsed && badge > 0 && (
        <span
          className="bg-destructive text-destructive-foreground ml-auto inline-flex min-w-5 items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
          aria-label={`${badge} unread`}
        >
          {badge > 99 ? '99+' : badge}
        </span>
      )}
    </button>
  )

  if (!nested || ctx.collapsed) return button

  // Curved-elbow tree connector (expanded only). The elbow draws the vertical
  // run from the row top to its vertical center, curves at the bottom-left
  // corner, then runs horizontally into the row. Non-last rows continue the
  // vertical line to the row bottom so it meets the next sibling's elbow.
  // The guide is centered (~21px) under the parent item's 20px icon.
  return (
    <div className="relative">
      <span
        aria-hidden
        className="border-sidebar-border absolute top-0 left-[21px] h-1/2 w-[15px] rounded-bl-lg border-b border-l"
      />
      {!isLast && (
        <span
          aria-hidden
          className="border-sidebar-border absolute top-1/2 bottom-0 left-[21px] border-l"
        />
      )}
      {button}
    </div>
  )
}

/**
 * Collapsed-rail parent: shows the icon button and, on hover or keyboard
 * focus, a flyout listing the parent's children. Built on Radix
 * DropdownMenu so the panel is portaled (escaping the nav's `overflow`
 * clip), positioned, dismissable, and keyboard-navigable. Hover-opening is
 * controlled manually; we suppress Radix's auto-focus when opened by pointer
 * so hovering doesn't steal focus, while keyboard opening still moves focus
 * into the menu.
 */
function CollapsedFlyout({ item, ctx }: { item: NavItem; ctx: NavRenderContext }) {
  const Icon = item.icon
  const isActive = isPathActive(ctx.currentPath, item.path)
  const { resolvedTheme } = useTheme()
  const [open, setOpen] = useState(false)
  const openedByPointer = useRef(false)
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)

  const cancelClose = () => {
    if (closeTimer.current) {
      clearTimeout(closeTimer.current)
      closeTimer.current = null
    }
  }
  const scheduleClose = () => {
    cancelClose()
    closeTimer.current = setTimeout(() => setOpen(false), 120)
  }

  return (
    <DropdownMenu.Root
      open={open}
      onOpenChange={(next) => {
        if (!next) openedByPointer.current = false
        setOpen(next)
      }}
      modal={false}
    >
      <div
        onMouseEnter={() => {
          openedByPointer.current = true
          cancelClose()
          setOpen(true)
        }}
        onMouseLeave={scheduleClose}
      >
        <DropdownMenu.Trigger asChild>
          <button
            onClick={() => ctx.onNavigate(item.path)}
            className={`flex w-full items-center justify-center rounded-lg px-3 py-2.5 transition-colors ${
              isActive
                ? 'bg-sidebar-accent text-sidebar-primary'
                : 'text-sidebar-foreground hover:bg-sidebar-accent'
            }`}
            aria-label={
              item.badge && item.badge > 0 ? `${item.label} (${item.badge} unread)` : item.label
            }
            title={item.label}
            aria-current={isActive ? 'page' : undefined}
          >
            <span className="relative flex shrink-0 items-center">
              <Icon className="h-5 w-5 shrink-0" aria-hidden />
              {!!item.badge && item.badge > 0 && (
                <span
                  className="bg-destructive absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full"
                  aria-hidden
                />
              )}
            </span>
          </button>
        </DropdownMenu.Trigger>
      </div>

      <DropdownMenu.Portal container={adminPortalContainer()}>
        <DropdownMenu.Content
          side="right"
          align="start"
          sideOffset={10}
          onMouseEnter={cancelClose}
          onMouseLeave={scheduleClose}
          onCloseAutoFocus={(e) => {
            // After a pointer-driven open, don't yank focus back to the rail
            // trigger (it would leave a stray focus ring once the cursor moves
            // away). Keyboard-driven opens keep the default focus-return.
            if (openedByPointer.current) e.preventDefault()
          }}
          // Radix portals to <body>, outside the `.actuate-admin` scope where
          // the theme tokens (`--popover`, `--radius`, …) live — without the
          // scope class the panel renders transparent with square corners.
          // Re-assert the scope (and current dark state) on the panel itself so
          // it inherits the tokens regardless of portal position.
          className={`actuate-admin ${resolvedTheme === 'dark' ? 'dark' : ''} bg-popover text-popover-foreground border-border motion-safe:animate-in motion-safe:fade-in z-50 min-w-44 overflow-hidden rounded-lg border p-1 shadow-lg`}
        >
          <div className="text-muted-foreground border-border mb-1 border-b px-3 py-2 text-xs font-medium">
            {item.label}
          </div>
          {item.children!.map((child) => {
            const ChildIcon = child.icon
            const childActive = isPathActive(ctx.currentPath, child.path)
            return (
              <DropdownMenu.Item
                key={`flyout-item:${item.path}:${child.path}`}
                onSelect={() => ctx.onNavigate(child.path)}
                className={`data-highlighted:bg-accent hover:bg-accent flex cursor-pointer items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none select-none ${
                  childActive ? 'text-primary' : 'text-foreground'
                }`}
                aria-current={childActive ? 'page' : undefined}
              >
                <ChildIcon className="text-muted-foreground h-4 w-4 shrink-0" aria-hidden />
                <span className="truncate">{child.label}</span>
                {!!child.badge && child.badge > 0 && (
                  <span
                    className="bg-destructive text-destructive-foreground ml-auto inline-flex min-w-5 items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
                    aria-label={`${child.badge} unread`}
                  >
                    {child.badge > 99 ? '99+' : child.badge}
                  </span>
                )}
              </DropdownMenu.Item>
            )
          })}
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

// ─── Config-driven nav builder ──────────────────────────────────────────────

function resolveIcon(collection: any): LucideIcon {
  const mapped = collection.admin?.icon ? ICON_MAP[collection.admin.icon] : undefined
  if (mapped) return mapped
  return collection.type === 'page' ? File : FileText
}

export interface NavItem {
  path: string
  label: string
  icon: LucideIcon
  /**
   * Section header. Items sharing the same `group` are grouped together
   * under a header rendered on first encounter. Items without a `group`
   * render at the top of the nav in document order.
   */
  group?: string
  /** Indented children rendered immediately after this item. */
  children?: NavItem[]
  /**
   * Optional unread/notification count. Rendered as a red pill (expanded)
   * or a dot (collapsed). Falsy / zero values render nothing.
   */
  badge?: number
}

/**
 * Build the sidebar nav tree from the CMS config. If no collections are
 * configured, returns the curated `defaultNavItems` tree. When
 * collections ARE configured, page-type collections inherit the
 * Pages / Saved Sections / Templates child block, and every static
 * destination (Media, Forms, SEO, Script Tags, Users, API Keys,
 * Settings) is anchored under the right Content / Settings group.
 */
export function buildNavItems(config: any): NavItem[] {
  if (!config?.collections) return defaultNavItems

  const raw = config.collections
  const collectionsList: any[] = Array.isArray(raw) ? raw : Object.values(raw)
  const visible = collectionsList.filter((c) => !c.admin?.hidden)

  // Treat `admin.group: 'Posts'` (case-insensitive) as a synonym for
  // `type: 'post'` so consumers can tag arbitrary collections into
  // the Posts area without changing the discriminator they already
  // use elsewhere (sitemaps, schema.org, etc.).
  const isPostsGroup = (c: any) =>
    typeof c.admin?.group === 'string' && c.admin.group.toLowerCase() === 'posts'
  const pages = visible.filter((c) => c.type === 'page' && !c.admin?.group)
  const posts = visible.filter((c) => c.type === 'post' || isPostsGroup(c))
  const otherUngrouped = visible.filter(
    (c) => c.type !== 'page' && c.type !== 'post' && !c.admin?.group,
  )
  // Anything explicitly grouped that isn't a posts collection.
  const grouped = visible.filter((c) => c.admin?.group && !isPostsGroup(c) && c.type !== 'post')

  const items: NavItem[] = [{ path: '/', label: 'Dashboard', icon: LayoutDashboard }]

  // Ungrouped collections that are neither page- nor post-type stay at
  // the top level, directly under Dashboard, before the CONTENT group.
  for (const collection of otherUngrouped) {
    items.push({
      label: collection.labels?.plural ?? collection.slug,
      path: `/${collection.slug}`,
      icon: resolveIcon(collection),
    })
  }

  // CONTENT group. Everything below shares `group: 'Content'`, so the
  // renderer emits the "CONTENT" header once — above Pages. Order:
  // Pages → Posts → static content destinations (Media / Forms / SEO /
  // Script Tags).

  // Pages-style collections render as plain Content entries (no nested
  // children). Saved Sections / Widgets now live under the dedicated
  // Templates parent below, not under Pages.
  pages.forEach((collection) => {
    items.push({
      label: collection.labels?.plural ?? collection.slug,
      path: `/${collection.slug}`,
      icon: resolveIcon(collection),
      group: 'Content',
    })
  })

  // Posts area — collapses every post-type collection under a single
  // "Posts" parent. The aggregated table at /posts and the type cards
  // at /posts/types are the entry points; individual post-type
  // collections are not enumerated in the sidebar to avoid the
  // duplicate-nav problem when a project has many types.
  if (posts.length > 0) {
    items.push({
      path: '/posts',
      label: 'Posts',
      icon: FileText,
      group: 'Content',
      children: POSTS_CHILDREN,
    })
  }

  // Templates — its own parent holding Saved Sections + Widgets (page
  // building structure, distinct from Pages content).
  items.push({
    path: '/page-templates',
    label: 'Templates',
    icon: Layers,
    group: 'Content',
    children: TEMPLATES_CHILDREN,
  })

  // Standard Content destinations (always present — they map to CMS
  // features, not collections).
  items.push(
    { path: '/media', label: 'Media', icon: Image, group: 'Content' },
    { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
    { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
    { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
  )

  // Config-supplied custom groups land after the standard Content block
  // and before Settings. Each renders under its own header.
  const groupBuckets = new Map<string, NavItem[]>()
  for (const col of grouped) {
    const group = col.admin.group as string
    const bucket = groupBuckets.get(group) ?? []
    bucket.push({
      label: col.labels?.plural ?? col.slug,
      path: `/${col.slug}`,
      icon: resolveIcon(col),
      group,
    })
    groupBuckets.set(group, bucket)
  }
  for (const bucket of groupBuckets.values()) items.push(...bucket)

  // Settings destinations — always last. Users + API Keys now live inside
  // the Settings view as tabs (see Settings.tsx), so only Settings is anchored
  // here.
  items.push({ path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' })

  return items
}
