import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'
import { ChevronDown, Circle, type LucideIcon } from 'lucide-react'
import * as Icons from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext'
import { TopBar } from '@/components/TopBar'
import type { MenuNode } from '@/types/menu'
import { cn } from '@/lib/cn'

function getIcon(name: string | null): LucideIcon {
  if (!name) return Circle
  const candidate = (Icons as unknown as Record<string, LucideIcon>)[name]
  return candidate ?? Circle
}

const TYPE_CODE_TO_INT: Record<string, number> = {
  ThauPhu: 1,
  GiaoKhoan: 2,
  NhaCungCap: 3,
  DichVu: 4,
  MuaBan: 5,
  NguyenTacNcc: 6,
  NguyenTacDv: 7,
}

// Reverse lookup int → code (cho auto-expand từ URL ?type=)
const INT_TO_TYPE_CODE: Record<number, string> = Object.fromEntries(
  Object.entries(TYPE_CODE_TO_INT).map(([k, v]) => [v, k]),
)

// [YC-029 W1 S190] ALLOWLIST pathname được phép auto-expand nhóm Ct_ từ `?type=`.
// 🔴 EXACT match, KHÔNG `startsWith('/contracts')`: `/contracts/workflow-matrix?type=3`
//    mang `type` = ApplicableType (=3 cho CẢ 7 loại HĐ), KHÔNG phải ContractType ⇒
//    startsWith sẽ tái sinh bug A (mở nhầm nhóm "Nhà cung cấp" cho mọi leaf Luồng duyệt).
//    Cùng lý do, `/hard-copies?type=1..7` (`Hdc_*` :70-76) nằm NGOÀI allowlist: nó có
//    nhóm menu riêng, để lọt vào đây là cross-talk mở nhóm HĐ khi user đang ở Bản cứng.
const CT_TYPE_PARAM_PATHS = ['/my-contracts', '/contracts/new', '/inbox'] as const

// Detect Ct_<Code> group key (no suffix, distinguish from leaf Ct_<Code>_List/Create/Pending)
const CT_GROUP_PATTERN = /^Ct_([^_]+)$/
function getCtGroupCode(key: string): string | null {
  const m = key.match(CT_GROUP_PATTERN)
  return m ? m[1] : null
}

// Pe_<Code> group key (Duyệt NCC / Duyệt NCC và Giải pháp). Riêng accordion
// để Pe_* mutex với nhau (không mix với Ct_*) — user có thể expand 1 Ct_ HĐ
// + 1 Pe_ phiếu cùng lúc vì 2 module khác nhau.
const PE_GROUP_PATTERN = /^Pe_([^_]+)$/
function getPeGroupCode(key: string): string | null {
  const m = key.match(PE_GROUP_PATTERN)
  return m ? m[1] : null
}

// PE type code → int (mirror BE PurchaseEvaluationType enum)
const PE_CODE_TO_INT: Record<string, number> = { DuyetNcc: 1, DuyetNccPhuongAn: 2 }
const INT_TO_PE_CODE: Record<number, string> = Object.fromEntries(
  Object.entries(PE_CODE_TO_INT).map(([k, v]) => [v, k]),
)

