local _, ns = ...

------------------------------------------------------------
-- Constants
------------------------------------------------------------
local PAD           = 14
local ICON_SIZE     = 32
local ICON_GAP      = 14
local ROW_H         = 40
local ROW_GAP       = 1
local SOURCE_W      = 190
local SECTION_HDR_H = 24
local FILTER_H      = 38
local SLOT_BAR_H    = 24
local HEADER_H      = 34

-- Two-row slot tab layout: armor on top, accessories+weapons on bottom
local SLOT_ROW_TOP = {
    { name = "Head",     label = "Head"     },
    { name = "Shoulder", label = "Shoulder" },
    { name = "Chest",    label = "Chest"    },
    { name = "Wrist",    label = "Wrist"    },
    { name = "Hands",    label = "Hands"    },
    { name = "Waist",    label = "Waist"    },
    { name = "Legs",     label = "Legs"     },
    { name = "Feet",     label = "Feet"     },
    { name = "Back",     label = "Back"     },
}
local SLOT_ROW_BOTTOM = {
    { name = "Neck",      label = "Neck"      },
    { name = "Finger",    label = "Ring"      },
    { name = "Trinket",   label = "Trinket"   },
    { name = "Main Hand", label = "Main Hand" },
    { name = "Off Hand",  label = "Off Hand"  },
}

------------------------------------------------------------
-- Frame/Icon/Label Pools
------------------------------------------------------------
local rowPool      = {}
local iconPool     = {}
local labelPool    = {}
local activeRows   = {}
local activeIcons  = {}
local activeLabels = {}

local function AcquireRow(parent)
    local row = table.remove(rowPool)
    if not row then
        row = CreateFrame("Frame", nil, parent)

        local srcLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        srcLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 10, -6)
        srcLabel:SetWidth(SOURCE_W - 16)
        srcLabel:SetJustifyH("LEFT")
        srcLabel:SetWordWrap(false)
        ns.ApplyTextShadow(srcLabel)
        row.srcLabel = srcLabel

        local bossLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        bossLabel:SetPoint("TOPLEFT", srcLabel, "BOTTOMLEFT", 0, -1)
        bossLabel:SetWidth(SOURCE_W - 16)
        bossLabel:SetJustifyH("LEFT")
        bossLabel:SetWordWrap(false)
        bossLabel:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
        ns.ApplyTextShadow(bossLabel)
        row.bossLabel = bossLabel

        local bg = row:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        row.bg = bg

        local hl = row:CreateTexture(nil, "HIGHLIGHT")
        hl:SetAllPoints()
        hl:SetColorTexture(1, 1, 1, 0.03)
        hl:SetBlendMode("ADD")

        row:EnableMouse(true)
    end

    row:SetParent(parent)
    row:ClearAllPoints()
    row:Show()
    table.insert(activeRows, row)
    return row
end

local function AcquireIcon(parent)
    local icon = table.remove(iconPool)
    if not icon then
        icon = CreateFrame("Button", nil, parent)
        icon:SetSize(ICON_SIZE, ICON_SIZE)
        icon:EnableMouse(true)

        -- 2px border: background fills full area, item texture inset on top
        local borderTex = icon:CreateTexture(nil, "BACKGROUND")
        borderTex:SetAllPoints()
        icon.borderTex = borderTex

        local tex = icon:CreateTexture(nil, "ARTWORK")
        tex:SetPoint("TOPLEFT", 2, -2)
        tex:SetPoint("BOTTOMRIGHT", -2, 2)
        tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        icon.tex = tex

        local tierBadge = icon:CreateFontString(nil, "OVERLAY")
        local fontPath = GameFontNormal:GetFont()
        tierBadge:SetFont(fontPath, 10, "OUTLINE")
        tierBadge:SetPoint("TOPRIGHT", icon, "TOPRIGHT", 2, 2)
        tierBadge:Hide()
        icon.tierBadge = tierBadge

        icon:SetScript("OnEnter", function(self)
            if self.itemLink then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetHyperlink(self.itemLink)
                GameTooltip:Show()
            end
        end)
        icon:SetScript("OnLeave", function() GameTooltip:Hide() end)

        icon:RegisterForClicks("LeftButtonUp")
        icon:SetScript("OnClick", function(self)
            if IsModifiedClick("CHATLINK") and self.itemLink then
                ChatEdit_InsertLink(self.itemLink)
            end
        end)
    end

    icon:SetParent(parent)
    icon:ClearAllPoints()
    icon.tierBadge:Hide()
    icon:Show()
    table.insert(activeIcons, icon)
    return icon
