local _, ns = ...

------------------------------------------------------------
-- Constants
------------------------------------------------------------
local PAD           = 14
local ICON_SIZE     = 34
local ICON_GAP      = 10
local ROW_H         = 54
local ROW_GAP       = 0
local SOURCE_W      = 200
local ITEMS_LEFT    = 248
local FILTER_H      = 38
local VIEW_TAB_H    = 28
local HEADER_H      = 34

------------------------------------------------------------
-- Difficulty border color lookup (by diffID)
------------------------------------------------------------
local DIFF_BORDER_COLORS = {
    [1]  = ns.LOOT_DIFF_COLORS.GREEN,   -- Normal dungeon
    [2]  = ns.LOOT_DIFF_COLORS.BLUE,    -- Heroic dungeon
    [23] = ns.LOOT_DIFF_COLORS.PURPLE,  -- Mythic dungeon
    [17] = ns.LOOT_DIFF_COLORS.GREEN,   -- LFR
    [14] = ns.LOOT_DIFF_COLORS.BLUE,    -- Normal raid
    [15] = ns.LOOT_DIFF_COLORS.PURPLE,  -- Heroic raid
    [16] = ns.LOOT_DIFF_COLORS.PURPLE,  -- Mythic raid
    [0]  = ns.LOOT_DIFF_COLORS.PURPLE,  -- World boss
}

-- Muted tan for mount rewards — visually distinct from any difficulty tier.
local MOUNT_BORDER_COLOR = { r = 0.60, g = 0.45, b = 0.22 }

-- Item-class helpers. Enum.ItemClass.Miscellaneous == 15, Recipe == 9.
-- Miscellaneous subclasses we keep: 2 Companion Pet, 5 Mount, 6 Mount Equipment.
-- Everything else in that class (junk, holiday, housing decor, other) is hidden.
local HIDDEN_MISC_SUBCLASSES = { [0] = true, [3] = true, [4] = true }

-- Name prefixes that indicate a profession recipe / housing blueprint.
-- Backup for items where classID info hasn't loaded yet.
local HIDDEN_NAME_PREFIXES = {
    "Pattern:", "Formula:", "Design:", "Recipe:", "Technique:",
    "Schematic:", "Plans:", "Manual:", "Blueprint:",
}

local function GetItemClassPair(itemID)
    if not itemID then return nil, nil end
    local _, _, _, _, _, classID, subClassID = GetItemInfoInstant(itemID)
    return classID, subClassID
end

local function IsMountItem(itemID)
    local classID, subClassID = GetItemClassPair(itemID)
    return classID == 15 and subClassID == 5
end