// User-side menu key → route. Differs from admin: Danh sách points to
// /my-contracts (user's own drafts).
// 🔴 [YC-029 W3 S190 — vá comment stale] Câu cũ "Duyệt to /inbox" HẾT ĐÚNG cho họ `Ct_*`:
// leaf "Đang duyệt" nay Ở LẠI `/my-contracts?type=N&pendingMe=1` (xem nhánh `Pending` :164).
// `/inbox` + `InboxPage` GIỮ NGUYÊN, đổi vai: hộp thư TỔNG mọi loại, vào bằng mục cố định
// "Hộp thư" (`USER_FIXED_TOP` / `staticResolvePath('__inbox')`), không còn là đích của leaf HĐ.
function resolvePath(key: string): string | null {
  const staticMap: Record<string, string> = {
    // [W2 KHKK S161] GĐ2 hết skeleton — 6 leaf re-point trang thật (gotcha #50:
    // thiếu route = sidebar drop SILENT; filter đi qua query param, page map FILTER_TO_PHASE).
    // [K4b S166 — spec K4b-7] 6 leaf nhóm 1 mang `group=1` TƯỜNG MINH. Chúng là 6 key
    // cũ KHÔNG-infix (S159, cấm đổi tên) nên không đi qua regex `Khkk_G{n}_*` bên dưới;
    // thiếu `group=1` ở đây thì "Danh sách" của nhóm 1 lại hiện phiếu của cả 8 nhóm —
    // đúng một mục nói dối giữa 8 mục nói thật. Trang coi "vắng group" = xem tất cả.
    Khkk_List: '/khkk/list?group=1',
    Khkk_Create: '/khkk/create?group=1',
    Khkk_Pending: '/khkk/list?group=1&filter=ChoDuyet',
    Khkk_Approved: '/khkk/list?group=1&filter=DaDuyet',
    // [W7 KHKK S161] GĐ4 hết skeleton — 7 leaf `Hdc_*` re-point trang thật
    // `/hard-copies?type=N` (N = ContractType int, mirror TYPE_CODE_TO_INT ở trên).
    // gotcha #50: thiếu/để sai route ở đây = leaf DROP IM LẶNG khỏi sidebar.
    Hdc_ThauPhu: '/hard-copies?type=1',
    Hdc_GiaoKhoan: '/hard-copies?type=2',
    Hdc_NhaCungCap: '/hard-copies?type=3',
    Hdc_DichVu: '/hard-copies?type=4',
    Hdc_MuaBan: '/hard-copies?type=5',
    Hdc_NguyenTacNcc: '/hard-copies?type=6',
    Hdc_NguyenTacDv: '/hard-copies?type=7',
    Dashboard: '/dashboard',
    Contracts: '/my-contracts',
    PurchaseEvaluations: '/purchase-evaluations',
    // [S159 2026-07-29] 2 root placeholder giai đoạn 2/4 (owner: menu hiển thị đúng 4 GĐ
    // như tab strip). Module chưa build → trỏ về Tổng quan pipeline (khung "Sắp triển khai").
    // LESSON gotcha #50: key DB mới PHẢI có route ở đây, không thì sidebar drop SILENT.
    KeHoachKyKet: '/khkk/list',   // [W2 S161] root GĐ2 hết placeholder — về danh sách phiếu
    HopDongCung: '/dashboard',
    // [W2 S161] 2 leaf còn lại — WfView đi alias matrix type=10 (App.tsx, không đẻ page mới)
    Khkk_WfView: '/khkk/workflow-matrix?type=10&group=1',
    Khkk_Deleted: '/khkk/list?group=1&view=deleted',
    // [Plan CA Hotfix 1 S29 2026-05-22] 4 master + 4 catalog leaf moved từ
    // fe-admin → fe-user. resolvePath PHẢI có route mapping nếu không
    // MenuLeaf line 238 `if (!path) return null` → sidebar drop silent.
    // Implementer Chunk B (06a441c) thêm Routes vào App.tsx + 4 page +
    // menuKeys.ts nhưng quên mirror staticMap resolvePath → bug UAT bro.
    Suppliers: '/master/suppliers',
    Projects: '/master/projects',
    Departments: '/master/departments',
    CatalogUnits: '/master/catalogs/units',
    CatalogMaterials: '/master/catalogs/materials',
    CatalogServices: '/master/catalogs/services',
    CatalogWorkItems: '/master/catalogs/work-items',
    // [K1 — S164, Mig 70] Danh mục Hợp đồng SP-002 — "chỗ thứ 4" Pattern 16-bis:
    // BE seed leaf `ContractCatalog` dưới group Catalogs, thiếu dòng này thì
    // resolvePath trả null → MenuLeaf DROP IM LẶNG khỏi sidebar (gotcha #50).
    ContractCatalog: '/master/catalogs/contract-catalog',
    // [Phase 10.1 G-H1 S33 2026-05-26] Module Hồ sơ Nhân sự (Mig 34). LESSON
    // Plan CA Hotfix 1 gotcha #50: PHẢI mirror staticMap khi thêm page mới
    // — nếu thiếu, MenuLeaf line ~250 `if (!path) return null` → sidebar drop silent.
    Hrm_HoSo: '/employees',
    // [Phase 10.2 G-H2 S35 2026-05-28] Cấu hình HRM 4 catalog leaf (Mig 35).
    // Pattern 16-bis 4-place mirror (staticMap = 4th place, dễ miss nhất).
    Hrm_Config_LeaveTypes: '/hrm/configs/leave-types',
    Hrm_Config_Holidays: '/hrm/configs/holidays',
    Hrm_Config_Shifts: '/hrm/configs/shifts',
    Hrm_Config_OtPolicies: '/hrm/configs/ot-policies',
    // [P11-C S51] danh mục xe công + tài xế — cùng page :kind-driven.
    Hrm_Config_Vehicles: '/hrm/configs/vehicles',
    Hrm_Config_Drivers: '/hrm/configs/drivers',
    // [Phase 10.2 G-O1 S34 2026-05-27] Module Văn phòng số — Danh bạ nội bộ.
    // 4-place mirror Pattern 16-bis: types/ + pages/ + App.tsx + menuKeys + staticMap.
    Off_Dashboard: '/office/dashboard',
    Off_DanhBa: '/directory',
    // [Phase 10.2 G-O2 S36 2026-05-28] Phòng họp Booking + Catalog (Mig 36).
    // Pattern 16-bis 4-place mirror 7× cumulative — staticMap = 4th place dễ miss.
    // View leaf + Book leaf đều trỏ calendar (user perspective); Manage trỏ catalog.
    Off_PhongHop_View: '/meeting-calendar',
    Off_PhongHop_Book: '/meeting-calendar',
    Off_PhongHop_Manage: '/meeting-rooms',
    Off_DeXuat_List: '/proposals',
    Off_DeXuat_Create: '/proposals/new',
    Off_DeXuat_Inbox: '/proposals?status=2&inboxOnly=true',
    // Phase 10.3-10.4 S38 — Workflow Apps skeleton (8 entries)
    Off_DonTu_Leave: '/workflow-apps/leave',
    Off_DonTu_Ot: '/workflow-apps/ot',
    Off_DonTu_Travel: '/workflow-apps/travel',
    Off_DatXe: '/workflow-apps/vehicle',
    Off_ItTicket: '/it-tickets',
    Off_ChamCong: '/attendance',
    Hrm_Dashboard: '/hr/dashboard',
  }
  if (staticMap[key]) return staticMap[key]

  // [S159-đợt5 owner "y chang Duyệt NCC"] +WfView/Approved/Deleted.
  // 🔴 [YC-029 W1 S190 — vá comment stale] Từ S188 HĐ có **7 bộ quy trình duyệt per-loại**
  // (`HD-TP`/`HD-GK`/`HD-NCC`/`HD-DV`/`HD-MB`/`HD-NT-NCC`/`HD-NT-DV` — nhà mã BE
  // `Domain/Contracts/ContractApprovalWorkflowFamily.cs:33-42`), KHÔNG còn "chung một bộ".
  // `type=3` ở URL WfView là **ApplicableType.Contract** (hằng cho cả họ), còn `ct=N` là
  // **danh-tính leaf** (ContractType 1..7): vừa cho active-state riêng từng leaf, vừa là
  // tham số trang WfView dùng để slice đúng bộ `HD-*` của loại đó.
  // ⚠️ `ct` (và `view` dưới đây) KHÔNG được cho vào `TRANSIENT_QUERY_KEYS` — xem chú ở đó.
  const match = key.match(/^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$/)
  if (match) {
    const [, code, action] = match
    const typeInt = TYPE_CODE_TO_INT[code]
    if (!typeInt) return null
    if (action === 'List') return `/my-contracts?type=${typeInt}`
    if (action === 'Create') return `/contracts/new?type=${typeInt}`
    // 🔴 [YC-029 W3 S190] "Đang duyệt" Ở LẠI trang danh sách thay vì nhảy sang `/inbox`
    // (anh: bấm mục con của loại HĐ mà bị đá sang hộp thư tổng = mất ngữ cảnh loại).
    // `pendingMe=1` là DANH-TÍNH leaf (KHÔNG vào `TRANSIENT_QUERY_KEYS` — xem chú ở đó),
    // trang tự ĐỔI ENDPOINT sang `GET /contracts/inbox` khi thấy nó.
    // GIỮ NGUYÊN route `/inbox` + `InboxPage`: đó là hộp thư TỔNG (mục cố định "Hộp thư"),
    // không xoá — đường vào khác, dữ liệu cùng nguồn.
    if (action === 'Pending') return `/my-contracts?type=${typeInt}&pendingMe=1`
    if (action === 'WfView') return `/contracts/workflow-matrix?type=3&ct=${typeInt}`
    // [YC-029 W1 S190] `view=approved` = KHOÁ ĐỊNH DANH (khuôn PE :185, H-4): `phase` nằm
    // trong TRANSIENT (là bộ lọc) nên URL "Đã duyệt" trùng hệt URL user tự lọc phase=9 trên
    // trang Danh sách ⇒ 2 leaf cùng sáng (bug B). Trang bỏ qua param lạ, chỉ `phase=9` lọc thật.
    if (action === 'Approved') return `/my-contracts?type=${typeInt}&phase=9&view=approved`
    if (action === 'Deleted') return `/my-contracts?type=${typeInt}&deleted=1`
  }

  // Pe_<Code>_<Action> cho module Duyệt NCC (user side)
  // [S155 đợt 2 — owner restructure menu 2026-07-27] +`Approved` +`Deleted`. Đây là
  // "chỗ thứ 4" của Pattern 16-bis (staticMap/resolvePath) — key seed ở BE mà quên nới
  // regex thì `resolvePath` trả null ⇒ MenuLeaf DROP IM LẶNG khỏi sidebar (gotcha #50).
  // 🔴 `WfView` GIỮ NGUYÊN chỉ ở fe-user (lead chốt đường (c)): fe-admin KHÔNG có
  // `WorkflowMatrixViewPage` nên thêm `WfView` bên đó sẽ đẻ link chết.
  const peMatch = key.match(/^Pe_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$/)
  if (peMatch) {
    const [, code, action] = peMatch
    const typeInt = PE_CODE_TO_INT[code]
    if (!typeInt) return null
    // [Plan AA S24 t1] "Luồng duyệt" leaf — read-only matrix view phía trên
    // Danh sách. User xem trước khi tạo phiếu ai duyệt step nào (filter
    // workflow IsUserSelectable=true admin ghim).
    if (action === 'WfView') return `/purchase-evaluations/workflow-matrix?type=${typeInt}`
    if (action === 'List') return `/purchase-evaluations?type=${typeInt}`
    // "Thao tác" leaf → workspace 2-panel (Q4 2026-05-07): pick + create + sửa
    // tables inline. Header-only `/new` page giữ tồn tại cho deep-link cũ
    // (PeDetailTabs "Sửa header" button vẫn navigate sang đó).
    if (action === 'Create') return `/purchase-evaluations/workspace?type=${typeInt}`
    if (action === 'Pending') return `/purchase-evaluations?type=${typeInt}&pendingMe=1`
    // [S155 đợt 2] 2 mục MỚI — CẢ HAI tái dùng `PurchaseEvaluationsListPage`, 0 route mới,
    // 0 page mới. "Đã duyệt" chỉ là filter phase sẵn có (`DaDuyet = 7`, đã thông 3 tầng);
    // "Đã xóa" là view riêng trong cùng page, gọi endpoint `/purchase-evaluations/deleted`.
    // 🔴 `view=approved` là KHOÁ ĐỊNH DANH, KHÔNG phải bộ lọc — trang bỏ qua nó, chỉ
    // `phase=7` mới thật sự lọc. Vì sao phải có: URL của mục "Đã duyệt" TRÙNG HỆT URL
    // mà user tự lọc trạng thái "Đã duyệt" trên trang Danh sách ⇒ không phân biệt được
    // bằng URL ⇒ menu sáng nhầm (bug UAT anh báo 2026-07-27). `phase` nằm trong
    // TRANSIENT (là bộ lọc) nên KHÔNG dùng làm danh tính được; `view` thì không.
    if (action === 'Approved') return `/purchase-evaluations?type=${typeInt}&phase=7&view=approved`
    if (action === 'Deleted') return `/purchase-evaluations?type=${typeInt}&deleted=1`
  }

  // ===================== [K4a — S166] Sidebar KHKK 8 NHÓM DUYỆT ======================
  // `Khkk_G{n}_{action}` — 42 leaf của nhóm 2..8 (BE `MenuKeys.KhkkLeaf` :77, sinh từ
  // `KhkkGroupNumbers = [2..8]` × `KhkkLeafActions` 6 hành động). Đây là "chỗ thứ 4"
  // Pattern 16-bis: BE seed 42 row mà thiếu nhánh này thì `resolvePath` trả null ⇒
  // MenuLeaf nhánh `if (!path) return null` (grep chuỗi đó — số dòng trượt theo edit) ⇒ 42 leaf DROP IM LẶNG (gotcha #50).
  //
  // 🔴 NHÓM 1 KHÔNG đi qua đây: 7 key cũ KHÔNG-infix (`Khkk_G1` + `Khkk_WfView` …
  //    `Khkk_Deleted`, seed S159) nằm trong `staticMap` bên trên và CẤM đổi tên
  //    (đổi = mồ côi permission-row đã cấp — bài S155). Vì thế regex dưới về mặt dữ
  //    liệu chỉ ăn n = 2..8; để dải `[1-8]` theo spec là CỐ Ý — rộng hơn tập seed một
  //    nấc, vô hại hôm nay và đúng sẵn nếu sau này nhóm 1 chuyển sang khuôn infix.
  //
  // 🔴 `?group=n` là DANH TÍNH điều hướng, không phải bộ lọc tuỳ hứng: nó KHÔNG nằm
  //    trong `TRANSIENT_QUERY_KEYS` (grep tên hằng — đừng neo số dòng) nên 6 leaf cùng nhóm và cùng leaf khác nhóm
  //    đều phân biệt được khi so khớp active (bài `view=approved` S155 ngay khối trên).
  //    Tên tham số `filter` / `view` / `group` lấy ĐÚNG từ `KhkkListPage` (grep
  //    `get('filter')` / `get('view')` / `get('group')` trong file đó — đừng neo số dòng,
  //    chính wave K4b đã làm số cũ trượt) — sai một chữ ở đây là leaf trỏ tới trang mở
  //    được nhưng lọc SAI, hỏng câm chứ không đỏ build.
  //
  // 🔴 fe-admin CỐ Ý KHÔNG nới regex này (lead chốt 2026-07-27, giữ lệch): fe-admin
  //    không có `WorkflowMatrixViewPage` (`App.tsx` bên đó chỉ có /khkk/list|create|:id)
  //    nên `WfView` bên admin sẽ là link chết; "Luồng duyệt" phía admin trỏ Designer.
  //    Khai lệch có chủ đích — xem ghi chú ràng buộc ngược tại `isAdminHidden` fe-admin.
  const khkkMatch = key.match(/^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$/)
  if (khkkMatch) {
    const [, group, action] = khkkMatch
    if (action === 'WfView') return `/khkk/workflow-matrix?type=10&group=${group}`
    if (action === 'List') return `/khkk/list?group=${group}`
    if (action === 'Create') return `/khkk/create?group=${group}`
    if (action === 'Pending') return `/khkk/list?group=${group}&filter=ChoDuyet`
    if (action === 'Approved') return `/khkk/list?group=${group}&filter=DaDuyet`
    if (action === 'Deleted') return `/khkk/list?group=${group}&view=deleted`
  }
  return null
}