end

local function AcquireLabel(parent)
    local label = table.remove(labelPool)
    if not label then
        label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    end
    label:SetParent(parent)
    label:ClearAllPoints()
    label:SetText("")
    label:SetWidth(0)        -- reset any previous width constraint
    label:SetWordWrap(false)
    label:Show()
    table.insert(activeLabels, label)
    return label
end

local function ReleaseAll()
    GameTooltip:Hide()
    for _, r in ipairs(activeRows) do r:Hide(); r:ClearAllPoints(); table.insert(rowPool, r) end
    wipe(activeRows)
    for _, ic in ipairs(activeIcons) do ic:Hide(); ic:ClearAllPoints(); ic.itemLink = nil; ic.tierBadge:Hide(); table.insert(iconPool, ic) end
    wipe(activeIcons)
    for _, lb in ipairs(activeLabels) do lb:Hide(); lb:ClearAllPoints(); table.insert(labelPool, lb) end
    wipe(activeLabels)
end

------------------------------------------------------------
-- Trinket Tier Badge Helper
------------------------------------------------------------
local TIER_BADGE_TEXT = { S = "|cffff8000S|r", A = "|cffa335eeA|r", B = "|cff0070ddB|r" }

local function ApplyTrinketBadge(iconFrame, itemID)
    if not itemID then return end
    local specKey = ns.GetPlayerSpecKey and ns:GetPlayerSpecKey()
    if not specKey then return end
    local rankings = ns.GetTrinketRankings and ns:GetTrinketRankings(specKey)
    if not rankings then return end
    for _, entry in ipairs(rankings) do
        if entry.id == itemID and entry.tier and TIER_BADGE_TEXT[entry.tier] then
            iconFrame.tierBadge:SetText(TIER_BADGE_TEXT[entry.tier])
            iconFrame.tierBadge:Show()
            return
        end
    end
end

------------------------------------------------------------
-- Helper: create a filter chip button
------------------------------------------------------------
local function MakeChip(parent, text, width, onClick)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(width, 24)
    btn:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 8,
        insets   = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    btn:SetBackdropColor(0.08, 0.08, 0.08, 0.9)
    btn:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
    ns.SmoothFrame(btn)
    ns.AddGlowHighlight(btn, 0.06)

    local lbl = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    lbl:SetPoint("CENTER")
    lbl:SetText(text)
    lbl:SetTextColor(unpack(ns.COLORS.TEXT_SECONDARY))
    ns.ApplyTextShadow(lbl)
    btn.label = lbl

    btn:SetScript("OnClick", onClick)
    return btn
end

------------------------------------------------------------
-- Layout helper: reposition bars + content for app/standalone
------------------------------------------------------------
local function LayoutSlotButtons(f)
    if not f._slotRowInfo then return end
    local barW = f._slotBar:GetWidth()
    if not barW or barW <= 0 then
        barW = f:GetWidth() - PAD * 2
    end
    -- Account for backdrop insets (4px each side)
    local innerW = barW - 8
    local innerPad = 4
    for _, info in ipairs(f._slotRowInfo) do
        local btnW = math.floor(innerW / info.rowCount)
        local rowY = innerPad + info.row * (SLOT_BAR_H + 1)
        info.btn:ClearAllPoints()
        info.btn:SetSize(btnW, SLOT_BAR_H)
        info.btn:SetPoint("TOPLEFT", f._slotBar, "TOPLEFT", innerPad + info.col * btnW, -rowY)
    end