local function NameHasHiddenPrefix(name)
    if not name then return false end
    for _, prefix in ipairs(HIDDEN_NAME_PREFIXES) do
        if name:sub(1, #prefix) == prefix then return true end
    end
    return false
end

-- Substrings found anywhere in the tooltip that mark an item as hidden.
-- Housing decor tooltips include a "Housing Decor" header line.
local HIDDEN_TOOLTIP_SUBSTRINGS = {
    "Housing Decor",
}

local tooltipHiddenCache = {}
local tooltipHiddenCacheSize = 0
local TOOLTIP_CACHE_MAX = 2048

local function TooltipMarksHidden(itemID)
    local cached = tooltipHiddenCache[itemID]
    if cached ~= nil then return cached end
    local data = C_TooltipInfo and C_TooltipInfo.GetItemByID and C_TooltipInfo.GetItemByID(itemID)
    if not data or not data.lines then return false end  -- not caching nil yet (may resolve later)
    -- Simple bounded cache: wipe when we hit the cap rather than implementing
    -- full LRU. Loot browser sessions rarely see more than a few thousand items.
    if tooltipHiddenCacheSize >= TOOLTIP_CACHE_MAX then
        wipe(tooltipHiddenCache)
        tooltipHiddenCacheSize = 0
    end
    for _, line in ipairs(data.lines) do
        local text = line.leftText
        if text then
            for _, sub in ipairs(HIDDEN_TOOLTIP_SUBSTRINGS) do
                if text:find(sub, 1, true) then
                    tooltipHiddenCache[itemID] = true
                    tooltipHiddenCacheSize = tooltipHiddenCacheSize + 1
                    return true
                end
            end
        end
    end
    tooltipHiddenCache[itemID] = false
    tooltipHiddenCacheSize = tooltipHiddenCacheSize + 1
    return false
end

function ns:IsLootBrowserHidden(itemID)
    if not itemID then return false end

    local classID, subClassID = GetItemClassPair(itemID)
    -- Recipes: Pattern/Formula/Design/Recipe/Technique/Schematic etc
    if classID == 9 then return true end
    -- Miscellaneous junk/holiday/decor/other
    if classID == 15 and HIDDEN_MISC_SUBCLASSES[subClassID] then return true end

    if TooltipMarksHidden(itemID) then return true end

    local name = GetItemInfo(itemID)
    if NameHasHiddenPrefix(name) then return true end

    return false
end

------------------------------------------------------------
-- 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, "YippYappLootRowTemplate")

        -- bg and dungBg/dungBgMask come from the XML template
        row.dungBg:Hide()

        local srcLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightMedium")
        srcLabel:SetPoint("LEFT", row, "LEFT", 12, 4)
        srcLabel:SetWidth(SOURCE_W - 20)
        srcLabel:SetJustifyH("LEFT")
        srcLabel:SetWordWrap(false)
        ns.ApplyTextShadow(srcLabel)
        row.srcLabel = srcLabel

        -- Count label (used in favorites view / item count)
        local countLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        countLabel:SetPoint("TOPLEFT", srcLabel, "BOTTOMLEFT", 0, -1)
        countLabel:SetWidth(SOURCE_W - 16)
        countLabel:SetJustifyH("LEFT")
        countLabel:SetWordWrap(false)
        countLabel:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
        ns.ApplyTextShadow(countLabel)
        row.countLabel = countLabel

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

        row:EnableMouse(true)

        -- Scroll arrows (persistent, shown only when row overflows)
        local function MakeArrow(dir)
            local b = CreateFrame("Button", nil, row)
            b:SetSize(18, 28)
            local tex = b:CreateTexture(nil, "ARTWORK")
            tex:SetAllPoints()
            tex:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up")
            if dir == "right" then tex:SetTexCoord(1, 0, 0, 1) end
            b.tex = tex
            b:SetScript("OnEnter", function(s) s.tex:SetVertexColor(1, 0.9, 0.3) end)
            b:SetScript("OnLeave", function(s) s.tex:SetVertexColor(1, 1, 1) end)
            b:Hide()
            return b
        end
        row.leftArrow  = MakeArrow("left")
        row.rightArrow = MakeArrow("right")
        row._scrollIdx = 0
        row._itemFrames = {}
    end

    row:SetParent(parent)
    row:ClearAllPoints()
    row.countLabel:SetText("")
    row.countLabel:Hide()
    row.dungBg:Hide()
    row.leftArrow:Hide()
    row.rightArrow:Hide()
    row._scrollIdx = 0
    wipe(row._itemFrames)
    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

        -- Favorite star (larger, yellow border style)
        local favStar = icon:CreateTexture(nil, "OVERLAY", nil, 2)
        favStar:SetSize(18, 18)
        favStar:SetPoint("TOPLEFT", icon, "TOPLEFT", -4, 4)
        favStar:SetAtlas("PetJournal-FavoritesIcon")
        favStar:SetVertexColor(1.0, 0.82, 0.0)
        favStar:Hide()
        icon.favStar = favStar

        icon:SetScript("OnEnter", function(self)
            if self.itemLink or self.itemID then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                if self.itemLink then
                    GameTooltip:SetHyperlink(self.itemLink)
                else
                    GameTooltip:SetItemByID(self.itemID)
                end
                if self.itemID then
                    GameTooltip:AddLine(" ")
                    if ns:IsLootFavorite(self.itemID) then
                        GameTooltip:AddLine("|cffFFD100Click to unfavorite|r")
                    else
                        GameTooltip:AddLine("|cffFFD100Click to favorite|r")
                    end
                end
                GameTooltip:Show()
            end
        end)
        icon:SetScript("OnLeave", function() GameTooltip:Hide() end)

        icon:RegisterForClicks("LeftButtonUp")
        icon:SetScript("OnClick", function(self, button)
            if IsModifiedClick("CHATLINK") and self.itemLink then
                ChatEdit_InsertLink(self.itemLink)
                return
            end
            if self.itemID then
                ns:ToggleLootFavorite(self.itemID)
                local isFav = ns:IsLootFavorite(self.itemID)
                if isFav then
                    self.favStar:Show()
                    self.borderTex:SetColorTexture(1.0, 0.82, 0.0, 1.0)
                else
                    self.favStar:Hide()
                    -- Restore difficulty border
                    local diffColor = self._diffColor
                    if diffColor then
                        self.borderTex:SetColorTexture(diffColor.r, diffColor.g, diffColor.b, 1.0)
                    else
                        self.borderTex:SetColorTexture(0.3, 0.3, 0.3, 1.0)
                    end
                end
                -- Refresh if in favorites mode
                if ns.lootBrowserState.isFavoritesMode and ns.LootBrowser_RefreshDisplay then
                    C_Timer.After(0, function() ns:LootBrowser_RefreshDisplay() end)
                end
                -- Update slot dropdown badge
                if ns.LootBrowser_UpdateSlotDropdown then ns:LootBrowser_UpdateSlotDropdown() end
            end
        end)
    end

    icon:SetParent(parent)
    icon:ClearAllPoints()
    icon.tierBadge:Hide()
    icon.favStar:Hide()
    icon.itemID = nil
    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)
    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(); r.dungBg:Hide(); table.insert(rowPool, r) end
    wipe(activeRows)
    for _, ic in ipairs(activeIcons) do ic:Hide(); ic:ClearAllPoints(); ic.itemLink = nil; ic.itemID = nil; ic._diffColor = nil; ic:SetSize(ICON_SIZE, ICON_SIZE); ic.tierBadge:Hide(); ic.favStar:Hide(); ic.tex:SetTexture(nil); 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