// Menu entries not applicable to user app — filtered out client-side so the
// sidebar only shows what matters to a contract drafter/approver.
// [Plan CA S29 2026-05-22] REMOVED `Master, Suppliers, Projects, Departments`
// khỏi hidden set — "Cấu hình danh mục dùng chung" giờ ở fe-user/eoffice cho
// role CatalogManager (admin tạo + gán qua Permission Matrix). Catalogs (root
// + 4 leaf) tree-inherit từ Master nên auto-visible. Forms/Reports/System/
// Users/Roles/Permissions vẫn ẩn (admin tools only).
const USER_HIDDEN_KEYS = new Set([
  'System', 'Users', 'Roles', 'Permissions',
  'Forms', 'Reports',
])

function filterForUser(nodes: MenuNode[]): MenuNode[] {
  // Filter 2 tầng: hardcode USER_HIDDEN_KEYS (system-level, structural never-show)
  // + dynamic isVisible (Mig 27 admin toggle qua MenuVisibilityPage). isVisible
  // mặc định true, admin set false → ẩn khỏi sidebar eOffice.
  return nodes
    .filter(n => !USER_HIDDEN_KEYS.has(n.key) && n.isVisible !== false)
    .map(n => ({ ...n, children: filterForUser(n.children) }))
}

// Mig 27: ưu tiên displayLabel admin custom, fallback label gốc.
function effectiveLabel(n: { label: string; displayLabel?: string | null }): string {
  return (n.displayLabel && n.displayLabel.trim()) || n.label
}