end

local function UpdateLayout(f)
    local topY = f.inAppMode and 0 or HEADER_H

    f._filterCard:ClearAllPoints()
    f._filterCard:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, -topY)
    f._filterCard:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PAD, -topY)

    f._slotBar:ClearAllPoints()
    f._slotBar:SetPoint("TOPLEFT", f._filterCard, "BOTTOMLEFT", 0, -4)
    f._slotBar:SetPoint("TOPRIGHT", f._filterCard, "BOTTOMRIGHT", 0, -4)

    f._content:ClearAllPoints()
    f._content:SetPoint("TOPLEFT", f._slotBar, "BOTTOMLEFT", 0, -6)
    f._content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PAD, 8)

    -- Defer button layout by one frame so anchors have resolved
    C_Timer.After(0, function()
        LayoutSlotButtons(f)
    end)
end

------------------------------------------------------------
-- Create Main Loot Browser Frame
------------------------------------------------------------
function ns:CreateLootBrowserFrame()
    if ns.LootBrowserFrame then return end

    local FRAME_W, FRAME_H = ns:GetAppFrameSize()
    local CW = FRAME_W - PAD * 2

    local f = CreateFrame("Frame", "YippYappLootBrowser", UIParent, "BackdropTemplate")
    f:SetSize(FRAME_W, FRAME_H)
    f:SetPoint("CENTER")
    f:SetFrameStrata("HIGH")
    f:SetClampedToScreen(true)
    f:EnableMouse(true)
    f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 16,
        insets   = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    f:SetBackdropColor(0.05, 0.05, 0.05, 0.97)
    f:SetBackdropBorderColor(0.35, 0.35, 0.35, 1)
    ns.SmoothFrame(f)
    f:Hide()
    ns.LootBrowserFrame = f

    -- Standalone header
    local titleFs = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    titleFs:SetPoint("TOPLEFT", PAD, -10)
    titleFs:SetText("|cff00ccffLoot Browser|r")
    ns.ApplyTextShadow(titleFs)
    f._titleFs = titleFs

    local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -2, -2)
    closeBtn:SetScript("OnClick", function() f:Hide() end)
    f._closeBtn = closeBtn

    -- ================================================================
    -- FILTER CARD (spec dropdown + stats in one row)
    -- ================================================================
    local filterCard = CreateFrame("Frame", nil, f, "BackdropTemplate")
    filterCard:SetHeight(FILTER_H)
    filterCard:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, -HEADER_H)
    filterCard:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PAD, -HEADER_H)
    filterCard:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 10,
        insets   = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    filterCard:SetBackdropColor(0.08, 0.08, 0.08, 0.9)
    filterCard:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
    ns.SmoothFrame(filterCard)
    f._filterCard = filterCard

    -- Spec dropdown trigger button
    local specLabel = filterCard:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    specLabel:SetPoint("LEFT", 10, 0)
    specLabel:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
    specLabel:SetText("Spec:")
    ns.ApplyTextShadow(specLabel)

    local specBtn = CreateFrame("Button", nil, filterCard, "BackdropTemplate")
    specBtn:SetSize(140, 24)
    specBtn:SetPoint("LEFT", specLabel, "RIGHT", 6, 0)
    specBtn:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 8,
        insets   = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    specBtn:SetBackdropColor(0.06, 0.06, 0.06, 0.9)
    specBtn:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
    ns.SmoothFrame(specBtn)
    ns.AddGlowHighlight(specBtn, 0.06)

    local specIcon = specBtn:CreateTexture(nil, "ARTWORK")
    specIcon:SetSize(16, 16)
    specIcon:SetPoint("LEFT", 4, 0)
    specIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    specBtn._icon = specIcon

    local specNameFs = specBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    specNameFs:SetPoint("LEFT", specIcon, "RIGHT", 4, 0)
    ns.ApplyTextShadow(specNameFs)
    specBtn._nameFs = specNameFs

    local specArrow = specBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    specArrow:SetPoint("RIGHT", -4, 0)
    specArrow:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
    specArrow:SetText("v")
    f._specBtn = specBtn

    -- Spec dropdown popup
    local specDropdown = CreateFrame("Frame", "YippYappLootSpecDropdown", UIParent, "BackdropTemplate")
    specDropdown:SetSize(160, 10)
    specDropdown:SetFrameStrata("DIALOG")
    specDropdown:SetClampedToScreen(true)
    specDropdown:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 12,
        insets   = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    specDropdown:SetBackdropColor(0.06, 0.06, 0.06, 0.97)
    specDropdown:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
    ns.SmoothFrame(specDropdown)
    specDropdown:EnableMouse(true)
    specDropdown:Hide()
    f._specDropdown = specDropdown

    specDropdown:SetScript("OnShow", function()
        tinsert(UISpecialFrames, "YippYappLootSpecDropdown")
    end)
    specDropdown:SetScript("OnHide", function()
        for i = #UISpecialFrames, 1, -1 do
            if UISpecialFrames[i] == "YippYappLootSpecDropdown" then
                table.remove(UISpecialFrames, i)
                break
            end
        end
    end)

    -- Build spec rows (up to 4)
    local specRows = {}
    local ddY = 6
    for si = 1, 4 do
        local row = CreateFrame("Button", nil, specDropdown)
        row:SetSize(148, 26)
        row:SetPoint("TOPLEFT", 6, -ddY)

        local rowHl = row:CreateTexture(nil, "HIGHLIGHT")
        rowHl:SetAllPoints()
        rowHl:SetColorTexture(1, 1, 1, 0.05)
        rowHl:SetBlendMode("ADD")

        local rowIcon = row:CreateTexture(nil, "ARTWORK")
        rowIcon:SetSize(18, 18)
        rowIcon:SetPoint("LEFT", 4, 0)
        rowIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        row._icon = rowIcon

        local rowName = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        rowName:SetPoint("LEFT", rowIcon, "RIGHT", 6, 0)
        ns.ApplyTextShadow(rowName)
        row._nameFs = rowName

        local rowCheck = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        rowCheck:SetPoint("RIGHT", -4, 0)
        row._check = rowCheck

        row.specIndex = si
        row:SetScript("OnClick", function(self)
            specDropdown:Hide()
            ns:LootBrowser_SwitchSpec(self.specIndex)
        end)
        row:Hide()
        specRows[si] = row
        ddY = ddY + 26
    end
    specDropdown:SetHeight(ddY + 4)
    f._specRows = specRows

    specBtn:SetScript("OnClick", function(self)
        if specDropdown:IsShown() then
            specDropdown:Hide()
        else
            -- Populate rows
            local numSpecs = GetNumSpecializations()
            local ddH = 6
            for i = 1, 4 do
                local row = specRows[i]
                if i <= numSpecs then
                    local specID, specName, _, specIcon = GetSpecializationInfo(i)
                    if specID then
                        row._icon:SetTexture(specIcon)
                        local isActive = (ns.lootBrowserState.selectedSpecIndex == i)
                        if isActive then
                            row._nameFs:SetText("|cff00ccff" .. specName .. "|r")
                            row._check:SetText("|cff00ccff>|r")
                        else
                            row._nameFs:SetText("|cffaaaaaa" .. specName .. "|r")
                            row._check:SetText("")
                        end
                        row:Show()
                        ddH = ddH + 26
                    else
                        row:Hide()
                    end
                else
                    row:Hide()
                end
            end
            specDropdown:SetHeight(ddH + 4)
            specDropdown:ClearAllPoints()
            specDropdown:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
            specDropdown:Show()
        end
    end)
    specBtn:SetScript("OnEnter", function(self)
        self:SetBackdropBorderColor(0.0, 0.8, 1.0, 0.6)
    end)
    specBtn:SetScript("OnLeave", function(self)
        self:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
    end)

    -- Divider
    local divTex = filterCard:CreateTexture(nil, "ARTWORK")
    divTex:SetSize(1, 20)
    divTex:SetPoint("LEFT", specBtn, "RIGHT", 12, 0)
    divTex:SetColorTexture(0.25, 0.25, 0.25, 0.5)

    -- Stat filter chips
    local statLabel = filterCard:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    statLabel:SetPoint("LEFT", divTex, "RIGHT", 12, 0)
    statLabel:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
    statLabel:SetText("Stats:")
    ns.ApplyTextShadow(statLabel)

    local statAllBtn = MakeChip(filterCard, "All", 36, function() ns:LootBrowser_ClearStats() end)
    statAllBtn:SetPoint("LEFT", statLabel, "RIGHT", 6, 0)
    f._statAllBtn = statAllBtn

    local statButtons = {}
    for i, statInfo in ipairs(ns.LOOT_SECONDARY_STATS) do
        local btn = MakeChip(filterCard, statInfo.short, 52, function(self)
            ns:LootBrowser_ToggleStat(self.statKey)
        end)
        btn.statKey = statInfo.key
        if i == 1 then
            btn:SetPoint("LEFT", statAllBtn, "RIGHT", 4, 0)
        else
            btn:SetPoint("LEFT", statButtons[i - 1], "RIGHT", 4, 0)
        end
        statButtons[i] = btn
    end
    f._statButtons = statButtons

    -- ================================================================
    -- SLOT TAB BAR (two rows in a card, below filter card)
    -- ================================================================
    local slotBar = CreateFrame("Frame", nil, f, "BackdropTemplate")
    slotBar:SetHeight(SLOT_BAR_H * 2 + 6)  -- two rows + inner padding
    slotBar:SetPoint("TOPLEFT", filterCard, "BOTTOMLEFT", 0, -4)
    slotBar:SetPoint("TOPRIGHT", filterCard, "BOTTOMRIGHT", 0, -4)
    slotBar:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 10,
        insets   = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    slotBar:SetBackdropColor(0.08, 0.08, 0.08, 0.9)
    slotBar:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
    ns.SmoothFrame(slotBar)
    f._slotBar = slotBar

    local slotBarButtons = {}
    local slotRowInfo = {}  -- { {btn, rowIdx, colIdx, rowCount}, ... } for relayout

    -- Helper to build one row of tabs
    local function BuildSlotRow(rowDefs, rowIdx, rowY)
        local count = #rowDefs
        for i, def in ipairs(rowDefs) do
            local slotInfo
            for _, sf in ipairs(ns.LOOT_SLOT_FILTERS) do
                if sf.name == def.name then slotInfo = sf; break end
            end
            if slotInfo then
                local btn = ns.CreateUnderlineTab(slotBar, def.label, { 0.0, 0.8, 1.0 })
                btn:SetHeight(SLOT_BAR_H)
                btn.label:SetFontObject("GameFontNormalSmall")

                btn.slotFilter = slotInfo.filter
                btn.slotName = slotInfo.name
                btn:SetScript("OnClick", function(self)
                    ns:LootBrowser_SwitchSlot(self.slotFilter, self.slotName)
                end)
                table.insert(slotBarButtons, btn)
                table.insert(slotRowInfo, { btn = btn, row = rowIdx, col = i - 1, rowCount = count })
            end
        end
    end

    BuildSlotRow(SLOT_ROW_TOP, 0, 0)
    BuildSlotRow(SLOT_ROW_BOTTOM, 1, SLOT_BAR_H + 2)
    f._slotBarButtons = slotBarButtons
    f._slotRowInfo = slotRowInfo

    -- ================================================================
    -- CONTENT AREA (below slot bar, scrollable loot display)
    -- ================================================================
    local content = CreateFrame("Frame", nil, f)
    content:SetPoint("TOPLEFT", slotBar, "BOTTOMLEFT", 0, -6)
    content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PAD, 8)
    f._content = content

    local scrollFrame = CreateFrame("ScrollFrame", nil, content, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 0, 0)
    scrollFrame:SetPoint("BOTTOMRIGHT", -24, 0)

    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetHeight(1)
    scrollChild:SetWidth(1)
    scrollFrame:SetScrollChild(scrollChild)
    f._scrollFrame = scrollFrame
    f._scrollChild = scrollChild

    local loadingText = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    loadingText:SetPoint("CENTER")
    loadingText:SetText("|cff555555Scanning loot...|r")
    loadingText:Hide()
    f._loadingText = loadingText

    local noItemsText = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    noItemsText:SetPoint("CENTER")
    noItemsText:Hide()
    f._noItemsText = noItemsText

    -- ESC + cleanup
    f:SetScript("OnShow", function()
        if f.inAppMode then return end
        tinsert(UISpecialFrames, "YippYappLootBrowser")
    end)
    f:SetScript("OnHide", function()
        ReleaseAll()
        GameTooltip:Hide()
        if f._specDropdown then f._specDropdown:Hide() end
        for i = #UISpecialFrames, 1, -1 do
            if UISpecialFrames[i] == "YippYappLootBrowser" then
                table.remove(UISpecialFrames, i)
                break
            end
        end
    end)