------------------------------------------------------------
-- 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
------------------------------------------------------------
local function UpdateLayout(f)
    -- In-app mode: nest inside the page's inner panel (starts at -10 from
    -- page top). Offset the filter card a bit below that so it fits cleanly.
    local topY = f.inAppMode and 10 or HEADER_H
    local hPad = f.inAppMode and 0 or PAD

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

    -- In-app mode: no card border/background — just a bottom divider line.
    if f.inAppMode then
        f._filterCard:SetBackdrop(nil)
        if not f._filterDivider then
            local div = f._filterCard:CreateTexture(nil, "OVERLAY")
            div:SetColorTexture(0.35, 0.35, 0.35, 0.8)
            div:SetHeight(1)
            div:SetPoint("BOTTOMLEFT", f._filterCard, "BOTTOMLEFT", 0, 0)
            div:SetPoint("BOTTOMRIGHT", f._filterCard, "BOTTOMRIGHT", 0, 0)
            f._filterDivider = div
        end
        f._filterDivider:Show()
    else
        f._filterCard:SetBackdrop({
            bgFile   = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            edgeSize = 10,
            insets   = { left = 2, right = 2, top = 2, bottom = 2 },
        })
        f._filterCard:SetBackdropColor(0.08, 0.08, 0.08, 0.9)
        f._filterCard:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
        if f._filterDivider then f._filterDivider:Hide() end
    end

    f._viewTabBar:ClearAllPoints()
    local tabGap = f.inAppMode and 0 or -4
    f._viewTabBar:SetPoint("TOPLEFT", f._filterCard, "BOTTOMLEFT", 0, tabGap)
    f._viewTabBar:SetPoint("TOPRIGHT", f._filterCard, "BOTTOMRIGHT", 0, tabGap)

    f._content:ClearAllPoints()
    f._content:SetPoint("TOPLEFT", f._viewTabBar, "BOTTOMLEFT", -hPad, -2)
    f._content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 4)
    f._content:SetClipsChildren(true)