// Accordion state cho groups Ct_<Code> (7 HĐ) + Pe_<Code> (2 phiếu) — mỗi
// family mutex độc lập. Auto-expand theo URL `?type=X` (Ct_ dùng route
// /contracts|/my-contracts|/inbox, Pe_ dùng /purchase-evaluations). Group
// không match prefix giữ behavior cũ (independent local useState).
type AccordionContextValue = {
  expandedCtCode: string | null
  setExpandedCtCode: (code: string | null) => void
  expandedPeCode: string | null
  setExpandedPeCode: (code: string | null) => void
}
const AccordionContext = createContext<AccordionContextValue>({
  expandedCtCode: null,
  setExpandedCtCode: () => {},
  expandedPeCode: null,
  setExpandedPeCode: () => {},
})

function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number }) {
  const hasChildren = node.children.length > 0
  if (hasChildren) return <MenuGroup node={node} depth={depth} />
  return <MenuLeaf node={node} depth={depth} />
}

function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
  const ctCode = getCtGroupCode(node.key)
  const peCode = getPeGroupCode(node.key)
  const accordion = useContext(AccordionContext)

  // Local state cho group thường (top-level "Hợp đồng" mặc định mở)
  const [localOpen, setLocalOpen] = useState(depth === 0)

  // Ct_<Code> / Pe_<Code> group: state controlled bởi accordion context,
  // mỗi family (Ct / Pe) mutex độc lập.
  const isCtAccordion = ctCode != null
  const isPeAccordion = peCode != null
  const isAccordion = isCtAccordion || isPeAccordion
  const open = isCtAccordion
    ? accordion.expandedCtCode === ctCode
    : isPeAccordion
      ? accordion.expandedPeCode === peCode
      : localOpen

  function toggle() {
    if (isCtAccordion) {
      accordion.setExpandedCtCode(open ? null : ctCode)
    } else if (isPeAccordion) {
      accordion.setExpandedPeCode(open ? null : peCode)
    } else {
      setLocalOpen(o => !o)
    }
  }

  const Icon = getIcon(node.icon)
  const isTopLevel = depth === 0

  return (
    <div>
      <button
        onClick={toggle}
        className={cn(
          'relative block w-full rounded-md text-left transition',
          isTopLevel
            ? 'px-3 py-2 pr-7 text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:bg-slate-100 whitespace-nowrap'
            : 'px-3 py-1.5 pr-7 text-[12px] font-medium leading-snug text-slate-600 hover:bg-slate-100 hover:text-slate-900',
          // Highlight Ct_ group đang active (accordion open) bằng tinted background
          isAccordion && open && 'bg-slate-50 text-slate-900',
        )}
      >
        {/* [Plan AA S24 t2 wrap fix] Bro UAT request: label dài 2 dòng phải về
            đầu hàng (under icon, KHÔNG indent sau icon). Pattern: inline-block
            icon + inline text → text wrap natural về left edge button.
            ChevronDown absolute right để KHÔNG bị đẩy xuống khi text wrap.
            Smaller text-[12px] + leading-snug compact 2-line height. */}
        <Icon className="mr-1.5 -mt-0.5 inline-block h-4 w-4 align-middle" />
        <span className="align-middle" title={effectiveLabel(node)}>{effectiveLabel(node)}</span>
        <ChevronDown className={cn(
          'absolute right-2 top-2 h-3.5 w-3.5 text-slate-400 transition',
          !open && '-rotate-90',
        )} />
      </button>
      {open && (
        <div className={cn(
          'mt-0.5 space-y-0.5',
          depth === 0 ? 'pl-2' : 'ml-3 mt-1 border-l border-slate-100 pl-3',
        )}>
          {node.children.map(c => (
            <MenuNodeRenderer key={c.key} node={c} depth={depth + 1} />
          ))}
        </div>
      )}
    </div>
  )
}

