import { Link, router, usePage } from '@inertiajs/react'
import { createPortal } from 'react-dom'
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import * as Icons from '@/Components/Icons'
import { useToast } from '@/Components/Toast'
import { NotificationBell } from '@/Components/Sidebar/NotificationBell'
import SettingsPopup from '@/Components/Sidebar/SettingsPopup'

const { ChevronDown, ChevronsLeft, LogOut, Moon, Settings, Sun } = Icons

// Menu links whose database tables `two db/step/` has not migrated yet, shared by
// HandleInertiaRequests as `migrationPending`. Empty on every normal database — it is only
// non-empty on a production being migrated one module at a time.
//
// The marked leaves stay CLICKABLE. Sidebar visibility is not the gate (ATURAN ABSOLUT #25);
// EnsureModuleMigrated is, and it answers 503 with the group and the tables still owed.
// Disabling the link here would hide that message behind a dead control.
const MigrationPendingContext = createContext(null)

function useIsMigrationPending(href) {
    const pending = useContext(MigrationPendingContext)
    if (!pending || !href) return false
    return pending.has(href.split('?')[0])
}

function MigrationPendingMark() {
    return (
        <span
            title="Not migrated yet — this page will answer 503 until its tables are applied"
            aria-label="not migrated yet"
            className="flex-shrink-0 rounded px-1 py-px text-[9px] font-bold uppercase leading-none tracking-wide text-amber-700 ring-1 ring-amber-400 dark:text-amber-300 dark:ring-amber-500/60"
        >
            db
        </span>
    )
}