end

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

    local FRAME_W, FRAME_H = ns:GetAppFrameSize()

    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 + slot 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

    -- ============ CLASS DROPDOWN ============
    local classLabel = filterCard:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    classLabel:SetPoint("LEFT", 10, 0)
    classLabel:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
    classLabel:SetText("Class:")
    ns.ApplyTextShadow(classLabel)

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

    local classNameFs = classBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    classNameFs:SetPoint("LEFT", 6, 0)
    ns.ApplyTextShadow(classNameFs)
    classBtn._nameFs = classNameFs
    f._classBtn = classBtn

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

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

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

    local classRows = {}
    do
        local y = 6
        for ci = 1, GetNumClasses() do
            local className, classFile, classID = GetClassInfo(ci)
            if className and classID then
                local row = CreateFrame("Button", nil, classDropdown)
                row:SetSize(148, 22)
                row:SetPoint("TOPLEFT", 6, -y)
                local hl = row:CreateTexture(nil, "HIGHLIGHT")
                hl:SetAllPoints(); hl:SetColorTexture(1, 1, 1, 0.05); hl:SetBlendMode("ADD")
                local fs = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                fs:SetPoint("LEFT", 6, 0)
                local hex = (RAID_CLASS_COLORS[classFile] and RAID_CLASS_COLORS[classFile].colorStr) or "ffffffff"
                fs:SetText("|c" .. hex .. className .. "|r")
                row._check = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                row._check:SetPoint("RIGHT", -4, 0)
                row.classID = classID
                row:SetScript("OnClick", function(self)
                    classDropdown:Hide()
                    ns:LootBrowser_SwitchClass(self.classID)
                end)
                table.insert(classRows, row)
                y = y + 22
            end
        end
        classDropdown:SetHeight(y + 4)
    end

    classBtn:SetScript("OnClick", function(self)
        if classDropdown:IsShown() then
            classDropdown:Hide()
        else
            for _, row in ipairs(classRows) do
                row._check:SetText(row.classID == ns.lootBrowserState.selectedClassID and "|cff00ccff>|r" or "")
            end
            classDropdown:ClearAllPoints()
            classDropdown:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
            classDropdown:Show()
        end
    end)
    classBtn:SetScript("OnEnter", function(self) self:SetBackdropBorderColor(0.0, 0.8, 1.0, 0.6) end)
    classBtn:SetScript("OnLeave", function(self) self:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5) end)

    local classDiv = filterCard:CreateTexture(nil, "ARTWORK")
    classDiv:SetSize(1, 20)
    classDiv:SetPoint("LEFT", classBtn, "RIGHT", 10, 0)
    classDiv:SetColorTexture(0.25, 0.25, 0.25, 0.5)

    -- ============ SPEC DROPDOWN ============
    local specLabel = filterCard:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    specLabel:SetPoint("LEFT", classDiv, "RIGHT", 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)

    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
            local classID = ns.lootBrowserState.selectedClassID
            local numSpecs = classID and GetNumSpecializationsForClassID(classID) or 0
            local ddH = 6
            for i = 1, 4 do
                local row = specRows[i]
                if i <= numSpecs then
                    local specID, specName, _, specIcon2 = GetSpecializationInfoForClassID(classID, i)
                    if specID then
                        row._icon:SetTexture(specIcon2)
                        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 between spec and slot
    local div1 = filterCard:CreateTexture(nil, "ARTWORK")
    div1:SetSize(1, 20)
    div1:SetPoint("LEFT", specBtn, "RIGHT", 10, 0)
    div1:SetColorTexture(0.25, 0.25, 0.25, 0.5)

    -- ============ SLOT DROPDOWN ============
    local slotLabel = filterCard:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    slotLabel:SetPoint("LEFT", div1, "RIGHT", 10, 0)
    slotLabel:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
    slotLabel:SetText("Slot:")
    ns.ApplyTextShadow(slotLabel)

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

    local slotBtnFs = slotBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    slotBtnFs:SetPoint("LEFT", 6, 0)
    ns.ApplyTextShadow(slotBtnFs)
    slotBtnFs:SetText("|cff00ccffHead|r")
    slotBtn._nameFs = slotBtnFs

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

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

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

    -- Build slot dropdown rows: Favorites + separator + 14 slots
    local SLOT_DD_ENTRIES = {
        { name = "Favorites", filter = "FAVORITES", isFavorites = true },
        { name = "All Slots", filter = "ALL_SLOTS", isAllSlots = true },
    }
    for _, sf in ipairs(ns.LOOT_SLOT_FILTERS) do
        table.insert(SLOT_DD_ENTRIES, { name = sf.name, filter = sf.filter })
    end

    local slotDDRows = {}
    local sdY = 6
    for idx, entry in ipairs(SLOT_DD_ENTRIES) do
        -- Separator after special entries (Favorites + All Slots)
        if idx == 3 then
            local sep = slotDropdown:CreateTexture(nil, "ARTWORK")
            sep:SetHeight(1)
            sep:SetPoint("TOPLEFT", 8, -sdY)
            sep:SetPoint("TOPRIGHT", -8, -sdY)
            sep:SetColorTexture(0.25, 0.25, 0.25, 0.5)
            sdY = sdY + 6
        end

        local row = CreateFrame("Button", nil, slotDropdown)
        row:SetSize(148, 22)
        row:SetPoint("TOPLEFT", 6, -sdY)

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

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

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

        -- Badge for favorites count
        local rowBadge = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        rowBadge:SetPoint("RIGHT", rowCheck, "LEFT", -4, 0)
        rowBadge:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
        rowBadge:Hide()
        row._badge = rowBadge

        row._entry = entry
        row:SetScript("OnClick", function(self)
            slotDropdown:Hide()
            ns:LootBrowser_SwitchSlot(self._entry.filter, self._entry.name)
        end)

        slotDDRows[idx] = row
        sdY = sdY + 22
    end
    slotDropdown:SetHeight(sdY + 6)
    f._slotDDRows = slotDDRows

    slotBtn:SetScript("OnClick", function(self)
        if slotDropdown:IsShown() then
            slotDropdown:Hide()
        else
            local favCount = ns:GetFavoritesCount()
            for _, row in ipairs(slotDDRows) do
                local entry = row._entry
                local isActive
                if entry.isFavorites then
                    isActive = ns.lootBrowserState.isFavoritesMode
                    if favCount > 0 then
                        row._badge:SetText("(" .. favCount .. ")")
                        row._badge:Show()
                    else
                        row._badge:Hide()
                    end
                elseif entry.isAllSlots then
                    isActive = ns.lootBrowserState.isAllSlots
                    row._badge:Hide()
                else
                    isActive = (not ns.lootBrowserState.isFavoritesMode and not ns.lootBrowserState.isAllSlots and ns.lootBrowserState.selectedSlot == entry.filter)
                    row._badge:Hide()
                end

                if isActive then
                    row._nameFs:SetText("|cff00ccff" .. entry.name .. "|r")
                    row._check:SetText("|cff00ccff>|r")
                else
                    row._nameFs:SetText("|cffaaaaaa" .. entry.name .. "|r")
                    row._check:SetText("")
                end
            end

            slotDropdown:ClearAllPoints()
            slotDropdown:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
            slotDropdown:Show()
        end
    end)
    slotBtn:SetScript("OnEnter", function(self)
        self:SetBackdropBorderColor(0.0, 0.8, 1.0, 0.6)
    end)
    slotBtn:SetScript("OnLeave", function(self)
        self:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.5)
    end)

    -- Divider between slot and stats
    local div2 = filterCard:CreateTexture(nil, "ARTWORK")
    div2:SetSize(1, 20)
    div2:SetPoint("LEFT", slotBtn, "RIGHT", 10, 0)
    div2:SetColorTexture(0.25, 0.25, 0.25, 0.5)

    -- ============ STAT FILTER CHIPS ============
    local statLabel = filterCard:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    statLabel:SetPoint("LEFT", div2, "RIGHT", 10, 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

    -- ============ INSTANCE TOGGLE BUTTON ============
    local instBtn = CreateFrame("Button", nil, filterCard, "BackdropTemplate")
    instBtn:SetSize(160, 24)
    instBtn:SetPoint("RIGHT", filterCard, "RIGHT", -8, 0)
    instBtn:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 8,
        insets   = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    instBtn:SetBackdropColor(0.0, 0.25, 0.4, 0.8)
    instBtn:SetBackdropBorderColor(0.0, 0.5, 0.8, 0.6)
    ns.SmoothFrame(instBtn)
    instBtn:Hide()
    f._instBtn = instBtn

    local instBtnFs = instBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    instBtnFs:SetPoint("CENTER")
    f._instBtnFs = instBtnFs

    ns.lootBrowserState.instanceFilter = false
    ns.lootBrowserState.instanceName = nil

    instBtn:SetScript("OnClick", function()
        ns.lootBrowserState.instanceFilter = not ns.lootBrowserState.instanceFilter
        ns:LootBrowser_ShowPage()
    end)
    instBtn:SetScript("OnEnter", function(self)
        self:SetBackdropColor(0.0, 0.35, 0.55, 0.9)
    end)
    instBtn:SetScript("OnLeave", function(self)
        if ns.lootBrowserState.instanceFilter then
            self:SetBackdropColor(0.0, 0.3, 0.5, 0.9)
        else
            self:SetBackdropColor(0.0, 0.25, 0.4, 0.8)
        end
    end)

    -- ================================================================
    -- VIEW TABS (Dungeons / Raids)
    -- ================================================================
    local viewTabBar = CreateFrame("Frame", nil, f)
    viewTabBar:SetHeight(VIEW_TAB_H)
    viewTabBar:SetPoint("TOPLEFT", filterCard, "BOTTOMLEFT", 0, -4)
    viewTabBar:SetPoint("TOPRIGHT", filterCard, "BOTTOMRIGHT", 0, -4)
    f._viewTabBar = viewTabBar

    local dungeonTab = ns.CreateUnderlineTab(viewTabBar, "Dungeons", { 0.0, 0.8, 1.0 })
    dungeonTab:SetSize(100, VIEW_TAB_H)
    dungeonTab:SetPoint("TOPLEFT", 4, 0)
    dungeonTab:SetScript("OnClick", function()
        ns:LootBrowser_SwitchView("dungeon")
    end)
    f._dungeonTab = dungeonTab

    local raidTab = ns.CreateUnderlineTab(viewTabBar, "Raids", { 0.0, 0.8, 1.0 })
    raidTab:SetSize(100, VIEW_TAB_H)
    raidTab:SetPoint("TOPLEFT", dungeonTab, "TOPRIGHT", 4, 0)
    raidTab:SetScript("OnClick", function()
        ns:LootBrowser_SwitchView("raid")
    end)
    f._raidTab = raidTab

    -- ================================================================
    -- CONTENT AREA (no scroll — content fits within frame)
    -- ================================================================
    local content = CreateFrame("Frame", nil, f)
    content:SetPoint("TOPLEFT", viewTabBar, "BOTTOMLEFT", 0, -4)
    content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PAD, 8)
    f._content = content

    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
        if f._slotDropdown then f._slotDropdown: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 then return end
    local state = ns.lootBrowserState
    if f._classBtn and state.selectedClassID then
        local className = GetClassInfo(state.selectedClassID)
        local hex = (RAID_CLASS_COLORS[state.selectedClassFile] and
            RAID_CLASS_COLORS[state.selectedClassFile].colorStr) or "ffffffff"
        f._classBtn._nameFs:SetText("|c" .. hex .. (className or "?") .. "|r")
    end
    if f._specBtn and state.selectedClassID and state.selectedSpecIndex then
        local specID, specName, _, specIcon =
            GetSpecializationInfoForClassID(state.selectedClassID, state.selectedSpecIndex)
        if specID then
            f._specBtn._icon:SetTexture(specIcon)
            f._specBtn._nameFs:SetText("|cff00ccff" .. specName .. "|r")
        end
    end