end

------------------------------------------------------------
-- Update filter card state
------------------------------------------------------------
function ns:LootBrowser_UpdateSpecBar()
    local f = ns.LootBrowserFrame
    if not f or not f._specBtn then return end
    local idx = ns.lootBrowserState.selectedSpecIndex
    if idx then
        local specID, specName, _, specIcon = GetSpecializationInfo(idx)
        if specID then
            f._specBtn._icon:SetTexture(specIcon)
            f._specBtn._nameFs:SetText("|cff00ccff" .. specName .. "|r")
        end
    end
end

function ns:LootBrowser_UpdateStatBar()
    local f = ns.LootBrowserFrame
    if not f then return end
    local stats = ns.lootBrowserState.selectedStats
    local numSel = #stats

    if numSel == 0 then
        f._statAllBtn:SetBackdropBorderColor(0.0, 0.8, 1.0, 0.6)
        f._statAllBtn.label:SetTextColor(0.0, 0.8, 1.0)
    else
        f._statAllBtn:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
        f._statAllBtn.label:SetTextColor(unpack(ns.COLORS.TEXT_SECONDARY))
    end

    for _, btn in ipairs(f._statButtons) do
        local isSel = false
        for _, key in ipairs(stats) do
            if key == btn.statKey then isSel = true; break end
        end
        if isSel then
            btn:SetBackdropBorderColor(0.0, 0.8, 1.0, 0.6)
            btn.label:SetTextColor(0.0, 0.8, 1.0)
        elseif numSel >= 2 then
            btn:SetBackdropBorderColor(0.15, 0.15, 0.15, 0.5)
            btn.label:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
        else
            btn:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
            btn.label:SetTextColor(unpack(ns.COLORS.TEXT_SECONDARY))
        end
    end