// Static nav for the ported NewUI prototype pages (dummy data, auth-gated).
// Hardcoded paths (not route()) so it never depends on Ziggy being loaded.
// Same {key,label,icon?,children?,href?} shape as the DB navItems, so it renders
// through the existing NavSection/TreeNode (accordion, active-state, collapse).
const prototypeNav = [
    { key: 'proto-approver-queue', label: 'Antrean Saya', icon: 'ClipboardCheck', children: [
        { key: 'proto-aq-qpm-v2', label: 'Quotation Approval PM v2', href: '/proto/approver-queue/quotation-approval-pm' },
        { key: 'proto-aq-embed', label: 'Approval PM (band demo)', href: '/proto/approver-queue/approval-pm' },
    ] },
    { key: 'proto-dashboards', label: 'Dashboards', icon: 'Grid', children: [
        { key: 'proto-dash-sales', label: 'Sales Dashboard', href: '/proto/dashboards/sales' },
        { key: 'proto-dash-sales-v2', label: 'Sales Dashboard v2', href: '/proto/dashboard-v2' },
        { key: 'proto-dash-sales-v3', label: 'Sales Dashboard v3', href: '/proto/dashboard-v3' },
        { key: 'proto-dash-pm', label: 'PM Dashboard', href: '/proto/dashboards/pm' },
        { key: 'proto-dash-pm-v2', label: 'PM Dashboard v2', href: '/proto/dashboard-v2/pm' },
        { key: 'proto-dash-pm-v3', label: 'PM Dashboard v3', href: '/proto/dashboard-v3/pm' },
        { key: 'proto-dash-sm', label: 'SM Dashboard', href: '/proto/dashboards/sm' },
        { key: 'proto-dash-sm-v2', label: 'SM Dashboard v2', href: '/proto/dashboard-v2/sm' },
        { key: 'proto-dash-sm-v3', label: 'SM Dashboard v3', href: '/proto/dashboard-v3/sm' },
        { key: 'proto-dash-gallery', label: 'Widget Gallery', href: '/proto/dashboards/gallery' },
        { key: 'proto-dash-preview', label: 'Widget Preview', href: '/proto/dashboards/preview' },
    ] },
    // Sample Order / Lab Work Request proto sections stay HIDDEN (their real DB-driven
    // menus were promoted to the top nav). Proto Quotations re-surfaced 2026-07-24 for
    // design reference (Customer Outstanding etc.).
    { key: 'proto-quotations', label: 'Quotations', icon: 'FileText', children: [
        { key: 'proto-qt-index', label: 'Quotations', href: '/proto/quotations' },
        { key: 'proto-qt-create', label: 'Create Quotation', href: '/proto/quotations/create' },
        // The archived v1 Create form — a REAL gated screen, unlike every other link here.
        // Users without the Quotation create grant see this link and get a 403 (accepted
        // 2026-08-11: prototypeNav has no per-link grant check). Register: GH #380.
        { key: 'proto-qt-create-v1', label: 'Create Quotation v1', href: '/proto/quotations/create-v1' },
        { key: 'proto-qt-approval-pm', label: 'Approval PM', href: '/proto/quotations/approval-pm' },
        { key: 'proto-qt-approval-sm', label: 'Approval SM', href: '/proto/quotations/approval-sm' },
        { key: 'proto-qt-view-head', label: 'View Head', href: '/proto/quotations/view-head' },
        { key: 'proto-qt-pto', label: 'Process to Order', href: '/proto/quotations/process-to-order' },
        { key: 'proto-qt-good-receive', label: 'Good Receive', href: '/proto/quotations/good-receive' },
    ] },
    // { key: 'proto-sample-order', label: 'Sample Order', icon: 'Package', children: [
    //     { key: 'proto-so-create', label: 'Request Sample', href: '/proto/sample-orders/create' },
    // ] },
    // { key: 'proto-lwr', label: 'Lab Work Request', icon: 'FlaskConical', children: [
    //     { key: 'lwr-index', label: 'View All LWR', href: '/lwrs' },
    //     { key: 'lwr-approval-pm', label: 'Approval PM', href: '/lwrs/approval-pm' },
    //     { key: 'proto-lwr-create', label: 'Create New LWR (Proto)', href: '/proto/lwr/create' },
    // ] },
    { key: 'proto-companies', label: 'Companies', icon: 'Building', children: [
        { key: 'proto-comp-index', label: 'Master Data Customer', href: '/proto/companies' },
        { key: 'proto-comp-create', label: 'Insert Company', href: '/proto/companies/create' },
        { key: 'proto-comp-market-survey', label: 'Market Survey', href: '/proto/companies/market-survey' },
        { key: 'proto-comp-market-survey-pm', label: 'Market Survey PM', href: '/proto/companies/market-survey-pm' },
        { key: 'proto-comp-market-survey-sm', label: 'Market Survey SM', href: '/proto/companies/market-survey-sm' },
    ] },
    { key: 'proto-stock', label: 'Stock Sample', icon: 'Box', children: [
        { key: 'proto-stock-index', label: 'Stock Sample', href: '/proto/stock-sample' },
        { key: 'proto-stock-insert', label: 'Insert Sample', href: '/proto/stock-sample/insert' },
        { key: 'proto-stock-all', label: 'All Stock', href: '/proto/stock-sample/all' },
        { key: 'proto-stock-edit', label: 'Edit Sample', href: '/proto/stock-sample/edit' },
    ] },
    { key: 'proto-complaints', label: 'Complain & Returns', icon: 'ShieldAlert', children: [
        { key: 'proto-complaints-create', label: 'Create Complain & Returns', href: '/proto/complaints/create' },
        { key: 'proto-complaints-view-all', label: 'View All', href: '/proto/complaints/view-all' },
        { key: 'proto-complaints-review-pm', label: 'Review PM', href: '/proto/complaints/review-pm' },
        { key: 'proto-complaints-approval-ceo', label: 'View CEO', href: '/proto/complaints/approval-ceo' },
    ] },
    { key: 'proto-complaint-handling', label: 'Complaint Handling', icon: 'MessageSquare', children: [
        { key: 'proto-ch-logistic', label: 'Create - Logistic', href: '/proto/complaints/handling-logistic' },
        { key: 'proto-ch-pm', label: 'Create - PM', href: '/proto/complaints/handling-pm' },
        { key: 'proto-ch-review-pm', label: 'Search - Review PM', href: '/proto/complaints/handling-review-pm' },
        { key: 'proto-ch-approval-ceo', label: 'Search - Approval CEO', href: '/proto/complaints/handling-approval-ceo' },
    ] },
    { key: 'proto-projects', label: 'Projects', icon: 'Kanban', children: [
        { key: 'proto-projects-board', label: 'Project Pipeline Board', href: '/proto/projects' },
        { key: 'proto-projects-create', label: 'Create Company Project', href: '/proto/projects/create' },
        { key: 'proto-projects-approval-pm', label: 'Approval PM', href: '/proto/projects/approval-pm' },
    ] },
    { key: 'proto-visit-report', label: 'Visit Report', icon: 'Route', children: [
        { key: 'proto-visit-report-index', label: 'View All Visit Report', href: '/proto/visit-report' },
        { key: 'proto-visit-report-create', label: 'Create Visit Plan', href: '/proto/visit-report/create' },
        { key: 'proto-visit-report-calendar', label: 'View Calendar', href: '/proto/visit-report/calendar' },
        { key: 'proto-visit-report-calendar-v2', label: 'View Calendar (Grid)', href: '/proto/visit-report/calendar-v2' },
        { key: 'proto-visit-report-report', label: 'Report (Detail)', href: '/proto/visit-report/report' },
        { key: 'proto-visit-report-report-v2', label: 'Report v2 (Unified)', href: '/proto/visit-report/report-v2' },
    ] },
    { key: 'proto-budget', label: 'Budget & Target', icon: 'BarChart', children: [
        { key: 'proto-budget-create', label: 'Create Budget & Target', href: '/proto/budget/create' },
        { key: 'proto-budget-approval-pm', label: 'Approval PM', href: '/proto/budget/approval-pm' },
        { key: 'proto-budget-approval-sm', label: 'Approval SM', href: '/proto/budget/approval-sm' },
        { key: 'proto-budget-report-5y', label: '5 Years Growth Report', href: '/proto/budget/report-5-years' },
        { key: 'proto-budget-pivot', label: 'View All', href: '/proto/budget/pivot' },
    ] },
    { key: 'proto-success-story', label: 'Success Story', icon: 'Trophy', children: [
        { key: 'proto-success-story-index', label: 'View All Success Story', href: '/proto/success-story' },
        { key: 'proto-success-story-create', label: 'Create Success Story', href: '/proto/success-story/create' },
    ] },
    { key: 'proto-sample-order-import', label: 'Sample Order Import', icon: 'FlaskConical', children: [
        { key: 'proto-soi-request', label: 'Request Sample Import', href: '/proto/sample-orders/request-import' },
        { key: 'proto-soi-all-request', label: 'All Request', href: '/proto/sample-orders/all-request-import' },
        { key: 'proto-soi-approval-pm', label: 'Approval PM', href: '/proto/sample-orders/approval-pm-import' },
    ] },
    { key: 'proto-netsuite', label: 'Netsuite Integration', icon: 'Package', children: [
        { key: 'proto-netsuite-stock', label: 'Stock Barang', href: '/proto/netsuite/stock' },
        { key: 'proto-netsuite-booking', label: 'Booking Barang', href: '/proto/netsuite/booking-barang' },
        { key: 'proto-netsuite-customer', label: 'Customer', href: '/proto/netsuite/customer' },
        { key: 'proto-netsuite-ar', label: 'Accounts Receivable', href: '/proto/netsuite/accounts-receivable' },
    ] },
    { key: 'proto-netsuite-po', label: 'Netsuite Purchase Order Manual', icon: 'FileText', children: [
        { key: 'proto-netsuite-po-create', label: 'Create Purchase Order', href: '/proto/netsuite-po/create' },
        { key: 'proto-netsuite-po-approval-ceo', label: 'Approval CEO', href: '/proto/netsuite-po/approval-ceo' },
    ] },
    { key: 'proto-forecast', label: 'Forecast', icon: 'DollarSign', children: [
        { key: 'proto-forecast-create', label: 'Create Forecast', href: '/proto/forecast/create' },
        { key: 'proto-forecast-review-pm', label: 'Review PM', href: '/proto/forecast/review-pm' },
        { key: 'proto-forecast-review-pic', label: 'Review PIC', href: '/proto/forecast/review-pic' },
    ] },
    { key: 'proto-ast', label: 'AST Integration', icon: 'Plug', children: [
        { key: 'proto-ast-customer', label: 'Customer', href: '/proto/ast-integration/customer' },
        { key: 'proto-ast-surat-jalan', label: 'Surat Jalan', href: '/proto/ast-integration/surat-jalan' },
    ] },
    { key: 'proto-documents', label: 'Documents', icon: 'FileText', children: [
        { key: 'proto-documents-index', label: 'List Document', href: '/proto/documents' },
    ] },
    { key: 'proto-emails', label: 'Email Templates', icon: 'MessageSquare', children: [
        { key: 'proto-email-quotation-revised', label: 'Quotation Revised', href: '/proto/emails/quotation-revised' },
        { key: 'proto-email-quotation-revised-v2', label: 'Quotation Revised V2', href: '/proto/emails/quotation-revised-v2' },
        { key: 'proto-email-quotation-revised-v3', label: 'Quotation Revised V3 (Wide)', href: '/proto/emails/quotation-revised-v3' },
        { key: 'proto-email-quotation-revised-v4', label: 'Quotation Revised V4', href: '/proto/emails/quotation-revised-v4' },
    ] },
    { key: 'proto-payroll', label: 'Payroll', icon: 'Users', children: [
        { key: 'proto-payroll-create-izin', label: 'Create Izin', href: '/proto/payroll/create-izin' },
    ] },
    { key: 'proto-print-label', label: 'Print Label CC Product', icon: 'Tag', children: [
        { key: 'proto-print-label-relabel', label: 'Print Relabel', href: '/proto/print-label/relabel' },
        { key: 'proto-print-label-history', label: 'Relabel History', href: '/proto/print-label/relabel-history' },
        { key: 'proto-print-label-barcode', label: 'Print Barcode', href: '/proto/print-label/barcode' },
    ] },
    { key: 'proto-general-affair', label: 'General Affair', icon: 'Briefcase', children: [
        { key: 'proto-ga-vehicle-service-create', label: 'Create Vehicle Service', href: '/proto/general-affair/vehicle-service/create' },
        { key: 'proto-ga-review-ga', label: 'Review GA', href: '/proto/general-affair/review-ga' },
        { key: 'proto-ga-approval-ga', label: 'Approval GA', href: '/proto/general-affair/approval-ga' },
        { key: 'proto-ga-finance-dp', label: 'Finance DP', href: '/proto/general-affair/finance-dp' },
    ] },
    { key: 'proto-principal', label: 'Principal', icon: 'BarChart', children: [
        { key: 'proto-principal-analysis-ast', label: 'Principal Analysis - AST', href: '/proto/principals/analysis-ast' },
    ] },
]