end

function ns:LootBrowser_UpdateSlotDropdown()
    local f = ns.LootBrowserFrame
    if not f or not f._slotBtn then return end
    local state = ns.lootBrowserState
    if state.isFavoritesMode then
        local count = ns:GetFavoritesCount()
        if count > 0 then
            f._slotBtn._nameFs:SetText("|cffFFD100Favorites|r |cff888888(" .. count .. ")|r")
        else
            f._slotBtn._nameFs:SetText("|cffFFD100Favorites|r")
        end
    elseif state.isAllSlots then
        f._slotBtn._nameFs:SetText("|cff00ccffAll Slots|r")
    else
        f._slotBtn._nameFs:SetText("|cff00ccff" .. (state.selectedSlotName or "Head") .. "|r")
    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_UpdateViewTabs()
    local f = ns.LootBrowserFrame
    if not f then return end
    local view = ns.lootBrowserState.selectedView
    if view == "dungeon" then
        ns.SetTabActive(f._dungeonTab)
        ns.SetTabInactive(f._raidTab)
    else
        ns.SetTabInactive(f._dungeonTab)
        ns.SetTabActive(f._raidTab)
    end
end

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

    -- Detect instance
    local instName = ns:GetCurrentInstanceName()
    ns.lootBrowserState.instanceName = instName

    -- Update instance toggle button
    if instName then
        f._instBtn:Show()
        if ns.lootBrowserState.instanceFilter then
            f._instBtnFs:SetText("|cff00d4ff" .. instName .. "|r")
            f._instBtn:SetBackdropColor(0.0, 0.3, 0.5, 0.9)
            f._instBtn:SetBackdropBorderColor(0.0, 0.7, 1.0, 0.8)
        else
            f._instBtnFs:SetText("|cff888888" .. instName .. "|r")
            f._instBtn:SetBackdropColor(0.0, 0.25, 0.4, 0.8)
            f._instBtn:SetBackdropBorderColor(0.0, 0.5, 0.8, 0.6)
        end
    else
        f._instBtn:Hide()
        ns.lootBrowserState.instanceFilter = false
    end

    ns:LootBrowser_UpdateSpecBar()
    ns:LootBrowser_UpdateSlotDropdown()
    ns:LootBrowser_UpdateStatBar()
    ns:LootBrowser_UpdateViewTabs()
    ns:LootBrowser_RefreshDisplay()