// Transient query keys — không phải "navigation identity", strip trước khi
// compare để menu giữ highlight khi user select row / search / filter.
// Ví dụ leaf "Danh sách" `?type=1` vẫn highlight khi user click phiếu →
// `?type=1&id=abc`. Trước đó exact-set match → mất highlight (bug UAT 2026-05-08).
//
// 🔴 [YC-029 W1 S190] CẤM thêm vào set dưới đây 3 param DANH-TÍNH của leaf HĐ: khoá-định-danh
// mục "Đã duyệt" (khuôn PE :185), khoá loại HĐ của leaf "Luồng duyệt", khoá "đến lượt tôi" của
// leaf "Đang duyệt" (W3). Cho vào set = bị strip khi so sánh ⇒ URL 2 leaf bằng nhau ⇒ menu
// sáng cùng lúc (đúng bug B anh báo). Chúng là DANH-TÍNH điều hướng, KHÔNG phải bộ lọc.
const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', 'awId'])

// So sánh 2 query string dạng key-value set (thứ tự param không quan trọng,
// transient keys ignored). Dùng để distinguish /path?type=2 vs /path?type=2&pendingMe=1.
function queryMatches(current: string, target: string): boolean {
  const a = new URLSearchParams(current)
  const b = new URLSearchParams(target)

  // 🔴 [S155 đợt 2] Transient chỉ được bỏ qua khi MENU ĐÍCH KHÔNG tự ghim key đó.
  // Bug UAT 2026-07-27 (anh báo): click "Danh sách" mà menu sáng "Đã duyệt".
  // Nguyên nhân: `phase` vừa là BỘ LỌC trạng thái trên trang danh sách (nên nằm
  // trong TRANSIENT — fix 2026-05-08), vừa vừa-mới trở thành DANH TÍNH điều hướng
  // của mục "Đã duyệt" (`?type=N&phase=7`, đợt 2). Strip cả 2 vế ⇒ "Danh sách"
  // (`[type]`) và "Đã duyệt" (`[type]` sau khi mất `phase`) thành KHÔNG PHÂN BIỆT
  // ĐƯỢC ⇒ URL `?type=1` khớp cả hai, mục render sau thắng.
  // Cùng một tham số không thể vừa là thứ-bị-bỏ-qua vừa là thứ-định-danh; nên
  // quyền quyết định thuộc về ĐÍCH: đích ghim `phase` ⇒ so khớp NGHIÊM; đích không
  // ghim ⇒ giữ nguyên hành vi cũ (lọc/chọn dòng không làm mất highlight).
  const pinnedByTarget = new Set(b.keys())
  const ignorable = (k: string) => TRANSIENT_QUERY_KEYS.has(k) && !pinnedByTarget.has(k)

  const aKeys = [...a.keys()].filter(k => !ignorable(k)).sort()
  const bKeys = [...b.keys()].filter(k => !ignorable(k)).sort()
  if (aKeys.length !== bKeys.length) return false
  return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
}