// Every nav glyph renders at ONE size regardless of a component's own default,
// so the sidebar reads uniformly (Icons.jsx defaults vary 14–22px).
function resolveIcon(name) {
    if (!name) return null
    const Cmp = Icons[name]
    return Cmp ? <Cmp size={18} /> : null
}

// Nav hrefs are exact paths, but the browser sits on URLs the menu has no row for:
// query strings (`/banks?page=2`) and sub-routes (`/banks/create`, `/complaints/ceo/937`).
// Strip query/hash + trailing slash so both sides compare as plain paths.
function normalizePath(value) {
    const path = String(value ?? '').split(/[?#]/)[0]
    return path.length > 1 ? path.replace(/\/+$/, '') : path
}

// Leaves always carry a `key` (navTreeForRole emits `menu-<id>`, prototypeNav hardcodes one);
// href is only the fallback.
const nodeKey = (node) => node?.key ?? node?.href ?? null

// ──────────────────────────────────────────────────────────────
// PENDING-WORK BADGES
//
// Counts arrive in the DEFERRED `navBadges` page prop, keyed by menu link:
// { '/quotations/approval-sm': 3 }. The server omits zero counts, so a missing
// key means "nothing waiting" — never "still loading". Until the deferred
// request lands the map is empty and no badge renders, which is why there is no
// skeleton state here.
//
// A number means "work you can act on". It is NOT the row count of the page —
// the two deliberately differ on several menus. See app/Support/NavBadges/.
// ──────────────────────────────────────────────────────────────

const EMPTY_BADGES = Object.freeze({})

const NavBadgeContext = createContext(EMPTY_BADGES)

// A leaf reports its own count; a group reports the sum of everything beneath it,
// so a collapsed section can still say how much is waiting inside it.
function badgeTotal(node, badges) {
    if (!node) return 0
    if (Array.isArray(node.children)) {
        return node.children.reduce((sum, child) => sum + badgeTotal(child, badges), 0)
    }
    return node.href ? (badges[node.href] || 0) : 0
}

function useBadgeTotal(node) {
    const badges = useContext(NavBadgeContext)
    return useMemo(() => badgeTotal(node, badges), [node, badges])
}

// FLAT, never gradient — a count is a state marker, not an action
// (.claude/rules/ui-conventions.md, "⛔ BUTTON GRADIENT"). The round shape is
// allowed: that same rule reserves circles for avatars, status dots and count pills.
// `muted` is for rolled-up group totals, so a parent never shouts louder than the
// leaf that actually holds the work.
function NavBadge({ count, muted = false }) {
    if (!count) return null

    return (
        <span
            className={`inline-grid h-[17px] min-w-[17px] flex-shrink-0 place-items-center rounded-full px-1 text-[10px] font-bold leading-none tabular-nums ${
                muted ? 'bg-secondary text-text-muted' : 'bg-primary-light text-primary'
            }`}
        >
            {count > 99 ? '99+' : count}
        </span>
    )
}

// The ONE nav leaf that owns the current URL, as its key. Longest prefix on a SEGMENT
// boundary wins, and only that single winner counts:
//   · segment boundary → `/companies` never claims `/companies-report`;
//   · longest wins     → `/sample-orders/request-import` lights up "Sample Order Import"
//                        and NOT also "Sample Order" (9 such cross-section pairs exist today);
//   · single winner    → two menu rows sharing one href can never light up two sections.
// Returning a KEY rather than the href is what makes that last guarantee hold.
function resolveActiveKey(nodes, url) {
    const path = normalizePath(url)
    if (!path) return null
    let bestKey = null
    let bestLen = -1
    const walk = (list) => {
        if (!Array.isArray(list)) return
        for (const node of list) {
            if (node.children) {
                walk(node.children)
                continue
            }
            const href = normalizePath(node.href)
            if (!href) continue
            if (path !== href && !path.startsWith(`${href}/`)) continue
            if (href.length > bestLen) {
                bestLen = href.length
                bestKey = nodeKey(node)
            }
        }
    }
    walk(nodes)
    return bestKey
}

// True when the winning leaf lives anywhere below `children` — this is what makes every
// ancestor (section header AND nested group) light up along with the page you are on.
function containsActiveKey(children, activeKey) {
    if (!Array.isArray(children) || !activeKey) return false
    for (const child of children) {
        if (child.children) {
            if (containsActiveKey(child.children, activeKey)) return true
        } else if (nodeKey(child) === activeKey) {
            return true
        }
    }
    return false
}

// Client-side page search. Keeps a leaf when its label matches; keeps a group when its
// own label matches (then ALL its children survive) or when any descendant matches
// (then only the matching descendants survive). Returns a fresh, pruned tree.
function filterNav(nodes, rawQuery) {
    const q = (rawQuery || '').trim().toLowerCase()
    if (!q) return nodes
    const walk = (list) => list.reduce((acc, node) => {
        const selfMatch = typeof node.label === 'string' && node.label.toLowerCase().includes(q)
        if (node.children) {
            const kids = walk(node.children)
            if (selfMatch || kids.length) acc.push({ ...node, children: selfMatch ? node.children : kids })
        } else if (selfMatch) {
            acc.push(node)
        }
        return acc
    }, [])
    return walk(nodes)
}

/**
 * Drop the parent's name where a child label merely echoes it.
 *
 * Legacy `menu.ShowName` rows are written to stand alone in a flat list, so almost every
 * leaf repeats its own group: "Approval SM - Company Rebate Request" sits under a group
 * already titled "Company Rebate", "Generate Rebate - Generate Company Rebate" under
 * "Generate Company Rebate". In a 201px rail that echo eats the half of the line that
 * carries the actual distinction, and the ellipsis lands on the wrong end.
 *
 * DISPLAY ONLY — the DB is untouched, `title` still carries the full name on hover, and
 * search keeps matching (and showing) the complete label.
 *
 * Deliberately narrow: it fires only on a literal " - ", and only when the two names
 * genuinely overlap in one of the two shapes the legacy data actually uses:
 *
 *   1. tail STARTS WITH the parent  — "Approval SM - Company Rebate Request" / "Company Rebate"
 *   2. parent CONTAINS the tail     — "Review PM - Complain Handling" / "Review Complain Handling"
 *
 * Shape 2 needs its own branch because the echo is interleaved rather than suffixed, and it
 * carries a 6-character floor: a two-letter tail like "PM" turns up inside plenty of unrelated
 * parents, and stripping on that would delete real words.
 *
 * ⚠️ Shape 1 removes ONLY the echoed part and keeps whatever follows it. That remainder is
 * not decoration — it is frequently the entire distinction between two sibling menus
 * ("View Request All - Complain Handling" vs "… (Read Only)"). Dropping the whole tail
 * produced 6 pairs of identically-named siblings across the live tree; keeping the
 * remainder drops that to 0 while still shortening 114 of 469 labels.
 *
 * Everything else is left alone. "Bank Division" under "Division" has no " - " at all — the
 * word there is part of the name, not an echo. "Vehicle Request - Review GA" under "Vehicle -
 * Service" repeats nothing its parent actually says, so it keeps its full label.
 */
function shortLabel(label, parentLabel) {
    if (typeof label !== 'string' || !parentLabel) return label
    const cut = label.indexOf(' - ')
    if (cut <= 0) return label
    const head = label.slice(0, cut).trim()
    if (!head) return label
    const tailRaw = label.slice(cut + 3).trim()
    const tail = tailRaw.toLowerCase()
    const parent = String(parentLabel).trim().toLowerCase()

    if (tail.startsWith(parent)) {
        const rest = tailRaw.slice(parent.length).trim()

        return rest ? `${head} ${rest}` : head
    }

    return tail.length >= 6 && parent.includes(tail) ? head : label
}

/**
 * Display labels for one sibling list, with the collision guard.
 *
 * Shortening is decided per GROUP, not per item, because it can only be judged against the
 * siblings: four groups in this menu tree hold two rows that shorten to the same words
 * ("Approval SM" twice under Company Rebate, "View All" twice under Complain & Returns,
 * …). Two menus that read identically are worse than a truncated one — you cannot tell
 * which is which at all — so a collision reverts BOTH of them to their full labels.
 * Measured against the live tree: 87 of 469 labels shorten, 4 collisions revert.
 */
function displayLabels(children, parentLabel) {
    const shorts = children.map((c) => shortLabel(c.label, parentLabel))
    const seen = new Map()
    shorts.forEach((s) => seen.set(s, (seen.get(s) || 0) + 1))

    return shorts.map((s, i) => (seen.get(s) > 1 ? children[i].label : s))
}

// Emphasize the matched substring inside a nav label (string labels only).
function highlightLabel(label, rawQuery) {
    const q = (rawQuery || '').trim()
    if (!q || typeof label !== 'string') return label
    const idx = label.toLowerCase().indexOf(q.toLowerCase())
    if (idx === -1) return label
    return (
        <>
            {label.slice(0, idx)}
            <mark className="bg-transparent font-bold text-primary">{label.slice(idx, idx + q.length)}</mark>
            {label.slice(idx + q.length)}
        </>
    )
}

// ──────────────────────────────────────────────────────────────
// TREE — connector-line navigation inside each section
// (mirrors proto/layout.css: vertical line + per-item horizontal stub;
//  first level uses left:-13/w:9, nested .tree-children uses left:-14/w:10)
// ──────────────────────────────────────────────────────────────

function TreeGroup({ node, activeKey, collapsed, nested = false, depth = 0, displayLabel, onNavigate, forceOpen = false, query = '' }) {
    // Computed once per group so the collision guard can see the whole sibling list.
    const childLabels = useMemo(() => displayLabels(node.children, node.label), [node.children, node.label])
    const isActiveBranch = useMemo(
        () => containsActiveKey(node.children, activeKey),
        [node.children, activeKey],
    )
    // Seed open when the branch holds the active route, but let the user's toggle win
    // afterwards — otherwise the chevron is a dead no-op for the active group.
    const groupTotal = useBadgeTotal(node)
    const [expanded, setExpanded] = useState(() => Boolean(node.defaultExpanded) || isActiveBranch)
    useEffect(() => {
        if (isActiveBranch) setExpanded(true)
    }, [isActiveBranch])
    // `forceOpen` (search results) always wins so every match stays visible.
    const open = forceOpen || expanded

    // Connector stub offset differs by depth (first-level vs inside .tree-children).
    const stubClass = nested
        ? 'before:left-[-14px] before:w-[10px]'
        : 'before:left-[-13px] before:w-[9px]'

    // ── Level 3+ groups: CAPTION, and the indent stops growing here ──────────
    // Every nesting level costs 21px (ml-[7px] + pl-[14px]) out of a 280px rail. By
    // the 4th level a label is down to ~22 characters and truncates — the font never
    // shrinks (12.5px from level 2 down), the WIDTH does. So instead of nesting again,
    // the group renders as a small caption and its children stay on the caption's own
    // indent.
    // Non-collapsible on purpose: exactly ONE group in the whole menu tree is this deep
    // (#400 Generate Company Rebate, 10 children), and it only renders while its parent
    // group is already open — so nothing ends up unreachable. If a second such group is
    // ever added with many children, give this branch its own collapse first.
    if (depth >= 1) {
        return (
            <div className="flex flex-col">
                <div
                    className={`flex items-center gap-2 px-[10px] pb-0.5 pt-2.5 text-[10px] font-bold uppercase tracking-[0.06em] ${
                        isActiveBranch ? 'text-primary' : 'text-text-placeholder'
                    }`}
                >
                    <span className="min-w-0 flex-1 truncate">
                        {highlightLabel(query ? node.label : (displayLabel ?? node.label), query)}
                    </span>
                    {/* No rollup badge here on purpose: this caption branch is the
                        non-collapsible one, so its leaves are ALWAYS visible with their own
                        numbers. A total beside them would read as double-counting. */}
                </div>
                {/* Hairline bracket marking where the group ENDS. Without it a sibling that
                    sorts after this group reads as one of its members — the caption opens a
                    group but nothing closed it. Drawn as an absolute 1px line inside the
                    children's existing px-[10px], so it costs ZERO horizontal space. */}
                <div className="relative before:absolute before:bottom-0 before:left-[3px] before:top-0 before:w-px before:bg-border-strong before:content-['']">
                    {node.children.map((child, i) => (
                        <TreeNode key={child.key ?? child.href} node={child} activeKey={activeKey} collapsed={collapsed} nested={nested} depth={depth + 1} displayLabel={childLabels[i]} onNavigate={onNavigate} forceOpen={forceOpen} query={query} />
                    ))}
                </div>
            </div>
        )
    }

    return (
        <div className="flex flex-col">
            <button
                type="button"
                aria-expanded={open}
                onClick={() => setExpanded((v) => !v)}
                className={`relative flex items-center w-full border-0 rounded-lg bg-transparent text-left cursor-pointer gap-2 min-h-[32px] px-[10px] py-[5px] text-[12.5px] justify-between transition-colors duration-150
                    before:absolute before:top-1/2 before:border-t before:content-[''] ${stubClass}
                    ${isActiveBranch
                        ? 'text-primary-hover font-semibold before:border-primary'
                        : 'text-text-body font-medium before:border-border-strong hover:bg-sidebar-hover hover:text-text-heading'}`}
            >
                <span className="flex-1 whitespace-nowrap overflow-hidden text-ellipsis">{highlightLabel(query ? node.label : (displayLabel ?? node.label), query)}</span>
                {/* Only while closed — open, the leaves below carry their own numbers. */}
                {!open && <NavBadge count={groupTotal} muted />}
                <ChevronDown
                    size={14}
                    className={`flex-shrink-0 transition-transform duration-300 ${open ? 'rotate-0' : '-rotate-90'} ${isActiveBranch ? 'text-primary' : 'text-text-placeholder'}`}
                />
            </button>
            <div
                className={`overflow-hidden relative border-l border-border-strong ml-[7px] pl-[14px] transition-[max-height] duration-300 ${open ? 'max-h-[600px]' : 'max-h-0'}`}
            >
                {node.children.map((child, i) => (
                    <TreeNode key={child.key ?? child.href} node={child} activeKey={activeKey} collapsed={collapsed} nested depth={depth + 1} displayLabel={childLabels[i]} onNavigate={onNavigate} forceOpen={forceOpen} query={query} />
                ))}
            </div>
        </div>
    )
}

function TreeNode({ node, activeKey, collapsed, nested = false, depth = 0, displayLabel, onNavigate, forceOpen = false, query = '' }) {
    // Both called before the branch below so the hook order is identical on both paths.
    const badgeCount = useBadgeTotal(node)
    const pendingMigration = useIsMigrationPending(node.href)

    if (node.children) {
        return <TreeGroup node={node} activeKey={activeKey} collapsed={collapsed} nested={nested} depth={depth} displayLabel={displayLabel} onNavigate={onNavigate} forceOpen={forceOpen} query={query} />
    }
    const isActive = Boolean(activeKey) && nodeKey(node) === activeKey
    // Stub line connector: nested vs first-level differ by left offset
    const stubClass = nested
        ? "before:left-[-14px] before:w-[10px]"
        : "before:left-[-13px] before:w-[9px]"
    const stubColor = isActive ? "before:border-primary" : "before:border-border-strong"

    return (
        <Link
            href={node.href || '#'}
            onClick={onNavigate}
            tabIndex={collapsed ? -1 : 0}
            title={node.label}
            className={`relative flex items-center w-full border-0 rounded-lg bg-transparent text-left no-underline cursor-pointer gap-2 min-h-[32px] px-[10px] py-[5px] text-[12.5px] transition-colors duration-150
                before:absolute before:top-1/2 before:border-t before:content-[''] ${stubClass} ${stubColor}
                ${isActive
                    ? 'bg-primary-light text-primary-hover font-semibold'
                    : 'text-text-body font-medium hover:bg-sidebar-hover hover:text-text-heading'}`}
        >
            <span className="min-w-0 flex-1 truncate">{highlightLabel(query ? node.label : (displayLabel ?? node.label), query)}</span>
            {pendingMigration && <MigrationPendingMark />}
            <NavBadge count={badgeCount} />
        </Link>
    )
}

// ──────────────────────────────────────────────────────────────
// NAV SECTION — top-level section with icon + label + chevron
// ──────────────────────────────────────────────────────────────

function NavSection({ section, activeKey, expandedKey, onHeaderClick, collapsed, onNavigate, forceOpen = false, query = '' }) {
    const hasChildren = Array.isArray(section.children) && section.children.length > 0
    // `forceOpen` (search results) expands every matching section at once, overriding the
    // usual single-open accordion. Still gated on !collapsed (search is hidden on the rail).
    const isExpanded = !collapsed && (forceOpen || expandedKey === section.key)
    const hasActive = containsActiveKey(section.children, activeKey)
    // Display labels for this section's direct children — computed here, not per child,
    // so the collision guard in displayLabels() can see the whole sibling list.
    const sectionChildLabels = useMemo(
        () => displayLabels(section.children || [], section.label),
        [section.children, section.label],
    )
        || (Boolean(activeKey) && !hasChildren && nodeKey(section) === activeKey)

    const icon = resolveIcon(section.icon)
    const tooltipLabel = section.label
    const sectionTotal = useBadgeTotal(section)

    // Collapsed-rail hover tooltip. The nav scroll container is `overflow-x-hidden`,
    // so an absolutely-positioned tooltip anchored to the right of the rail gets
    // clipped and never shows. Render it `fixed` (escapes any clipping), positioned
    // from the hovered button's rect — instant, unlike the ~1s native `title` delay.
    const [tip, setTip] = useState(null)
    const showTip = (e) => {
        if (!collapsed) return
        const r = e.currentTarget.getBoundingClientRect()
        setTip({ top: r.top + r.height / 2, left: r.right + 12 })
    }
    const hideTip = () => setTip(null)

    // Header button (or Link if section is a direct link). rounded-[10px] matches
    // the proto `.nav-section-header` panel radius (--radius-panel: 10px).
    const headerBase = `relative flex items-center w-full border-0 rounded-[10px] bg-transparent font-medium text-left cursor-pointer transition-colors duration-150
        ${hasActive ? 'text-primary-hover' : 'text-text-body hover:bg-sidebar-hover hover:text-text-heading'}`
    const headerSizing = collapsed
        ? 'justify-center min-h-0 mx-auto p-0 w-11 h-11'
        : 'gap-3 min-h-[42px] px-3 text-[13px] justify-start'
    const headerFinal = `${headerBase} ${headerSizing} group/navheader`

    const navIcon = (
        <span
            aria-hidden="true"
            className={`relative inline-grid place-items-center w-6 h-6 flex-shrink-0 transition-colors duration-150 ${hasActive ? 'text-primary' : 'text-text-muted group-hover/navheader:text-text-heading'}`}
        >
            {icon}
            {/* On the collapsed rail there is no room for a number, so pending work shows
                as a presence dot. The count itself is one click (or one un-collapse) away. */}
            {collapsed && sectionTotal > 0 && (
                <span className="absolute -right-0.5 -top-0.5 h-[7px] w-[7px] rounded-full bg-primary" />
            )}
        </span>
    )

    // Fixed-positioned so it escapes the nav's `overflow-x-hidden` scroll clip.
    // Surface chip (bg-card + border + shadow), NOT a solid dark slab: it sits on top of
    // page content, and a black block read as colliding with the cards underneath. Using
    // tokens also makes it follow light/dark automatically.
    const tooltip = collapsed && tip ? (
        <span
            role="tooltip"
            style={{ position: 'fixed', top: tip.top, left: tip.left, transform: 'translateY(-50%)' }}
            className="pointer-events-none z-[99999] whitespace-nowrap rounded-lg border border-border bg-card px-2.5 py-1.5 text-[11.5px] font-semibold text-foreground shadow-lg"
        >
            {tooltipLabel}
        </span>
    ) : null;

    const tooltipPortal = tooltip ? createPortal(tooltip, document.body) : null;

    // Sections without children render as a flat link
    if (!hasChildren && section.href) {
        return (
            <div className="flex flex-col" data-section={section.key}>
                <Link href={section.href} onClick={onNavigate} onMouseEnter={showTip} onMouseLeave={hideTip} tabIndex={collapsed ? -1 : 0} className={headerFinal}>
                    {navIcon}
                    {!collapsed && (
                        <>
                            <span className="flex-1 min-w-0 whitespace-nowrap overflow-hidden text-ellipsis">{highlightLabel(section.label, query)}</span>
                            <NavBadge count={sectionTotal} />
                        </>
                    )}
                </Link>
                {tooltipPortal}
            </div>
        )
    }

    return (
        <div className="flex flex-col" data-section={section.key}>
            <button
                type="button"
                tabIndex={collapsed ? -1 : 0}
                aria-expanded={isExpanded}
                onClick={() => onHeaderClick(section.key)}
                onMouseEnter={showTip}
                onMouseLeave={hideTip}
                className={headerFinal}
            >
                {navIcon}
                {!collapsed && (
                    <>
                        <span className="flex-1 min-w-0 whitespace-nowrap overflow-hidden text-ellipsis">{highlightLabel(section.label, query)}</span>
                        {/* Closed only — expanded, the rows beneath carry their own numbers. */}
                        {!isExpanded && <NavBadge count={sectionTotal} muted />}
                        <span
                            aria-hidden="true"
                            className={`inline-grid place-items-center w-4 h-4 text-text-placeholder flex-shrink-0 transition-transform duration-300 ${isExpanded ? 'rotate-0' : '-rotate-90'}`}
                        >
                            <ChevronDown size={14} className="block" />
                        </span>
                    </>
                )}
            </button>
            {tooltipPortal}
            {!collapsed && (
                <div
                    className={`overflow-hidden transition-[max-height] duration-300 ${isExpanded ? 'max-h-[1200px]' : 'max-h-0'}`}
                >
                    <nav
                        className="flex flex-col relative gap-0.5 pt-1 pb-2 pl-9
                            before:absolute before:left-[23px] before:top-1 before:bottom-2 before:w-px before:bg-border-strong before:content-['']"
                    >
                        {section.children.map((child, i) => (
                            <TreeNode key={child.key ?? child.href} node={child} activeKey={activeKey} collapsed={collapsed} displayLabel={sectionChildLabels[i]} onNavigate={onNavigate} forceOpen={forceOpen} query={query} />
                        ))}
                    </nav>
                </div>
            )}
        </div>
    )
}

// ──────────────────────────────────────────────────────────────
// SIDEBAR — shell + header + nav + footer
// ──────────────────────────────────────────────────────────────

export default function Sidebar({ collapsed, onToggleCollapse, onToggleTheme, activeHref }) {
    const { props } = usePage()
    const user = props.auth?.user
    const settingsEntries = props.settingsEntries ?? []
    // {left, bottom} of the trigger while the popup is open; null when closed.
    const [settingsAnchor, setSettingsAnchor] = useState(null)
    const settingsTriggerRef = useRef(null)
    // Stable identity: SettingsPopup's outside-click effect depends on this, and an
    // inline arrow here would re-add both document listeners on every Sidebar render
    // while the popup is open (NotificationBell's `close` uses the same useCallback([])
    // shape for the same reason).
    const closeSettings = useCallback(() => setSettingsAnchor(null), [])
    const userName = user?.Nama || user?.Username || 'Sales'
    const userInitial = useMemo(() => {
        if (user?.AstSalesCode) return user.AstSalesCode
        if (!userName) return 'SA'
        const parts = userName.trim().split(/\s+/)
        if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase()
        return userName.slice(0, 2).toUpperCase()
    }, [user, userName])
    const userSub = user?.PositionName || 'Sales'

    const sections = useMemo(() => Array.isArray(props.navItems) ? props.navItems : [], [props.navItems])
    // The prototype links are hardcoded, not DB-driven, so revoking a rolemenu grant cannot
    // hide them — the server decides, via the same predicate that 404s /proto/* in production
    // (App\Support\Prototype). Absent prop => hidden: a link that leads to a 404 is worse than
    // a missing one, and this prop rides every full visit.
    const protoSections = useMemo(
        () => (props.showPrototypeNav === true ? prototypeNav : []),
        [props.showPrototypeNav],
    )
    // DB-driven sections + the static prototype sections share one accordion.
    const allSections = useMemo(() => [...sections, ...protoSections], [sections, protoSections])
    const { show } = useToast()
    // A Set, and null when there is nothing pending, so the common case is one identity
    // check per leaf rather than a lookup.
    const migrationPending = useMemo(() => {
        const list = props.migrationPending
        return Array.isArray(list) && list.length > 0 ? new Set(list) : null
    }, [props.migrationPending])

    // Resolve the URL to a single owning leaf ONCE, here, then hand that key down. Every
    // level (section header, nested group, leaf) then answers "am I / do I contain the
    // winner?" against the same answer, so they can never disagree.
    const activeKey = useMemo(() => resolveActiveKey(allSections, activeHref), [allSections, activeHref])

    // The section that owns the current route. Mirrors NavSection's `hasActive` logic
    // (subtree OR the section's own direct link) so flat top-level sections match too.
    const activeSectionKey = useMemo(
        () =>
            allSections.find(
                (s) => containsActiveKey(s.children, activeKey)
                    || (Boolean(activeKey) && !s.children?.length && nodeKey(s) === activeKey),
            )?.key ?? null,
        [allSections, activeKey],
    )

    const [expandedKey, setExpandedKey] = useState(() => activeSectionKey ?? sections[0]?.key ?? protoSections[0]?.key ?? null)

    // Client-side page search — filters both the DB nav and the prototype nav by label.
    const [query, setQuery] = useState('')
    const searching = query.trim().length > 0
    const dbNav = useMemo(() => filterNav(sections, query), [sections, query])
    const protoNav = useMemo(() => filterNav(protoSections, query), [protoSections, query])
    const noResults = searching && dbNav.length === 0 && protoNav.length === 0

    // Sidebar sits in a persistent layout, so it never remounts on navigation. Re-open the
    // section containing the newly-active route when it changes, without overriding a manual
    // collapse/switch the user made within the same route.
    const prevActiveSectionKey = useRef(activeSectionKey)
    useEffect(() => {
        if (activeSectionKey && activeSectionKey !== prevActiveSectionKey.current) {
            setExpandedKey(activeSectionKey)
        }
        prevActiveSectionKey.current = activeSectionKey
    }, [activeSectionKey])

    const handleHeaderClick = (key) => {
        if (collapsed) {
            onToggleCollapse?.(false)
            setExpandedKey(key)
        } else {
            setExpandedKey((prev) => (prev === key ? null : key))
        }
    }

    // Auto-hide: collapse the sidebar to its rail right after navigating to a chosen
    // leaf so the page gets the space. The rail is click-to-expand again.
    const handleNavigate = () => onToggleCollapse?.(true)

    const handleLogout = () => {
        if (typeof route === 'function') {
            router.post(route('logout'))
        } else {
            show('Logout route belum tersedia.')
        }
    }

    // Deferred prop — absent on the first render of every visit, then filled in by the
    // follow-up request. Falling back to a FROZEN shared object (not a fresh {}) keeps the
    // context value referentially stable, so arriving badges re-render the tree once
    // instead of every nav item re-rendering on every unrelated page visit.
    const badges = useMemo(
        () => (props.navBadges && typeof props.navBadges === 'object' ? props.navBadges : EMPTY_BADGES),
        [props.navBadges],
    )

    // Deferred like navBadges, so it is genuinely ABSENT for one round trip after every full
    // visit. Passed through undefined-and-all rather than coerced with `|| 0`: NotificationBell
    // tells "not here yet" apart from "the server says zero", and collapsing the two made the
    // badge blink off on every navigation before coming back (fixed 2026-08-10).
    //
    // Since 2026-08-09 this counts UNREAD INBOX EVENTS, not the sum of the sidebar badges.
    // The two are different numbers on purpose: a badge counts work you must DO and can only
    // reach zero by doing it, while this counts news you have not READ. Do not add them.
    const bellCount = typeof props.bellCount === 'number' ? props.bellCount : undefined

    return (
        <NavBadgeContext.Provider value={badges}>
        <aside
            id="sidebar"
            aria-label="Main navigation"
            tabIndex={collapsed ? -1 : 0}
            // Collapsed rail is click-to-expand (NewUI behaviour) — clicking any empty
            // area or icon re-opens the sidebar; inner controls stopPropagation as needed.
            onClick={() => { if (collapsed) onToggleCollapse?.(false) }}
            className={`sticky top-0 h-screen flex flex-col bg-surface border-r border-border overflow-visible z-30 ${collapsed ? 'cursor-pointer' : ''}`}
        >
            {/* Header — brand + collapse toggle */}
            <div
                className={`flex items-center gap-2 border-b border-border flex-shrink-0 h-16 ${collapsed ? 'justify-center px-[10px]' : 'justify-between px-[14px]'}`}
            >
                <div className="flex items-center min-w-0 gap-2.5">
                    <span
                        className="inline-grid place-items-center rounded-[12px] font-extrabold flex-shrink-0 w-[38px] h-[38px] text-[13px] text-white uppercase"
                        style={{ background: 'linear-gradient(135deg, #9333ea, var(--color-primary))' }}
                        title={userName}
                    >
                        {userInitial}
                    </span>
                    {!collapsed && (
                        <div className="min-w-0 overflow-hidden">
                            <strong className="block font-extrabold text-text-heading truncate text-[13px] tracking-tight leading-tight" title={userName}>
                                {userName}
                            </strong>
                            <small className="block font-semibold uppercase text-text-muted truncate text-[9.5px] tracking-[0.06em] leading-[1.4]" title={userSub}>
                                {userSub}
                            </small>
                        </div>
                    )}
                </div>
                {!collapsed && <NotificationBell count={bellCount} collapsed={false} onNavigate={handleNavigate} />}
                {!collapsed && (
                    <button
                        type="button"
                        aria-label="Collapse sidebar"
                        aria-expanded={!collapsed}
                        onClick={(e) => { e.stopPropagation(); onToggleCollapse?.(!collapsed) }}
                        className="inline-grid place-items-center border-0 rounded-lg bg-transparent text-text-muted cursor-pointer flex-shrink-0 w-[30px] h-[30px] transition-colors duration-150 hover:bg-sidebar-hover hover:text-primary"
                    >
                        <ChevronsLeft className="transition-transform duration-300" />
                    </button>
                )}
            </div>

            {/* Collapsed rail has no room for the bell beside the avatar (38px + 30px is
                wider than the rail), so it sits on its own row underneath — the same place
                the legacy app put it. The sidebar auto-collapses after navigating, so this
                is the state users see most; dropping the bell here would hide it in normal use. */}
            {collapsed && (
                <div className="flex flex-shrink-0 justify-center pt-2">
                    <NotificationBell count={bellCount} collapsed onNavigate={handleNavigate} />
                </div>
            )}

            {/* Search — filter pages by name (hidden on the collapsed rail) */}
            {!collapsed && (
                <div className="flex-shrink-0 px-[10px] pt-3 pb-1">
                    <div className="relative">
                        <span className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-text-placeholder" aria-hidden="true">
                            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></svg>
                        </span>
                        <input
                            type="search"
                            value={query}
                            onChange={(e) => setQuery(e.target.value)}
                            placeholder="Search pages…"
                            aria-label="Search pages"
                            className="w-full rounded-full border border-transparent bg-sidebar-hover py-2 pl-9 pr-8 text-[12.5px] text-text-body outline-none transition-colors placeholder:text-text-placeholder hover:bg-surface-tint focus:border-primary/50 focus:bg-surface [&::-webkit-search-cancel-button]:hidden"
                        />
                        {searching && (
                            <button
                                type="button"
                                onClick={() => setQuery('')}
                                aria-label="Clear search"
                                className="absolute right-2 top-1/2 grid -translate-y-1/2 place-items-center text-text-placeholder transition-colors hover:text-text-heading"
                            >
                                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
                            </button>
                        )}
                    </div>
                </div>
            )}

            {/* Nav — section accordion */}
            <nav className="flex flex-col overflow-y-auto overflow-x-hidden min-h-0 flex-1 px-[10px] py-3 gap-1">
                <MigrationPendingContext.Provider value={migrationPending}>
                {dbNav.map((section) => (
                    <NavSection
                        key={section.key}
                        section={section}
                        activeKey={activeKey}
                        expandedKey={expandedKey}
                        onHeaderClick={handleHeaderClick}
                        collapsed={collapsed}
                        onNavigate={handleNavigate}
                        forceOpen={searching}
                        query={query}
                    />
                ))}

                {/* Prototype (NewUI port) — static dummy pages, below the DB nav. */}
                {protoNav.length > 0 && (collapsed ? (
                    <div className="mx-auto my-2 h-px w-7 bg-border" aria-hidden="true" />
                ) : (
                    <p className="px-3 pt-4 pb-1 text-[10px] font-bold uppercase tracking-wider text-text-placeholder">
                        Prototype (dummy)
                    </p>
                ))}
                {protoNav.map((section) => (
                    <NavSection
                        key={section.key}
                        section={section}
                        activeKey={activeKey}
                        expandedKey={expandedKey}
                        onHeaderClick={handleHeaderClick}
                        collapsed={collapsed}
                        onNavigate={handleNavigate}
                        forceOpen={searching}
                        query={query}
                    />
                ))}

                </MigrationPendingContext.Provider>

                {noResults && (
                    <p className="px-3 py-6 text-center text-[12px] text-text-placeholder">
                        No pages match “{query.trim()}”.
                    </p>
                )}
            </nav>

            {/* Footer — Settings / Theme / Logout.
                Settings renders ONLY when the server offered this user at least one entry
                (`settingsEntries`, from App\Support\Settings\SettingsRegistry). That is the whole
                fix for GH #380 entry 1: the button used to be a hardcoded control with nothing
                behind it, so it was removed. Now it cannot exist without a destination, because
                the same registry call answers the route's 403. Do not hardcode an entry here. */}
            <div
                onClick={(e) => e.stopPropagation()}
                className={`flex-shrink-0 flex flex-col border-t border-border gap-0.5 ${collapsed ? 'px-[10px] py-[10px]' : 'px-[10px] pt-[10px] pb-[14px]'}`}
            >
                {settingsEntries.length > 0 && (
                    <div ref={settingsTriggerRef}>
                        <NavFlat
                            icon={<Settings />}
                            label="Settings"
                            collapsed={collapsed}
                            onClick={(e) => {
                                const r = e.currentTarget.getBoundingClientRect()
                                setSettingsAnchor((open) => (open ? null : {
                                    left: r.right + 12,
                                    bottom: window.innerHeight - r.bottom,
                                }))
                            }}
                        />
                    </div>
                )}
                <NavFlat
                    icon={
                        <>
                            <span className="dark:hidden"><Sun /></span>
                            <span className="hidden dark:block"><Moon /></span>
                        </>
                    }
                    label={
                        <>
                            <span className="dark:hidden">Light mode</span>
                            <span className="hidden dark:block">Dark mode</span>
                        </>
                    }
                    onClick={onToggleTheme}
                    ariaLabel="Toggle dark mode"
                    collapsed={collapsed}
                    tooltipLabel="Toggle theme"
                />
                <NavFlat
                    icon={<LogOut />}
                    label="Logout"
                    onClick={handleLogout}
                    collapsed={collapsed}
                    danger
                />
            </div>

            {settingsAnchor && (
                <SettingsPopup
                    entries={settingsEntries}
                    anchor={settingsAnchor}
                    triggerRef={settingsTriggerRef}
                    onClose={closeSettings}
                />
            )}
        </aside>
        </NavBadgeContext.Provider>
    )
}

// ──────────────────────────────────────────────────────────────
// NavFlat — footer action button (Theme/Logout)
// ──────────────────────────────────────────────────────────────

function NavFlat({ icon, label, onClick, ariaLabel, collapsed, danger = false, tooltipLabel }) {
    const tipText = tooltipLabel ?? (typeof label === 'string' ? label : '')

    // Same fixed-position hover tip as NavSection. The previous CSS-only variant
    // (group-hover/navflat on an absolutely-positioned span) never became visible,
    // so the footer icons had no labels on the collapsed rail.
    const [tip, setTip] = useState(null)
    const showTip = (e) => {
        if (!collapsed || !tipText) return
        const r = e.currentTarget.getBoundingClientRect()
        setTip({ top: r.top + r.height / 2, left: r.right + 12 })
    }
    const hideTip = () => setTip(null)
    const baseColors = danger
        ? 'text-danger hover:bg-danger-bg hover:text-danger-text'
        : 'text-text-body hover:bg-sidebar-hover hover:text-text-heading'
    const sizing = collapsed
        ? 'justify-center min-h-0 mx-auto p-0 w-11 h-11'
        : 'gap-3 min-h-[42px] px-3 text-[13px] justify-start'

    const navflatTooltip = collapsed && tip && tipText ? (
        <span
            role="tooltip"
            style={{ position: 'fixed', top: tip.top, left: tip.left, transform: 'translateY(-50%)' }}
            className="pointer-events-none z-[99999] whitespace-nowrap rounded-lg border border-border bg-card px-2.5 py-1.5 text-[11.5px] font-semibold text-foreground shadow-lg"
        >
            {tipText}
        </span>
    ) : null;
    const navflatTooltipPortal = navflatTooltip ? createPortal(navflatTooltip, document.body) : null;

    return (
        <div className="relative">
            <button
                type="button"
                tabIndex={collapsed ? -1 : 0}
                aria-label={ariaLabel}
                onClick={onClick}
                onMouseEnter={showTip}
                onMouseLeave={hideTip}
                className={`group/navflat relative flex items-center w-full border-0 rounded-[10px] bg-transparent font-medium text-left cursor-pointer transition-colors duration-150 ${baseColors} ${sizing}`}
            >
                <span
                    aria-hidden="true"
                    className={`inline-grid place-items-center w-6 h-6 flex-shrink-0 transition-colors duration-150 ${danger ? 'text-danger' : 'text-text-muted group-hover/navflat:text-text-heading'}`}
                >
                    {icon}
                </span>
                {!collapsed && (
                    <span className="flex-1 min-w-0 whitespace-nowrap overflow-hidden text-ellipsis">{label}</span>
                )}
            </button>
            {navflatTooltipPortal}
        </div>
    )
}