end

------------------------------------------------------------
-- Render helper: source type color
------------------------------------------------------------
local SOURCE_COLORS = {
    dungeon   = { 0.12, 0.75, 0.12 },
    raid      = { 0.64, 0.21, 0.93 },
    worldboss = { 0.0,  0.44, 0.87 },
}

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

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

    local specIndex = ns.lootBrowserState.selectedSpecIndex
    if not specIndex then return end

    f._loadingText:Show()

    local isFavorites = ns.lootBrowserState.isFavoritesMode
    local isAllSlots = ns.lootBrowserState.isAllSlots
    local view = ns.lootBrowserState.selectedView

    -- Build a lookup of shaped data (items per instance name)
    local shapedLookup = {}  -- instanceName -> shaped entry
    -- For raid view, keep raw per-boss results too
    local rawResults = nil

    local statKeys = ns.lootBrowserState.selectedStats
    local hasStatFilter = statKeys and #statKeys > 0

    if isFavorites then
        local shaped = ns:ScanFavoritesLoot()
        if hasStatFilter then shaped = ns:FilterShapedLootByStats(shaped, statKeys) end
        for _, inst in ipairs(shaped) do
            shapedLookup[inst.instanceName] = inst
        end
    elseif isAllSlots then
        local shaped = ns:ScanAllSlotsLoot()
        if hasStatFilter then shaped = ns:FilterShapedLootByStats(shaped, statKeys) end
        for _, inst in ipairs(shaped) do
            shapedLookup[inst.instanceName] = inst
        end
    else
        local slotFilter = ns.lootBrowserState.selectedSlot
        if not slotFilter then
            f._loadingText:Hide()
            return
        end

        local results = ns:ScanLootBrowserSlot(specIndex, slotFilter)

        -- Apply instance filter
        if ns.lootBrowserState.instanceFilter and ns.lootBrowserState.instanceName then
            results = ns:FilterLootByInstance(results, ns.lootBrowserState.instanceName)
        end

        -- Apply stat filter
        if ns.lootBrowserState.selectedStats and #ns.lootBrowserState.selectedStats > 0 then
            results = ns:FilterLootByStats(results, ns.lootBrowserState.selectedStats)
        end

        -- Keep raw results for raid boss-by-boss view
        if view == "raid" then
            rawResults = results
        end

        local shaped = ns:ReshapeLootByInstance(results)
        for _, inst in ipairs(shaped) do
            shapedLookup[inst.instanceName] = inst
        end
    end

    f._loadingText:Hide()

    local cache = ns:GetInstanceCache()
    if not cache then
        f._noItemsText:SetText("|cff555555Could not load instance data. Try /reload.|r")
        f._noItemsText:Show()
        return
    end

    local contentW = f._content:GetWidth()
    if contentW <= 1 then contentW = 800 end
    local itemAreaW = contentW - ITEMS_LEFT - 10
    local maxIconsPerRow = math.floor(itemAreaW / (ICON_SIZE + ICON_GAP))
    if maxIconsPerRow < 1 then maxIconsPerRow = 1 end

    local yOff = 0
    local globalRowIdx = 0

    -- Helper to render one item row
    local COMPACT_ROW_H = 44
    local COMPACT_ICON  = 30

    local function RenderRow(rowLabel, rowSubLabel, sourceType, bgTexture, items, showDimmed, compact, encounterID)
        -- Filter out hidden items (housing decor etc) before layout math
        local filtered = {}
        for _, it in ipairs(items or {}) do
            if not (it.itemID and ns:IsLootBrowserHidden(it.itemID)) then
                table.insert(filtered, it)
            end
        end
        items = filtered

        local numItems = #items
        local iconSz = compact and COMPACT_ICON or ICON_SIZE
        local gap    = compact and 8 or ICON_GAP
        local baseH  = compact and COMPACT_ROW_H or ROW_H
        -- Reserve room at both ends for scroll arrows (18px each + 4px padding).
        local ARROW_W = 22
        local trackLeft  = ITEMS_LEFT + ARROW_W
        local trackRight = contentW - ARROW_W
        local trackW = trackRight - trackLeft
        if trackW < iconSz then trackW = iconSz end
        local visibleCount = math.floor((trackW + gap) / (iconSz + gap))
        if visibleCount < 1 then visibleCount = 1 end
        local rowHeight = baseH  -- always single-row now

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

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

        if bgTexture then
            row.dungBg:SetTexture(bgTexture)
            row.dungBg:Show()
        end

        -- Boss portrait for raid rows
        local labelXOff = 12
        if encounterID and compact then
            local portrait = AcquireIcon(row)
            local portraitSz = baseH - 8
            portrait:SetSize(portraitSz, portraitSz)
            portrait:SetPoint("LEFT", row, "LEFT", 8, 0)
            portrait.borderTex:SetColorTexture(0, 0, 0, 0)
            local ok, _, _, _, creatureDisplayID = pcall(EJ_GetCreatureInfo, 1, encounterID)
            if ok and creatureDisplayID and creatureDisplayID > 0 then
                SetPortraitTextureFromCreatureDisplayID(portrait.tex, creatureDisplayID)
                portrait.tex:SetTexCoord(0, 1, 0, 1)
            end
            portrait:EnableMouse(false)
            labelXOff = 8 + portraitSz + 6
        end

        if compact then
            row.srcLabel:SetFontObject("GameFontNormalSmall")
        else
            row.srcLabel:SetFontObject("GameFontHighlightMedium")
        end
        row.srcLabel:ClearAllPoints()
        row.srcLabel:SetPoint("LEFT", row, "LEFT", labelXOff, compact and 0 or 4)
        row.srcLabel:SetTextColor(1, 1, 1)
        row.srcLabel:SetText(rowLabel)
        row.srcLabel:SetAlpha(showDimmed and 0.4 or 1)

        if rowSubLabel then
            row.countLabel:SetText(rowSubLabel)
            row.countLabel:Show()
        end

        -- Create icon frames for all items (positioned during Relayout below)
        local iconYBase = -math.floor((rowHeight - iconSz) / 2)
        for _, item in ipairs(items) do
            local iconFrame = AcquireIcon(row)
            if compact then iconFrame:SetSize(COMPACT_ICON, COMPACT_ICON) end

            if item.itemID and C_Item and C_Item.RequestLoadItemDataByID then
                C_Item.RequestLoadItemDataByID(item.itemID)
            end

            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
            iconFrame.itemID = item.itemID

            local isMount = item.itemID and IsMountItem(item.itemID)
            local diffColor = isMount and MOUNT_BORDER_COLOR or DIFF_BORDER_COLORS[item.diffID]
            iconFrame._diffColor = diffColor
            local isFav = item.itemID and ns:IsLootFavorite(item.itemID)
            if isFav then
                iconFrame.borderTex:SetColorTexture(1.0, 0.82, 0.0, 1.0)
                iconFrame.favStar:Show()
            elseif diffColor then
                iconFrame.borderTex:SetColorTexture(diffColor.r, diffColor.g, diffColor.b, 1.0)
            else
                iconFrame.borderTex:SetColorTexture(0.3, 0.3, 0.3, 1.0)
            end

            table.insert(row._itemFrames, iconFrame)
        end

        -- Horizontal-scroll layout: show up to `visibleCount` starting at row._scrollIdx
        local function Relayout()
            local maxStart = math.max(0, numItems - visibleCount)
            if row._scrollIdx < 0 then row._scrollIdx = 0 end
            if row._scrollIdx > maxStart then row._scrollIdx = maxStart end
            for i, frame in ipairs(row._itemFrames) do
                local slot = i - 1 - row._scrollIdx
                if slot >= 0 and slot < visibleCount then
                    frame:ClearAllPoints()
                    frame:SetPoint("TOPLEFT", row, "TOPLEFT",
                        trackLeft + slot * (iconSz + gap), iconYBase)
                    frame:Show()
                else
                    frame:Hide()
                end
            end
            local overflow = numItems > visibleCount
            if overflow then
                row.leftArrow:ClearAllPoints()
                row.leftArrow:SetPoint("LEFT", row, "LEFT", ITEMS_LEFT, 0)
                row.leftArrow:SetAlpha(row._scrollIdx > 0 and 1 or 0.3)
                row.leftArrow:EnableMouse(row._scrollIdx > 0)
                row.leftArrow:Show()

                row.rightArrow:ClearAllPoints()
                row.rightArrow:SetPoint("LEFT", row, "LEFT", trackRight + 2, 0)
                row.rightArrow:SetAlpha(row._scrollIdx < maxStart and 1 or 0.3)
                row.rightArrow:EnableMouse(row._scrollIdx < maxStart)
                row.rightArrow:Show()
            else
                row.leftArrow:Hide()
                row.rightArrow:Hide()
            end
        end

        row.leftArrow:SetScript("OnClick", function()
            row._scrollIdx = row._scrollIdx - 1
            Relayout()
        end)
        row.rightArrow:SetScript("OnClick", function()
            row._scrollIdx = row._scrollIdx + 1
            Relayout()
        end)
        Relayout()

        yOff = yOff + rowHeight + ROW_GAP
        globalRowIdx = globalRowIdx + 1
    end

    -- Helper to render a section header (for raid names)
    local SECTION_H = 18
    local function RenderSectionHeader(text)
        local row = AcquireRow(f._content)
        row:SetSize(contentW, SECTION_H)
        row:SetPoint("TOPLEFT", f._content, "TOPLEFT", 0, -yOff)
        row.bg:SetColorTexture(0.12, 0.06, 0.18, 0.6)
        row.srcLabel:SetFontObject("GameFontNormal")
        row.srcLabel:ClearAllPoints()
        row.srcLabel:SetPoint("LEFT", row, "LEFT", 12, 0)
        row.srcLabel:SetTextColor(0.75, 0.45, 1.0)
        row.srcLabel:SetText(text)
        row.srcLabel:SetAlpha(1)
        yOff = yOff + SECTION_H
    end

    if view == "dungeon" then
        -- Dungeon view: one row per dungeon, always show all
        for _, inst in ipairs(cache.dungeons) do
            local data = shapedLookup[inst.name]
            local items = data and data.items or {}
            local subLabel = nil
            if isFavorites and #items > 0 then
                subLabel = "|cff888888" .. #items .. " item" .. (#items ~= 1 and "s" or "") .. "|r"
            end
            RenderRow(inst.name, subLabel, "dungeon", inst.bgTexture, items, #items == 0)
        end

        -- In favorites mode, sort by count
        -- (already sorted from ScanFavoritesLoot, but we iterate cache order above)
        -- TODO: could reorder, but keeping cache order is cleaner for dungeons

    elseif view == "raid" then
        -- Raid view: boss-by-boss rows grouped under raid headers
        -- Use raw per-boss results when available, otherwise fall back to shaped data
        for _, raidInst in ipairs(cache.raids) do
            RenderSectionHeader(raidInst.name)

            for _, boss in ipairs(raidInst.bosses) do
                -- Collect items for this boss from raw results
                local bossItems = {}
                if rawResults then
                    for _, entry in ipairs(rawResults) do
                        if entry.sourceName == raidInst.name and entry.bossName == boss.name then
                            -- Pick highest-difficulty items, dedup by itemID
                            local seen = {}
                            for diffID, diffItems in pairs(entry.items) do
                                local priority = ({[23]=6,[2]=5,[1]=4,[16]=6,[15]=5,[14]=4,[17]=3,[0]=4})[diffID] or 0
                                for _, item in ipairs(diffItems) do
                                    if not seen[item.itemID] or priority > (seen[item.itemID]._priority or 0) then
                                        seen[item.itemID] = {
                                            itemID = item.itemID, name = item.name,
                                            icon = item.icon, itemLink = item.itemLink,
                                            diffID = diffID, _priority = priority,
                                        }
                                    end
                                end
                            end
                            for _, item in pairs(seen) do
                                item._priority = nil
                                table.insert(bossItems, item)
                            end
                        end
                    end
                elseif isFavorites or isAllSlots then
                    -- For all-slots/favorites, data is already shaped by instance
                    -- Show all items from this raid under each boss (simplified)
                    local data = shapedLookup[raidInst.name]
                    if data then
                        for _, item in ipairs(data.items) do
                            if item.bossName == boss.name then
                                table.insert(bossItems, item)
                            end
                        end
                    end
                end

                RenderRow(boss.name, nil, "raid", nil, bossItems, #bossItems == 0, true, boss.encounterID)
            end
        end
    end
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

    f.inAppMode = enabled
    UpdateLayout(f)
end

------------------------------------------------------------
-- Auto-detect instance changes for loot browser
------------------------------------------------------------
local lootZoneFrame = CreateFrame("Frame")
lootZoneFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
lootZoneFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
lootZoneFrame:SetScript("OnEvent", function()
    local f = ns.LootBrowserFrame
    if not f or not f:IsShown() then return end

    local newInst = ns:GetCurrentInstanceName()
    local oldInst = ns.lootBrowserState.instanceName

    if newInst ~= oldInst then
        if newInst and not oldInst then
            ns.lootBrowserState.instanceFilter = true
        elseif not newInst then
            ns.lootBrowserState.instanceFilter = false
        end
        ns:LootBrowser_ShowPage()
    end
end)