function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
  const Icon = getIcon(node.icon)
  const path = resolvePath(node.key)
  const location = useLocation()
  if (!path) return null
  const isDeep = depth >= 2

  // Custom active check: pathname match + query string match (set equality).
  // Fix bug: /purchase-evaluations?type=2 và ?type=2&pendingMe=1 cùng highlight
  // vì NavLink default chỉ check pathname.
  const [targetPath, targetQuery = ''] = path.split('?')
  const pathnameMatches = location.pathname === targetPath
  const qMatches = queryMatches(location.search.replace(/^\?/, ''), targetQuery)
  const isActive = pathnameMatches && qMatches

  return (
    <NavLink
      to={path}
      title={effectiveLabel(node)}
      className={cn(
        // Density-first: active leaf gets a brand left-rail + tint (crisp
        // selected affordance, NAMGROUP). Inactive stays quiet slate.
        'relative block rounded-md leading-snug transition-colors',
        // [S159-đợt5 owner] leaf cấp-0 render qua MenuLeaf ("Tổng quan quy trình") viết
        // HOA đồng bộ group header cấp-0 (khuôn MenuGroup :260). 🔴 KHAI ĐÚNG PHẠM VI
        // (reviewer FLAG-3): "Hộp thư" KHÔNG đi qua đây — nó là USER_FIXED_TOP render
        // bằng StaticLeaf (lớp CSS riêng, vẫn chữ thường). Đồng bộ nốt = quyết owner.
        depth === 0
          ? 'px-3 py-2 text-[11px] font-semibold uppercase tracking-wide'
          : isDeep
            ? 'px-3 py-1 text-[11px]'
            : 'px-3 py-1.5 text-[12px] font-medium',
        isActive
          ? 'bg-brand-50 font-semibold text-brand-700 before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:bg-brand-600'
          : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
      )}
    >
      {/* [Plan AA S24 t2 wrap fix] inline-block icon + inline text → label dài
          wrap về đầu hàng (under icon, KHÔNG indent sau icon). */}
      <Icon className={cn('mr-2 -mt-0.5 inline-block align-middle', isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
      <span className="align-middle">{effectiveLabel(node)}</span>
    </NavLink>
  )
}

// Static entries prepended to the dynamic menu tree — these are user-app
// specific (inbox + quick create) not backed by MenuItems DB rows.
const USER_FIXED_TOP: MenuNode[] = [
  { key: '__inbox', label: 'Hộp thư', parentKey: null, order: 0, icon: 'Inbox', canRead: true, canCreate: true, canUpdate: true, canDelete: true, isVisible: true, displayLabel: null, children: [] },
]

function staticResolvePath(key: string): string | null {
  if (key === '__inbox') return '/inbox'
  return null
}

function StaticLeaf({ node }: { node: MenuNode }) {
  const Icon = getIcon(node.icon)
  const path = staticResolvePath(node.key)
  if (!path) return null
  return (
    <NavLink
      to={path}
      end
      title={effectiveLabel(node)}
      className={({ isActive }) =>
        cn(
          'relative block rounded-md px-3 py-1.5 text-[12px] font-medium leading-snug transition-colors',
          isActive
            ? 'bg-brand-50 font-semibold text-brand-700 before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:bg-brand-600'
            : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
        )
      }
    >
      {/* [Plan AA S24 t2 wrap fix] inline-block icon + inline text — consistent
          với MenuLeaf + MenuGroup. */}
      <Icon className="mr-2 -mt-0.5 inline-block h-4 w-4 align-middle" />
      <span className="align-middle">{effectiveLabel(node)}</span>
    </NavLink>
  )
}

export function Layout() {
  const { menu } = useAuth()
  const filteredMenu = filterForUser(menu)
  const location = useLocation()

  // Accordion state — 1 Ct_ HĐ + 1 Pe_ phiếu expand tối đa. Auto-sync theo
  // URL: /contracts|/my-contracts|/inbox?type=N → Ct, /purchase-evaluations
  // ?type=N → Pe. Khác pathname gốc → skip (không reset, giữ user choice).
  const [expandedCtCode, setExpandedCtCode] = useState<string | null>(null)
  const [expandedPeCode, setExpandedPeCode] = useState<string | null>(null)

  useEffect(() => {
    const params = new URLSearchParams(location.search)

    // 🔴 [YC-029 W1 S190 — bug A] `ct` ĐI TRƯỚC `type`: leaf "Luồng duyệt" của 7 nhóm HĐ
    // đi URL `/contracts/workflow-matrix?type=3&ct=N`, trong đó `type=3` là ApplicableType
    // (hằng cho cả họ) chứ KHÔNG phải ContractType ⇒ đọc `type` ở đây sẽ mở nhầm nhóm 03
    // "Nhà cung cấp" cho cả 7 leaf. `ct` mới là danh-tính loại HĐ của leaf.
    // Clamp `/^[1-7]$/` bằng REGEX CHUỖI, không `Number()`: `Number('03')===3` lọt lưới.
    // `ct` rác/thiếu ⇒ rơi xuống nhánh `type` cũ (không tự chế hành vi mới).
    const rawCt = params.get('ct')
    if (rawCt !== null && /^[1-7]$/.test(rawCt)) {
      const ctCode = INT_TO_TYPE_CODE[Number(rawCt)]
      if (ctCode) setExpandedCtCode(ctCode)
      return
    }

    const typeParam = params.get('type')
    if (!typeParam) return

    const n = Number(typeParam)
    if (location.pathname.startsWith('/purchase-evaluations')) {
      const code = INT_TO_PE_CODE[n]
      if (code) setExpandedPeCode(code)
    } else if ((CT_TYPE_PARAM_PATHS as readonly string[]).includes(location.pathname)) {
      // [YC-029 W1 S190] CHỈ 3 pathname trong allowlist EXACT (:32-38) mới được suy nhóm
      // HĐ từ `?type=`. Trước đây `else` trần ⇒ MỌI trang có `?type=` đều đá accordion
      // (workflow-matrix, hard-copies…). Trang nào cần auto-expand thì THÊM TÊN vào
      // allowlist — đừng nới lại thành startsWith.
      const code = INT_TO_TYPE_CODE[n]
      if (code) setExpandedCtCode(code)
    }
  }, [location.pathname, location.search])

  const accordionValue = useMemo(
    () => ({ expandedCtCode, setExpandedCtCode, expandedPeCode, setExpandedPeCode }),
    [expandedCtCode, expandedPeCode],
  )

  return (
    <AccordionContext.Provider value={accordionValue}>
      {/* Brand stripe — dải nhận diện #1F7DC1 chạy suốt đỉnh app. 2026-06-16
          anh "nâng màu": dày hơn (h-1.5) + dải gradient xanh đậm bắt mắt. */}
      <div className="flex h-screen flex-col">
        <div className="h-1.5 shrink-0 bg-gradient-to-r from-brand-700 via-brand-400 to-brand-600" />
        <div className="flex min-h-0 flex-1">
        <aside className="flex w-72 flex-col border-r border-slate-200 bg-white xl:w-80">
          {/* Logo cap — brand gradient wash đậm hơn (was brand-50/70). */}
          <div className="flex h-16 items-center border-b border-brand-100 bg-gradient-to-r from-brand-50 via-brand-50/60 to-transparent px-5">
            <Link to="/dashboard" className="flex items-center gap-2.5">
              <img src="/logo.png" alt="Solutions" className="h-8 w-auto" />
              <span className="rounded-md bg-brand-600 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white">ERP</span>
            </Link>
          </div>
          <nav className="flex-1 space-y-1 overflow-y-auto p-3">
            {USER_FIXED_TOP.map(n => <StaticLeaf key={n.key} node={n} />)}
            {filteredMenu.map(n => (
              <MenuNodeRenderer key={n.key} node={n} depth={0} />
            ))}
          </nav>
        </aside>
        <div className="flex flex-1 flex-col overflow-hidden">
          <TopBar />
          <main className="flex-1 overflow-auto">
            <Outlet />
          </main>
        </div>
        </div>
      </div>
    </AccordionContext.Provider>
  )
}