end

function ns:LootBrowser_UpdateSlotBar()
    local f = ns.LootBrowserFrame
    if not f then return end
    for _, btn in ipairs(f._slotBarButtons) do
        if ns.lootBrowserState.selectedSlot == btn.slotFilter then
            ns.SetTabActive(btn)
        else
            ns.SetTabInactive(btn)
        end
    end
end

------------------------------------------------------------
-- Show / Refresh
------------------------------------------------------------
function ns:LootBrowser_ShowPage()
    local f = ns.LootBrowserFrame
    if not f then return end

    LayoutSlotButtons(f)
    ns:LootBrowser_UpdateSpecBar()
    ns:LootBrowser_UpdateStatBar()
    ns:LootBrowser_UpdateSlotBar()

    ns:LootBrowser_RefreshDisplay()
end

function ns:LootBrowser_RefreshDisplay()
    local f = ns.LootBrowserFrame
    if not f then return end

    ReleaseAll()
    f._loadingText:Hide()
    f._noItemsText:Hide()
    f._scrollFrame:Hide()

    local specIndex = ns.lootBrowserState.selectedSpecIndex
    local slotFilter = ns.lootBrowserState.selectedSlot
    if not specIndex or slotFilter == nil then return end

    -- Set scroll child width now that frame has real dimensions
    local scrollW = f._scrollFrame:GetWidth()
    if scrollW and scrollW > 0 then
        f._scrollChild:SetWidth(scrollW)
    end

    f._loadingText:Show()

    local results = ns:ScanLootBrowserSlot(specIndex, slotFilter)
    local preFilterCount = results and #results or 0

    if ns.lootBrowserState.selectedStats and #ns.lootBrowserState.selectedStats > 0 then
        results, preFilterCount = ns:FilterLootByStats(results, ns.lootBrowserState.selectedStats)
    end

    f._loadingText:Hide()

    if not results or #results == 0 then
        if preFilterCount > 0 then
            local slotName = ns.lootBrowserState.selectedSlotName or "this slot"
            if slotName == "Trinket" then
                f._noItemsText:SetText("|cff555555No items match these stats.\n|cff444444(Trinkets often use proc effects instead of secondary stats.)|r")
            else
                f._noItemsText:SetText("|cff555555No items match these stats.|r")
            end
        else
            f._noItemsText:SetText("|cff555555No items found for this slot and spec.|r")
        end
        f._noItemsText:Show()
        return
    end

    f._scrollFrame:Show()

    local sortOrder = { dungeon = 1, raid = 2, worldboss = 3 }
    table.sort(results, function(a, b)
        local oa = sortOrder[a.sourceType] or 9
        local ob = sortOrder[b.sourceType] or 9
        if oa ~= ob then return oa < ob end
        return a.sourceName < b.sourceName
    end)

    local scrollChildW = f._scrollChild:GetWidth()
    if scrollChildW <= 1 then scrollChildW = 800 end
    local yOff = 0
    local lastSourceType = nil
    local colW = ICON_SIZE + ICON_GAP
    local isTrinketSlot = (ns.lootBrowserState.selectedSlotName == "Trinket")
    local rowIndex = 0

    for _, entry in ipairs(results) do
        if entry.sourceType ~= lastSourceType then
            if yOff > 0 then yOff = yOff + 6 end

            local sectionLabel = AcquireLabel(f._scrollChild)
            sectionLabel:SetFontObject("GameFontNormal")
            sectionLabel:SetPoint("TOPLEFT", f._scrollChild, "TOPLEFT", 4, -yOff)
            sectionLabel:SetWidth(SOURCE_W - 10)
            sectionLabel:SetJustifyH("LEFT")

            local difficulties
            if entry.sourceType == "dungeon" then
                sectionLabel:SetText("Dungeons")
                difficulties = ns.LOOT_DIFFICULTIES.DUNGEON
            elseif entry.sourceType == "raid" then
                sectionLabel:SetText("Raids")
                difficulties = ns.LOOT_DIFFICULTIES.RAID
            else
                sectionLabel:SetText("World Bosses")
                difficulties = ns.LOOT_DIFFICULTIES.WORLD_BOSS
            end
            sectionLabel:SetTextColor(unpack(ns.COLORS.TEXT_HEADER))

            -- Difficulty column headers — position at icon center, no width clamp
            for di, diff in ipairs(difficulties) do
                local col = diff.colOffset or (di - 1)
                local dLabel = AcquireLabel(f._scrollChild)
                dLabel:SetFontObject("GameFontNormalSmall")
                dLabel:SetPoint("LEFT", f._scrollChild, "TOPLEFT",
                    SOURCE_W + col * colW, -yOff - 8)
                dLabel:SetJustifyH("LEFT")
                dLabel:SetText(diff.fullName)
                local c = ns.LOOT_DIFF_COLORS[diff.color]
                if c then dLabel:SetTextColor(c.r, c.g, c.b) end
            end

            yOff = yOff + SECTION_HDR_H
            lastSourceType = entry.sourceType
            rowIndex = 0
        end

        local difficulties
        if entry.sourceType == "dungeon" then
            difficulties = ns.LOOT_DIFFICULTIES.DUNGEON
        elseif entry.sourceType == "raid" then
            difficulties = ns.LOOT_DIFFICULTIES.RAID
        else
            difficulties = ns.LOOT_DIFFICULTIES.WORLD_BOSS
        end

        local maxItems = 1
        for _, diff in ipairs(difficulties) do
            local diffItems = entry.items[diff.id]
            if diffItems and #diffItems > maxItems then maxItems = #diffItems end
        end
        local rowHeight = (maxItems > 1) and (8 + maxItems * (ICON_SIZE + ICON_GAP)) or ROW_H

        local row = AcquireRow(f._scrollChild)
        row:SetSize(scrollChildW, rowHeight)
        row:SetPoint("TOPLEFT", f._scrollChild, "TOPLEFT", 0, -yOff)

        if rowIndex % 2 == 0 then
            row.bg:SetColorTexture(1, 1, 1, 0.02)
        else
            row.bg:SetColorTexture(0, 0, 0, 0)
        end

        if entry.sourceType == "dungeon" then
            row.srcLabel:SetTextColor(0.12, 0.75, 0.12)
        elseif entry.sourceType == "raid" then
            row.srcLabel:SetTextColor(0.64, 0.21, 0.93)
        else
            row.srcLabel:SetTextColor(0.0, 0.44, 0.87)
        end
        row.srcLabel:SetText(entry.sourceName)
        row.bossLabel:SetText(entry.bossName)

        for di, diff in ipairs(difficulties) do
            local col = diff.colOffset or (di - 1)
            local diffItems = entry.items[diff.id]
            if diffItems then
                for itemIdx, item in ipairs(diffItems) do
                    local iconFrame = AcquireIcon(row)
                    local iy = -4 - ((itemIdx - 1) * (ICON_SIZE + ICON_GAP))
                    iconFrame:SetPoint("TOPLEFT", row, "TOPLEFT", SOURCE_W + col * colW, iy)

                    if item.icon then
                        iconFrame.tex:SetTexture(item.icon)
                    elseif item.itemID then
                        local _, _, _, _, iconTex = GetItemInfoInstant(item.itemID)
                        iconFrame.tex:SetTexture(iconTex)
                    end

                    iconFrame.itemLink = item.itemLink

                    local c = ns.LOOT_DIFF_COLORS[diff.color]
                    if c then
                        iconFrame.borderTex:SetColorTexture(c.r, c.g, c.b, 1.0)
                    end

                    if isTrinketSlot then
                        ApplyTrinketBadge(iconFrame, item.itemID)
                    end
                end
            end
        end

        yOff = yOff + rowHeight + ROW_GAP
        rowIndex = rowIndex + 1
    end

    f._scrollChild:SetHeight(yOff + 20)
end

------------------------------------------------------------
-- App Mode (embedded in AppFrame)
------------------------------------------------------------
function ns:SetLootBrowserAppMode(enabled, contentWidth, contentHeight)
    local f = ns.LootBrowserFrame
    if not f then return end

    if enabled then
        f:SetMovable(false)
        f:EnableMouse(false)
        f:SetBackdrop(nil)
        local dw, dh = ns:GetAppFrameSize()
        f:SetSize(contentWidth or dw, contentHeight or (dh - 34))
        f._titleFs:Hide()
        f._closeBtn:Hide()
    else
        f:SetMovable(true)
        f:EnableMouse(true)
        f:SetBackdrop({
            bgFile   = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            edgeSize = 16,
            insets   = { left = 4, right = 4, top = 4, bottom = 4 },
        })
        f:SetBackdropColor(0.05, 0.05, 0.05, 0.97)
        f:SetBackdropBorderColor(0.35, 0.35, 0.35, 1)
        local dw, dh = ns:GetAppFrameSize()
        f:SetSize(dw, dh)
        f._titleFs:Show()
        f._closeBtn:Show()
    end

    UpdateLayout(f)
end
