-- armurier/cl_ui.lua
-- Interface utilisateur DBALib pour la boutique de l'armurier.
-- Menu à 3 onglets : Armes / Munitions / Autres
-- Chaque onglet affiche, dans un ScrollPanel, un en-tête par catégorie suivi
-- de ses lignes d'items (buildItemRow).
-- Bouton "Gérer le catalogue" visible uniquement si LocalPlayer():IsAdmin().
--
-- NOTE : vérification DBALib en lazy (pas d'early-return au top-level)
--        pour respecter l'ordre de chargement GMod.

Armurier    = Armurier    or {}
Armurier.UI = Armurier.UI or {}

-- Garde défensive : le catalogue peut ne pas être encore reçu à la 1re ouverture
Armurier.CatalogData = Armurier.CatalogData or { categories = {}, items = {} }

-- ============================================================================
-- State
-- ============================================================================

local CurrentFrame  = nil
local CurrentCaisse = nil

-- ============================================================================
-- Helper : DBALib disponible ?
-- ============================================================================

local function dbaLibReady()
    if DBALib and DBALib.UI and DBALib.UI.Frame and DBALib.UI.Tabs then
        return true
    end
    ErrorNoHalt("[Armurier] DBALib v0.8.0+ requis — UI désactivée.\n")
    return false
end

-- ============================================================================
-- Helper : calcule la réduction locale (%) pour le joueur local.
-- Soft-dépendance : retourne 0 si dbalevel absent ou State inexistant.
-- ============================================================================

local function getDiscountLocal()
    if not (DBALevel and DBALevel.State and DBALevel.State.GetLevel) then return 0 end
    local lvl = DBALevel.State.GetLevel("legal") or 0
    local cfg  = Armurier and Armurier.Config or {}
    local tierLevels = math.max(1, math.floor(cfg.DiscountTierLevels or 5))
    local perTier    = math.floor(cfg.DiscountPerTier or 2)
    local maxDisc    = math.floor(cfg.DiscountMax or 30)
    return math.min(maxDisc, math.floor(lvl / tierLevels) * perTier)
end

-- ============================================================================
-- Helper : fermer la frame proprement
-- ============================================================================

local function closeFrame()
    if IsValid(CurrentFrame) then
        CurrentFrame:Close()
    end
    CurrentFrame  = nil
    CurrentCaisse = nil
end

-- ============================================================================
-- Armurier.UI.OnBuyResult — appelé depuis cl_net.lua
-- ============================================================================

function Armurier.UI.OnBuyResult(ok, msg)
    if not dbaLibReady() then return end

    if ok then
        DBALib.Sound("ui_notify")
        DBALib.UI.Notify({
            message  = msg,
            type     = "success",
            duration = 4,
        })
    else
        DBALib.Sound("ui_error")
        DBALib.UI.Notify({
            message  = msg,
            type     = "danger",
            duration = 5,
        })
    end
end

-- ============================================================================
-- Armurier.UI.RefreshMenu — re-construit le menu si ouvert
-- Appelé après CatalogUpdate pour refléter les changements admin.
-- ============================================================================

function Armurier.UI.RefreshMenu()
    if not IsValid(CurrentFrame) then return end
    -- Fermer et réouvrir avec la même caisse
    local caisse = CurrentCaisse
    closeFrame()
    if IsValid(caisse) then
        timer.Simple(0.05, function()
            if IsValid(caisse) then
                Armurier.UI.OpenMenu(caisse)
            end
        end)
    end
end

-- ============================================================================
-- Helper : une arme de cette classe est-elle droppée (spawned_weapon) à portée
-- de la caisse ? Lecture clientside des entités networkées (WeaponClass est un
-- NetworkVar de spawned_weapon, IsSpawnedWeapon est défini en shared).
-- ============================================================================

local function hasNearbyDroppedWeapon(caisse, weaponClass)
    if not IsValid(caisse) then return false end
    local radius = (Armurier.Config and Armurier.Config.SellRadius) or 150
    for _, e in ipairs(ents.FindInSphere(caisse:GetPos(), radius)) do
        if IsValid(e) and e.IsSpawnedWeapon and e.GetWeaponClass
           and e:GetWeaponClass() == weaponClass then
            return true
        end
    end
    return false
end

-- ============================================================================
-- Helper : construire une ligne d'item (auto-portante)
-- Une ligne dockée TOP, pleine largeur, qui repositionne ses enfants dans son
-- PerformLayout → robuste quelle que soit la largeur (pas de calcul figé au
-- moment du build, contrairement aux composants Card/Accordion).
-- ============================================================================

local function buildItemRow(parent, item, caisse)
    if not dbaLibReady() then return end

    local S     = DBALib.Scale
    local Fonts = DBALib.Fonts or {}
    local pal   = (DBALib.Theme and DBALib.Theme.palette) or {}

    -- Hauteur adaptée selon les infos affichées (stats arme = plus haute)
    local rowH = item.kind == "weapon" and S(90) or S(72)

    -- Géométrie : preview carrée à gauche, colonne texte ensuite
    local prevSize = rowH - S(16)
    local textX    = S(8) + prevSize + S(14)

    local row = vgui.Create("DPanel", parent)
    row:Dock(TOP)
    row:SetTall(rowH)
    row:DockMargin(S(6), S(3), S(6), S(3))

    -- -----------------------------------------------------------------------
    -- Calcul du prix (avec réduction si arme)
    -- -----------------------------------------------------------------------
    local discount  = (item.kind == "weapon") and getDiscountLocal() or 0
    local basePrice = item.price
    local paid      = basePrice
    if discount > 0 then
        paid = math.floor(basePrice * ((100 - discount) / 100))
    end

    local stockMax  = (Armurier and Armurier.Config and Armurier.Config.StockMax) or 10
    local curStock  = math.max(0, math.floor(tonumber(item.stock) or stockMax))
    local outOfStock = curStock <= 0

    -- -----------------------------------------------------------------------
    -- Données stats arme (weapon uniquement, lazy — weapons.Get peut être nil)
    -- -----------------------------------------------------------------------
    local swepDmg, swepClip, swepAmmo
    if item.kind == "weapon" then
        local swep = weapons.Get(item.class)
        if swep and swep.Primary then
            swepDmg  = swep.Primary.Damage
            swepClip = swep.Primary.ClipSize
            swepAmmo = swep.Primary.Ammo
        end
    end

    -- -----------------------------------------------------------------------
    -- Paint
    -- -----------------------------------------------------------------------
    row.Paint = function(s, w, h)
        local bg = (pal.bg and pal.bg.elevated) or Color(40, 40, 46)
        draw.RoundedBox(S(6), 0, 0, w, h, bg)

        local nameC  = (pal.text and pal.text.primary) or Color(235, 235, 235)
        local goldC  = (pal.gold and pal.gold.base) or Color(212, 175, 55)
        local mutedC = (pal.text and pal.text.muted) or Color(140, 140, 140)
        local okC    = (pal.semantic and pal.semantic.success) or Color(120, 200, 120)
        local isWep  = item.kind == "weapon"

        local function fmtM(n)
            return (DBALib.Format and DBALib.Format.Money(n)) or (tostring(n) .. " €")
        end

        -- Nom (ligne 1)
        local nameY = isWep and S(16) or (h / 2 - S(10))
        draw.SimpleText(item.class, Fonts.Body_md or "DermaDefaultBold",
            textX, nameY, nameC, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)

        -- Prix (ligne 2) : barré + réduit + -X% si réduction
        local priceY = isWep and S(42) or (h / 2 + S(11))
        if discount > 0 then
            local baseTxt = fmtM(basePrice)
            surface.SetFont(Fonts.Body_xs or "DermaDefault")
            local baseW = surface.GetTextSize(baseTxt)
            draw.SimpleText(baseTxt, Fonts.Body_xs or "DermaDefault",
                textX, priceY, mutedC, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
            surface.SetDrawColor(mutedC)
            surface.DrawRect(textX, priceY, baseW, 1)
            draw.SimpleText(fmtM(paid), Fonts.Body_md or "DermaDefault",
                textX + baseW + S(8), priceY, goldC, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
            surface.SetFont(Fonts.Body_md or "DermaDefault")
            local paidW = surface.GetTextSize(fmtM(paid))
            draw.SimpleText("-" .. discount .. "%", Fonts.Body_xs or "DermaDefault",
                textX + baseW + S(8) + paidW + S(6), priceY, okC, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
        else
            draw.SimpleText(fmtM(paid), Fonts.Body_md or "DermaDefault",
                textX, priceY, goldC, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
        end

        -- Stats arme (ligne 3)
        if isWep then
            local statStr = string.format("Dégâts %s   ·   Chargeur %s   ·   %s",
                tostring(swepDmg or "—"), tostring(swepClip or "—"), tostring(swepAmmo or "—"))
            draw.SimpleText(statStr, Fonts.Body_xs or "DermaDefault",
                textX, S(68), mutedC, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
        end

        -- Stock : haut-droite (au-dessus du bouton), couleur selon niveau
        local stockColor = mutedC
        if outOfStock then
            stockColor = (pal.semantic and pal.semantic.danger) or Color(220, 60, 60)
        elseif curStock <= math.ceil(stockMax / 4) then
            stockColor = (pal.semantic and pal.semantic.warning) or Color(220, 160, 40)
        end
        draw.SimpleText("Stock " .. curStock .. "/" .. stockMax, Fonts.Body_xs or "DermaDefault",
            w - S(12), S(14), stockColor, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
    end

    -- -----------------------------------------------------------------------
    -- Preview 3D (DModelPanel) — remplace SpawnIcon (#9)
    -- -----------------------------------------------------------------------
    local modelPanel = nil
    if item.model and item.model ~= "" then
        local ok, result = pcall(function()
            local mp = vgui.Create("DModelPanel", row)
            mp:SetMouseInputEnabled(false)
            mp:SetModel(item.model)
            mp:SetFOV(40)
            -- Éclairage (sinon le modèle est rendu très sombre / invisible)
            mp:SetAmbientLight(Color(150, 150, 160))
            mp:SetDirectionalLight(BOX_TOP,   Color(255, 255, 255))
            mp:SetDirectionalLight(BOX_FRONT, Color(210, 210, 210))
            -- Cadrage caméra depuis la bounding box réelle du modèle (sinon la
            -- caméra regarde l'origine et le modèle est hors-champ).
            local ent = mp:GetEntity()
            if IsValid(ent) then
                local mn, mx = ent:GetRenderBounds()
                local center = (mn + mx) * 0.5
                local sz     = mn:Distance(mx)
                if sz <= 0 then sz = 16 end
                mp:SetCamPos(center + Vector(sz * 1.4, 0, sz * 0.35))
                mp:SetLookAt(center)
            end
            -- Rotation lente (turntable)
            mp.LayoutEntity = function(_, e)
                if IsValid(e) then e:SetAngles(Angle(0, (RealTime() * 25) % 360, 0)) end
            end
            return mp
        end)
        if ok and IsValid(result) then
            modelPanel = result
        end
    end

    -- -----------------------------------------------------------------------
    -- Bouton Acheter / Vendre / Rupture
    -- -----------------------------------------------------------------------
    local sellable = (item.kind == "weapon") and hasNearbyDroppedWeapon(caisse, item.class)
    local ratio    = (Armurier and Armurier.Config and Armurier.Config.SellbackRatio) or 0.70

    local function fmtMoney(n)
        return (DBALib.Format and DBALib.Format.Money(n)) or (tostring(n) .. " €")
    end

    local buyBtn
    if outOfStock and not sellable then
        -- Rupture de stock → bouton désactivé
        buyBtn = DBALib.UI.Button({
            parent   = row,
            label    = "Rupture",
            variant  = "secondary",
            size     = "sm",
            disabled = true,
            onClick  = function() end,
        })
    else
        buyBtn = DBALib.UI.Button({
            parent  = row,
            label   = sellable and "Vendre" or "Acheter",
            variant = sellable and "danger" or "primary",
            size    = "sm",
            onClick = function()
                if not IsValid(caisse) then
                    DBALib.UI.Notify({ message = "Caisse invalide.", type = "danger", duration = 3 })
                    return
                end

                -- --- Mode VENTE ---
                if sellable then
                    local sellPrice = math.floor(item.price * ratio)
                    DBALib.UI.Confirm({
                        title   = "Confirmer la vente",
                        message = string.format("Vendre %s pour %s ?", item.class, fmtMoney(sellPrice)),
                        onYes = function()
                            net.Start("Armurier.SellItem")
                                net.WriteEntity(caisse)
                                net.WriteUInt(item.id, 16)
                            net.SendToServer()
                        end,
                    })
                    return
                end

                -- --- Mode ACHAT ---
                local money = LocalPlayer():getDarkRPVar("money") or 0
                if money < paid then
                    DBALib.Sound("ui_error")
                    DBALib.UI.Notify({
                        message  = "Fonds insuffisants (" .. fmtMoney(paid) .. " requis).",
                        type     = "warning",
                        duration = 4,
                    })
                    return
                end

                local confirmMsg = string.format("Acheter %s pour %s ?", item.class, fmtMoney(paid))
                if discount > 0 then
                    confirmMsg = confirmMsg .. string.format("\n(Réduction %d%% appliquée — base %s)", discount, fmtMoney(basePrice))
                end

                DBALib.UI.Confirm({
                    title   = "Confirmer l'achat",
                    message = confirmMsg,
                    onYes = function()
                        net.Start("Armurier.BuyItem")
                            net.WriteEntity(caisse)
                            net.WriteUInt(item.id, 16)
                        net.SendToServer()
                    end,
                })
            end,
        })
    end

    -- -----------------------------------------------------------------------
    -- PerformLayout : preview à gauche, bouton en bas-droite (stock dessiné
    -- en haut-droite dans Paint, réduction inline dans la ligne de prix).
    -- -----------------------------------------------------------------------
    row.PerformLayout = function(s, w, h)
        if IsValid(modelPanel) then
            modelPanel:SetSize(prevSize, prevSize)
            modelPanel:SetPos(S(8), (h - prevSize) / 2)
        end
        if IsValid(buyBtn) then
            local bw, bh = S(110), S(34)
            buyBtn:SetSize(bw, bh)
            buyBtn:SetPos(w - bw - S(10), h - bh - S(10))
        end
    end

    return row
end

-- ============================================================================
-- Helper : (re)construire le contenu filtré dans le canvas d'un ScrollPanel
-- Appelée à la construction initiale et après chaque changement du filtre.
-- ============================================================================

local function buildPageContent(canvas, cats, allItems, filterTerm, caisse)
    if not dbaLibReady() then return end

    local S     = DBALib.Scale
    local Fonts = DBALib.Fonts or {}
    local pal   = (DBALib.Theme and DBALib.Theme.palette) or {}

    -- Vider le canvas
    canvas:Clear()

    local term = string.lower(filterTerm or "")

    local anyVisible = false
    for _, cat in ipairs(cats) do
        -- Filtrer les items de cette catégorie
        local catItems = {}
        for _, item in ipairs(allItems) do
            if item.category_id == cat.id then
                -- filtre insensible à la casse sur item.class
                if term == "" or string.find(string.lower(item.class), term, 1, true) then
                    catItems[#catItems + 1] = item
                end
            end
        end

        -- Si aucun item après filtre → masquer la catégorie
        if #catItems == 0 then
            -- ne rien créer (catégorie masquée)
        else
            anyVisible = true
            -- En-tête de catégorie
            local header = vgui.Create("DPanel", canvas)
            header:Dock(TOP)
            header:SetTall(S(30))
            header:DockMargin(S(6), S(10), S(6), S(2))
            header.Paint = function(s, w, h)
                local goldC = (pal.gold and pal.gold.base) or Color(212, 175, 55)
                draw.SimpleText(cat.name, Fonts.Body_lg or "DermaDefaultBold",
                    S(4), h / 2, goldC, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
                local faint = (pal.gold and pal.gold.faint) or Color(212, 175, 55, 60)
                surface.SetDrawColor(faint)
                surface.DrawRect(0, h - 1, w, 1)
            end

            for _, item in ipairs(catItems) do
                buildItemRow(canvas, item, caisse)
            end
        end
    end

    -- Si aucun résultat de recherche
    if not anyVisible then
        local emptyLbl = vgui.Create("DLabel", canvas)
        emptyLbl:SetText(term ~= "" and "Aucun résultat pour « " .. filterTerm .. " »."
                                     or "Aucun item dans cette page.")
        emptyLbl:SetFont(Fonts.Body_sm or "DermaDefault")
        emptyLbl:SetTextColor((pal.text and pal.text.muted) or Color(150, 150, 150))
        emptyLbl:Dock(TOP)
        emptyLbl:DockMargin(S(12), S(12), S(12), S(6))
    end
end

-- ============================================================================
-- Helper : construire le panneau d'une page (onglet)
-- page   : "weapon" | "ammo" | "accessory"
-- caisse : entity
-- ============================================================================

local function buildPagePanel(pagePanel, page, caisse)
    if not dbaLibReady() then return end

    local S = DBALib.Scale

    -- Récupérer les catégories de cette page
    local cats = {}
    for id, cat in pairs(Armurier.CatalogData.categories) do
        if cat.page == page then
            cats[#cats + 1] = cat
        end
    end
    table.sort(cats, function(a, b) return a.id < b.id end)

    -- Pré-collecter tous les items de la page triés par id
    local allItems = {}
    for id, item in pairs(Armurier.CatalogData.items) do
        if item.page == page then
            allItems[#allItems + 1] = item
        end
    end
    table.sort(allItems, function(a, b) return a.id < b.id end)

    -- Si aucune catégorie → EmptyState centré sur toute la page.
    if #cats == 0 then
        local emptyState = DBALib.UI.EmptyState({
            parent  = pagePanel,
            title   = "Catalogue vide",
            message = "Aucune catégorie pour cette page.\nUn admin peut en créer via l'onglet 'Gerer'.",
        })
        if IsValid(emptyState) then
            emptyState:Dock(FILL)
        end
        return
    end

    -- -----------------------------------------------------------------------
    -- Barre de recherche (#7)
    -- Les deux variables sont déclarées AVANT d'être utilisées dans les closures.
    -- -----------------------------------------------------------------------
    local currentFilter = ""
    local scroll        = nil  -- référence forward-déclarée

    local searchInput = DBALib.UI.SearchInput({
        parent      = pagePanel,
        placeholder = "Rechercher…",
        onChange    = function(txt)
            currentFilter = txt or ""
            if IsValid(scroll) then
                buildPageContent(scroll:GetCanvas(), cats, allItems, currentFilter, caisse)
            end
        end,
    })
    if IsValid(searchInput) then
        searchInput:Dock(TOP)
        searchInput:DockMargin(S(6), S(6), S(6), S(4))
        searchInput:SetTall(S(34))
    end

    -- Scroll panel principal
    scroll = DBALib.UI.ScrollPanel({
        parent = pagePanel,
        w      = pagePanel:GetWide(),
        h      = pagePanel:GetTall(),
    })
    scroll:Dock(FILL)

    -- Contenu initial (sans filtre)
    buildPageContent(scroll:GetCanvas(), cats, allItems, "", caisse)
end

-- ============================================================================
-- Helper : construire le panneau admin (gestion du catalogue)
-- ============================================================================

local function buildAdminPanel(adminPanel, caisse)
    if not dbaLibReady() then return end

    local S = DBALib.Scale

    local scroll = DBALib.UI.ScrollPanel({
        parent = adminPanel,
        w      = adminPanel:GetWide(),
        h      = adminPanel:GetTall(),
    })
    scroll:Dock(FILL)

    local canvas = scroll:GetCanvas()

    -- -----------------------------------------------------------------------
    -- Section : Ajouter une catégorie
    -- -----------------------------------------------------------------------
    local secCatTitle = vgui.Create("DLabel", canvas)
    secCatTitle:SetText("Ajouter une catégorie")
    secCatTitle:SetFont((DBALib.Fonts and DBALib.Fonts.Body_lg) or "DermaDefaultBold")
    secCatTitle:SetTextColor(
        (DBALib.Theme and DBALib.Theme.palette and DBALib.Theme.palette.gold
         and DBALib.Theme.palette.gold.base) or Color(212, 175, 55)
    )
    secCatTitle:SizeToContents()
    secCatTitle:Dock(TOP)
    secCatTitle:DockMargin(S(8), S(8), S(8), S(4))

    local addCatForm = DBALib.UI.Form({
        parent      = canvas,
        fields      = {
            {
                id          = "page",
                label       = "Page",
                type        = "dropdown",
                options     = {
                    { id = "weapon",    label = "Armes" },
                    { id = "ammo",      label = "Munitions" },
                    { id = "accessory", label = "Autres" },
                },
                value       = "weapon",
            },
            {
                id          = "name",
                label       = "Nom de la catégorie",
                type        = "text",
                placeholder = "Ex: Pistolets, Fusils...",
                maxLength   = 64,
                validator   = DBALib and DBALib.Validators and DBALib.Validators.Required or nil,
            },
        },
        submitLabel = "Créer",
        showCancel  = false,
        onSubmit    = function(values)
            local page = values.page or "weapon"
            local name = values.name or ""
            name = name:match("^%s*(.-)%s*$")
            if name == "" then
                DBALib.UI.Notify({ message = "Nom vide.", type = "warning", duration = 3 })
                return
            end

            net.Start("Armurier.AdminAddCategory")
                net.WriteString(page)
                net.WriteString(string.sub(name, 1, 64))
            net.SendToServer()
        end,
    })
    if IsValid(addCatForm) then
        addCatForm:Dock(TOP)
        addCatForm:DockMargin(S(8), S(4), S(8), S(8))
    end

    -- -----------------------------------------------------------------------
    -- Divider
    -- -----------------------------------------------------------------------
    local div1 = DBALib.UI.Divider({ parent = canvas })
    if IsValid(div1) then
        div1:Dock(TOP)
        div1:DockMargin(S(8), S(4), S(8), S(4))
    end

    -- -----------------------------------------------------------------------
    -- Section : Supprimer une catégorie
    -- -----------------------------------------------------------------------
    local secDelCatTitle = vgui.Create("DLabel", canvas)
    secDelCatTitle:SetText("Supprimer une catégorie")
    secDelCatTitle:SetFont((DBALib.Fonts and DBALib.Fonts.Body_lg) or "DermaDefaultBold")
    secDelCatTitle:SetTextColor(
        (DBALib.Theme and DBALib.Theme.palette and DBALib.Theme.palette.semantic
         and DBALib.Theme.palette.semantic.danger) or Color(220, 60, 60)
    )
    secDelCatTitle:SizeToContents()
    secDelCatTitle:Dock(TOP)
    secDelCatTitle:DockMargin(S(8), S(8), S(8), S(4))

    -- Dropdown des catégories existantes
    local catOptions = {}
    for id, cat in pairs(Armurier.CatalogData.categories) do
        catOptions[#catOptions + 1] = {
            id    = tostring(cat.id),
            label = string.format("#%d [%s] %s", cat.id, cat.page, cat.name),
        }
    end
    table.sort(catOptions, function(a, b) return tonumber(a.id) < tonumber(b.id) end)

    if #catOptions == 0 then
        catOptions = { { id = "0", label = "(aucune catégorie)" } }
    end

    local delCatForm = DBALib.UI.Form({
        parent      = canvas,
        fields      = {
            {
                id      = "cat_id",
                label   = "Catégorie à supprimer",
                type    = "dropdown",
                options = catOptions,
                value   = catOptions[1] and catOptions[1].id or "0",
            },
        },
        submitLabel = "Supprimer",
        showCancel  = false,
        onSubmit    = function(values)
            local cat_id = tonumber(values.cat_id) or 0
            if cat_id <= 0 then
                DBALib.UI.Notify({ message = "Sélectionnez une catégorie valide.", type = "warning", duration = 3 })
                return
            end

            local cat = Armurier.CatalogData.categories[cat_id]
            local catName = cat and cat.name or ("#" .. cat_id)

            DBALib.UI.Confirm({
                title   = "Supprimer la catégorie",
                message = string.format(
                    "Supprimer la catégorie '%s' ?\nTous ses items seront aussi supprimés.",
                    catName
                ),
                onYes = function()
                    net.Start("Armurier.AdminRemoveCategory")
                        net.WriteUInt(cat_id, 16)
                    net.SendToServer()
                end,
            })
        end,
    })
    if IsValid(delCatForm) then
        delCatForm:Dock(TOP)
        delCatForm:DockMargin(S(8), S(4), S(8), S(8))
    end

    -- -----------------------------------------------------------------------
    -- Divider
    -- -----------------------------------------------------------------------
    local div2 = DBALib.UI.Divider({ parent = canvas })
    if IsValid(div2) then
        div2:Dock(TOP)
        div2:DockMargin(S(8), S(4), S(8), S(4))
    end

    -- -----------------------------------------------------------------------
    -- Section : Ajouter un item
    -- -----------------------------------------------------------------------
    local secAddItemTitle = vgui.Create("DLabel", canvas)
    secAddItemTitle:SetText("Ajouter un item au catalogue")
    secAddItemTitle:SetFont((DBALib.Fonts and DBALib.Fonts.Body_lg) or "DermaDefaultBold")
    secAddItemTitle:SetTextColor(
        (DBALib.Theme and DBALib.Theme.palette and DBALib.Theme.palette.gold
         and DBALib.Theme.palette.gold.base) or Color(212, 175, 55)
    )
    secAddItemTitle:SizeToContents()
    secAddItemTitle:Dock(TOP)
    secAddItemTitle:DockMargin(S(8), S(8), S(8), S(4))

    -- Options de catégories pour le dropdown d'items
    local itemCatOptions = {}
    for id, cat in pairs(Armurier.CatalogData.categories) do
        itemCatOptions[#itemCatOptions + 1] = {
            id    = tostring(cat.id),
            label = string.format("[%s] %s", cat.page, cat.name),
        }
    end
    table.sort(itemCatOptions, function(a, b) return tonumber(a.id) < tonumber(b.id) end)

    if #itemCatOptions == 0 then
        itemCatOptions = { { id = "0", label = "(créer une catégorie d'abord)" } }
    end

    -- State local pour le kind (pour choisir le bon picker)
    local addItemKindValue = "weapon"
    local addItemPageValue = "weapon"
    local addItemClassValue = ""
    local addItemModelValue = ""
    -- Forward-declaration : référencé par la closure onChange du kindDropdown
    -- (assigné plus bas dans la section classRow).
    local classDisplay

    -- Ligne : kind + page
    local kindRow = vgui.Create("DPanel", canvas)
    kindRow:SetTall(S(40))
    kindRow:Dock(TOP)
    kindRow:DockMargin(S(8), S(4), S(8), S(4))
    kindRow:SetPaintBackground(false)

    local kindLabel = vgui.Create("DLabel", kindRow)
    kindLabel:SetText("Kind / Page :")
    kindLabel:SetFont((DBALib.Fonts and DBALib.Fonts.Body_sm) or "DermaDefault")
    kindLabel:SetTextColor(
        (DBALib.Theme and DBALib.Theme.palette and DBALib.Theme.palette.text
         and DBALib.Theme.palette.text.muted) or Color(150, 150, 150)
    )
    kindLabel:SetWide(S(120))
    kindLabel:SetTall(S(30))
    kindLabel:SetPos(0, S(5))

    local kindDropdown = DBALib.UI.Dropdown({
        parent  = kindRow,
        options = {
            { id = "weapon",    label = "Arme (weapon)" },
            { id = "ammo",      label = "Munition (ammo)" },
            { id = "accessory", label = "Accessoire (accessory)" },
        },
        value   = "weapon",
        onChange = function(val)
            addItemKindValue = val
            addItemPageValue = val
            -- Réinitialiser la sélection de classe (le picker change de type)
            addItemClassValue = ""
            addItemModelValue = ""
            if IsValid(classDisplay) then
                classDisplay:SetText("(non sélectionné)")
            end
        end,
    })
    if IsValid(kindDropdown) then
        kindDropdown:SetSize(S(220), S(32))
        kindDropdown:SetPos(S(125), S(4))
    end

    -- Ligne : EntityPicker / class
    local classRow = vgui.Create("DPanel", canvas)
    classRow:SetTall(S(50))
    classRow:Dock(TOP)
    classRow:DockMargin(S(8), S(4), S(8), S(4))
    classRow:SetPaintBackground(false)

    local classLabel = vgui.Create("DLabel", classRow)
    classLabel:SetText("Classe (SWEP/SENT) :")
    classLabel:SetFont((DBALib.Fonts and DBALib.Fonts.Body_sm) or "DermaDefault")
    classLabel:SetTextColor(
        (DBALib.Theme and DBALib.Theme.palette and DBALib.Theme.palette.text
         and DBALib.Theme.palette.text.muted) or Color(150, 150, 150)
    )
    classLabel:SetWide(S(140))
    classLabel:SetTall(S(30))
    classLabel:SetPos(0, S(10))

    classDisplay = vgui.Create("DLabel", classRow)
    classDisplay:SetText("(non sélectionné)")
    classDisplay:SetFont((DBALib.Fonts and DBALib.Fonts.Body_md) or "DermaDefault")
    classDisplay:SetTextColor(
        (DBALib.Theme and DBALib.Theme.palette and DBALib.Theme.palette.text
         and DBALib.Theme.palette.text.primary) or Color(230, 230, 230)
    )
    classDisplay:SetWide(S(180))
    classDisplay:SetPos(S(145), S(8))

    local pickBtn = DBALib.UI.Button({
        parent  = classRow,
        label   = "Choisir...",
        variant = "ghost",
        size    = "sm",
        onClick = function()
            -- weapon → picker armes ; ammo → picker munitions ; accessory → entités
            local pickerKind = "entities"
            if addItemKindValue == "weapon" then
                pickerKind = "weapons"
            elseif addItemKindValue == "ammo" then
                pickerKind = "ammo"
            end

            DBALib.UI.EntityPicker({
                kind     = pickerKind,
                onSelect = function(item)
                    -- EntityPicker renvoie l'item { id = classe, label = nom }.
                    addItemClassValue = (item and item.id) or ""
                    -- Le model est déduit serveur-side (sv_storage.AddItem) si vide.
                    addItemModelValue = ""
                    if IsValid(classDisplay) then
                        classDisplay:SetText(addItemClassValue ~= "" and addItemClassValue or "(non sélectionné)")
                    end
                end,
            })
        end,
    })
    if IsValid(pickBtn) then
        pickBtn:SetSize(S(100), S(32))
        pickBtn:SetPos(S(330), S(9))
    end

    -- Ligne : catégorie + prix
    local addItemForm = DBALib.UI.Form({
        parent      = canvas,
        fields      = {
            {
                id      = "cat_id",
                label   = "Catégorie",
                type    = "dropdown",
                options = itemCatOptions,
                value   = itemCatOptions[1] and itemCatOptions[1].id or "0",
            },
            {
                id    = "price",
                label = "Prix",
                type  = "money",
                value = 1000,
                min   = 1,
            },
        },
        submitLabel = "Ajouter l'item",
        showCancel  = false,
        onSubmit    = function(values)
            local cat_id = tonumber(values.cat_id) or 0
            local price  = math.floor(tonumber(values.price) or 0)

            if addItemClassValue == "" then
                DBALib.UI.Notify({ message = "Sélectionnez une classe d'arme/entité.", type = "warning", duration = 3 })
                return
            end
            if cat_id <= 0 then
                DBALib.UI.Notify({ message = "Sélectionnez une catégorie valide.", type = "warning", duration = 3 })
                return
            end
            if price <= 0 then
                DBALib.UI.Notify({ message = "Prix invalide.", type = "warning", duration = 3 })
                return
            end

            net.Start("Armurier.AdminAddItem")
                net.WriteString(addItemKindValue)
                net.WriteString(addItemPageValue)
                net.WriteUInt(cat_id, 16)
                net.WriteString(addItemClassValue)
                net.WriteString(addItemModelValue)
                net.WriteUInt(price, 24)
            net.SendToServer()

            -- Reset
            addItemClassValue = ""
            addItemModelValue = ""
            if IsValid(classDisplay) then
                classDisplay:SetText("(non sélectionné)")
            end
        end,
    })
    if IsValid(addItemForm) then
        addItemForm:Dock(TOP)
        addItemForm:DockMargin(S(8), S(4), S(8), S(8))
    end

    -- -----------------------------------------------------------------------
    -- Divider
    -- -----------------------------------------------------------------------
    local div3 = DBALib.UI.Divider({ parent = canvas })
    if IsValid(div3) then
        div3:Dock(TOP)
        div3:DockMargin(S(8), S(4), S(8), S(4))
    end

    -- -----------------------------------------------------------------------
    -- Section : Supprimer un item
    -- -----------------------------------------------------------------------
    local secDelItemTitle = vgui.Create("DLabel", canvas)
    secDelItemTitle:SetText("Supprimer un item")
    secDelItemTitle:SetFont((DBALib.Fonts and DBALib.Fonts.Body_lg) or "DermaDefaultBold")
    secDelItemTitle:SetTextColor(
        (DBALib.Theme and DBALib.Theme.palette and DBALib.Theme.palette.semantic
         and DBALib.Theme.palette.semantic.danger) or Color(220, 60, 60)
    )
    secDelItemTitle:SizeToContents()
    secDelItemTitle:Dock(TOP)
    secDelItemTitle:DockMargin(S(8), S(8), S(8), S(4))

    -- Dropdown des items
    local itemOptions = {}
    for id, item in pairs(Armurier.CatalogData.items) do
        itemOptions[#itemOptions + 1] = {
            id    = tostring(item.id),
            label = string.format("#%d [%s] %s (%d €)", item.id, item.kind, item.class, item.price),
        }
    end
    table.sort(itemOptions, function(a, b) return tonumber(a.id) < tonumber(b.id) end)

    if #itemOptions == 0 then
        itemOptions = { { id = "0", label = "(aucun item)" } }
    end

    local delItemForm = DBALib.UI.Form({
        parent      = canvas,
        fields      = {
            {
                id      = "item_id",
                label   = "Item à supprimer",
                type    = "dropdown",
                options = itemOptions,
                value   = itemOptions[1] and itemOptions[1].id or "0",
                searchable = true,
            },
        },
        submitLabel = "Supprimer",
        showCancel  = false,
        onSubmit    = function(values)
            local item_id = tonumber(values.item_id) or 0
            if item_id <= 0 then
                DBALib.UI.Notify({ message = "Sélectionnez un item valide.", type = "warning", duration = 3 })
                return
            end

            local item = Armurier.CatalogData.items[item_id]
            local itemName = item and item.class or ("#" .. item_id)

            DBALib.UI.Confirm({
                title   = "Supprimer l'item",
                message = "Supprimer '" .. itemName .. "' du catalogue ?",
                onYes   = function()
                    net.Start("Armurier.AdminRemoveItem")
                        net.WriteUInt(item_id, 16)
                    net.SendToServer()
                end,
            })
        end,
    })
    if IsValid(delItemForm) then
        delItemForm:Dock(TOP)
        delItemForm:DockMargin(S(8), S(4), S(8), S(8))
    end
end

-- ============================================================================
-- Armurier.UI.OpenMenu — appelé depuis cl_net.lua quand le serveur autorise
-- ============================================================================

function Armurier.UI.OpenMenu(caisse)
    if not dbaLibReady() then return end

    -- Un seul menu à la fois
    if IsValid(CurrentFrame) then
        closeFrame()
        return
    end

    if not IsValid(caisse) then
        DBALib.UI.Notify({ message = "Caisse invalide.", type = "danger", duration = 3 })
        return
    end

    CurrentCaisse = caisse

    DBALib.Sound("ui_open")

    local S      = DBALib.Scale
    local isAdmin = LocalPlayer():IsAdmin()

    -- Construire la liste des onglets
    local tabs = {
        {
            id      = "weapons",
            label   = "Armes",
            builder = function(p)
                buildPagePanel(p, "weapon", caisse)
            end,
        },
        {
            id      = "ammo",
            label   = "Munitions",
            builder = function(p)
                buildPagePanel(p, "ammo", caisse)
            end,
        },
        {
            id      = "accessories",
            label   = "Autres",
            builder = function(p)
                buildPagePanel(p, "accessory", caisse)
            end,
        },
    }

    -- Onglet admin uniquement si admin (vérification client légère — serveur re-valide)
    if isAdmin then
        tabs[#tabs + 1] = {
            id      = "admin",
            label   = "Gerer",
            builder = function(p)
                buildAdminPanel(p, caisse)
            end,
        }
    end

    local frameW = S(860)
    local frameH = S(620)

    -- Hauteur réservée pour la barre de renommage (visible si armurier ou admin)
    local canRename  = (TEAM_ARMURIER and LocalPlayer():Team() == TEAM_ARMURIER)
                       or LocalPlayer():IsAdmin()
    local renameBarH = canRename and S(44) or 0

    CurrentFrame = DBALib.UI.Frame({
        title     = "Caisse Armurier",
        w         = frameW,
        h         = frameH,
        draggable = true,
        closable  = true,
        content   = function(body)
            -- ----------------------------------------------------------------
            -- Barre de renommage (visible uniquement pour armurier + admin)
            -- ----------------------------------------------------------------
            if canRename then
                local renameBar = vgui.Create("DPanel", body)
                renameBar:Dock(TOP)
                renameBar:SetTall(renameBarH)
                renameBar:DockMargin(0, 0, 0, S(4))
                renameBar:SetPaintBackground(false)

                local renameBtn = DBALib.UI.Button({
                    parent  = renameBar,
                    label   = "Renommer la boutique",
                    variant = "ghost",
                    size    = "sm",
                    onClick = function()
                        if not IsValid(CurrentCaisse) then
                            DBALib.UI.Notify({ message = "Caisse invalide.", type = "danger", duration = 3 })
                            return
                        end

                        local currentName = CurrentCaisse:GetNW2String("armurier_name", "")
                        local maxLen = (Armurier.Config and Armurier.Config.ShopNameMaxLen) or 32

                        -- Capture caisse pour la closure (CurrentCaisse peut changer)
                        local targetCaisse = CurrentCaisse

                        DBALib.UI.Frame({
                            title     = "Renommer la boutique",
                            w         = S(420),
                            h         = S(230),
                            draggable = true,
                            closable  = true,
                            content   = function(modalBody)
                                local form = DBALib.UI.Form({
                                    parent = modalBody,
                                    w      = 360,
                                    fields = {
                                        {
                                            id          = "name",
                                            label       = "Nom de la boutique",
                                            type        = "text",
                                            placeholder = currentName ~= "" and currentName
                                                          or ((Armurier.Config and Armurier.Config.DefaultShopName) or "Armurerie"),
                                            maxLength   = maxLen,
                                            validator   = DBALib and DBALib.Validators
                                                          and DBALib.Validators.MaxLength
                                                          and DBALib.Validators.MaxLength(maxLen)
                                                          or nil,
                                        },
                                    },
                                    submitLabel = "Valider",
                                    showCancel  = false,
                                    onSubmit    = function(values)
                                        local newName = string.sub(tostring(values.name or ""), 1, maxLen)
                                        net.Start("Armurier.SetCaisseName")
                                            net.WriteEntity(targetCaisse)
                                            net.WriteString(newName)
                                        net.SendToServer()
                                        DBALib.Sound("ui_click")
                                    end,
                                })
                                if IsValid(form) then
                                    -- PAS de Dock(FILL) : le Form calcule sa taille (et la
                                    -- position absolue de son bouton) une seule fois au build.
                                    -- Le docker écraserait cette taille → bouton coupé.
                                    -- On le centre simplement à sa taille naturelle.
                                    local fw = form:GetWide()
                                    form:SetPos(math.floor((modalBody:GetWide() - fw) / 2), S(8))
                                end
                            end,
                        })
                    end,
                })
                if IsValid(renameBtn) then
                    renameBtn:Dock(LEFT)
                    renameBtn:DockMargin(0, S(5), S(8), S(5))
                end
            end

            -- ----------------------------------------------------------------
            -- Onglets principaux
            -- ----------------------------------------------------------------
            -- Restaurer l'onglet actif après un refresh (sinon retour sur "Armes"
            -- à chaque action admin). On retombe sur "weapons" si l'onglet mémorisé
            -- n'existe pas (ex: onglet admin mémorisé mais joueur non-admin).
            local wanted = Armurier.UI._lastTab or "weapons"
            local exists = false
            for _, t in ipairs(tabs) do
                if t.id == wanted then exists = true break end
            end
            if not exists then wanted = "weapons" end

            local tabsPanel = DBALib.UI.Tabs({
                parent   = body,
                tabs     = tabs,
                default  = wanted,
                onChange = function(id)
                    Armurier.UI._lastTab = id
                end,
            })
            if IsValid(tabsPanel) then
                tabsPanel:Dock(FILL)
            end
        end,
    })

    if IsValid(CurrentFrame) then
        local origClose = CurrentFrame.OnClose
        CurrentFrame.OnClose = function(self)
            CurrentFrame  = nil
            CurrentCaisse = nil
            if origClose then origClose(self) end
        end
    end
end
