--------------------------------------------------------------------------------
-- S-Frames: Bag Container UI (Bags/Container.lua)
--------------------------------------------------------------------------------

SFrames.Bags.Container = {}

local SLOT_SIZE = 36
local SPACING = 6
local MARGIN = 10
local TOP_OFFSET = 52 -- Space for title + search bar + gold
local TEXT_EMPTY = "\231\169\186"
local TEXT_ITEM = "\231\137\169\229\147\129"
local TEXT_BACKPACK = "\232\131\140\229\140\133"
local TEXT_BAGS_TITLE = "\232\131\140\229\140\133"
local TEXT_BANK_TITLE = "\233\147\182\232\161\140"
local TEXT_SORT = "\230\149\180\231\144\134"
local TEXT_SETTINGS = "\232\174\190\231\189\174"
local TEXT_BAG_SLOT = "\232\131\140\229\140\133\230\167\189"
local TEXT_BAG_EMPTY = "\231\169\186"
local TEXT_BAG_SLOTS = "\230\160\188"
local TEXT_EQUIPPED_BAG = "\229\183\178\232\163\133\229\164\135\232\131\140\229\140\133"
local TEXT_HS_USE = "\231\130\185\229\135\187\228\189\191\231\148\168\231\130\137\231\159\179"
local TEXT_HS_NOT_FOUND = "\232\131\140\229\140\133\228\184\173\230\156\170\230\137\190\229\136\176\231\130\137\231\159\179\227\128\130"
local TEXT_CHARACTER = "\232\167\146\232\137\178"
local TEXT_ONLINE = "\229\156\168\231\186\191"
local TEXT_OFFLINE = "\231\166\187\231\186\191"
local PANEL_BG_ALPHA = 0.55
local SLOT_BG_ALPHA = 0.22
local CHARACTER_SELECTOR_ICON = "Interface\\CHARACTERFRAME\\TemporaryPortrait-Female-Human"

local _A = SFrames.ActiveTheme

local BagFrame = nil
local ItemSlots = {}
local playerBagInvSlots = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 }

local function IsLiveBankOpen()
    local bankFrame = _G["SFramesBankFrame"]
    if not bankFrame or not bankFrame:IsVisible() then return false end
    if not SFrames.Bags.Bank then return false end
    return not SFrames.Bags.Bank.isOffline
end

local function SetTooltipFromContainerItem(bagID, slotID)
    if not GameTooltip then return false end
    GameTooltip:ClearLines()

    if GameTooltip.SetBagItem then
        local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end)
        if ok then
            local left1 = _G["GameTooltipTextLeft1"]
            if left1 and left1:GetText() and left1:GetText() ~= "" then
                return true
            end
        end
    end

    local link = GetContainerItemLink(bagID, slotID)
    if link then
        local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
        if ok then
            local left1 = _G["GameTooltipTextLeft1"]
            if left1 and left1:GetText() and left1:GetText() ~= "" then
                return true
            end
        end

        local name = GetItemInfo(link)
        if name and name ~= "" then
            GameTooltip:SetText(name, 1, 1, 1)
            return true
        end
    end

    return false
end

local function HasTooltipText()
    local left1 = _G["GameTooltipTextLeft1"]
    return left1 and left1:GetText() and left1:GetText() ~= ""
end

local function GetItemNameFromLink(link)
    if type(link) ~= "string" or link == "" then
        return nil
    end

    local name = GetItemInfo(link)
    if name and name ~= "" then
        return name
    end

    local _, _, parsed = string.find(link, "%[(.+)%]")
    if parsed and parsed ~= "" then
        return parsed
    end

    return nil
end

local function ShowMerchantCursorForSlot(button)
    if not button then return end
    if button.bagID == nil or button.slotID == nil then return end
    if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then return end
    if not ((MerchantFrame and MerchantFrame:IsVisible()) or (SFramesMerchantFrame and SFramesMerchantFrame:IsVisible())) then return end

    local texture, _, locked = GetContainerItemInfo(button.bagID, button.slotID)
    if not texture or locked then return end

    -- Prefer container-aware API when available.
    if ShowContainerSellCursor then
        local ok = pcall(function()
            ShowContainerSellCursor(button.bagID, button.slotID)
        end)
        if ok then return end
    end

    -- Fallback to generic merchant sell cursor.
    if ShowMerchantSellCursor then
        local ok = pcall(function()
            ShowMerchantSellCursor()
        end)
        if ok then return end
    end

    -- Final fallback for clients lacking both APIs.
    if SetCursor then
        pcall(function()
            SetCursor("BUY_CURSOR")
        end)
    end
end

local function ResolvePlayerBagInvSlots()
    local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 }
    for bagIndex = 1, 4 do
        local invSlot = nil
        local liveBtn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"]

        -- Most reliable when Blizzard character UI is loaded.
        if liveBtn and liveBtn.GetID then
            local id = liveBtn:GetID()
            if type(id) == "number" and id > 0 then
                invSlot = id
            end
        end

        if (not invSlot) and ContainerIDToInventoryID then
            local ok, slotID = pcall(function() return ContainerIDToInventoryID(bagIndex) end)
            if ok and type(slotID) == "number" and slotID > 0 then
                invSlot = slotID
            end
        end

        if (not invSlot) and GetInventorySlotInfo then
            local slotNames = {
                "Bag" .. (bagIndex - 1) .. "Slot",
                "Bag" .. bagIndex .. "Slot",
                "CharacterBag" .. (bagIndex - 1) .. "Slot",
            }
            for _, slotName in ipairs(slotNames) do
                local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
                if ok and type(slotID) == "number" and slotID > 0 then
                    invSlot = slotID
                    break
                end
            end
        end

        playerBagInvSlots[bagIndex] = invSlot or fallback[bagIndex]
    end
end

local function GetLivePlayerBagIconTexture(bagIndex)
    local btn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"]
    if not btn then return nil end

    local icon = _G[btn:GetName() .. "IconTexture"] or _G[btn:GetName() .. "Icon"]
    if icon and icon.GetTexture then
        local tex = icon:GetTexture()
        if tex and tex ~= "" then
            return tex
        end
    end

    return nil
end

local function IsDisabledIconTexture(tex)
    if type(tex) ~= "string" then return false end
    local lower = string.lower(tex)
    lower = string.gsub(lower, "\\", "/")
    return string.find(lower, "disabled", 1, true) ~= nil
end

local function IsPaperdollBagPlaceholder(tex)
    if type(tex) ~= "string" then return false end
    local lower = string.lower(tex)
    lower = string.gsub(lower, "\\", "/")

    if string.find(lower, "ui-paperdoll-slot-bag", 1, true) then
        return true
    end

    -- Some clients may return slightly different placeholder paths.
    if string.find(lower, "paperdoll", 1, true) and
       string.find(lower, "slot", 1, true) and
       string.find(lower, "bag", 1, true) then
        return true
    end

    return false
end

local function IsUsableBagIconTexture(tex)
    if type(tex) ~= "string" or tex == "" then
        return false
    end
    if IsPaperdollBagPlaceholder(tex) then
        return false
    end
    if IsDisabledIconTexture(tex) then
        return false
    end
    return true
end

local function GetIconFromItemLink(link)
    if not link then return nil end

    local _, _, _, _, _, _, _, _, tex = GetItemInfo(link)
    if tex then return tex end

    local _, _, itemID = string.find(link, "item:(%d+)")
    if itemID then
        local _, _, _, _, _, _, _, _, tex2 = GetItemInfo("item:" .. itemID)
        if tex2 then return tex2 end
    end

    return nil
end

local function BuildPlayerTradeLinkSet()
    local links = {}
    if not (TradeFrame and TradeFrame:IsVisible()) then return links end
    if not GetTradePlayerItemLink then return links end
    for i = 1, 6 do
        local link = GetTradePlayerItemLink(i)
        if link and link ~= "" then
            links[link] = true
        end
    end
    return links
end

local function CreateCoinDisplay(parent, frameName)
    local frame = CreateFrame("Frame", frameName, parent, "SmallMoneyFrameTemplate")
    frame:SetFrameStrata(parent:GetFrameStrata())
    frame:SetFrameLevel(parent:GetFrameLevel() + 30)
    frame:SetWidth(140)
    frame:SetHeight(16)
    return frame
end

local function SetCoinDisplayMoney(display, copper)
    if not display then return end
    local value = tonumber(copper) or 0
    if value < 0 then value = 0 end

    if SmallMoneyFrame_SetAmount then
        local ok = pcall(function() SmallMoneyFrame_SetAmount(display, value) end)
        if ok then return end
    end

    if MoneyFrame_Update and display.GetName then
        local frameName = display:GetName()
        if frameName and frameName ~= "" then
            pcall(function() MoneyFrame_Update(frameName, value) end)
        end
    end
end

-- Create a single item slot button
local function CreateSlot(parent, id)
    local button = CreateFrame("Button", "SFramesBagSlot" .. id, parent, "ItemButtonTemplate")
    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    button:RegisterForDrag("LeftButton")

    -- Rounded backdrop style (matching CharacterPanel equipment slots)
    local DEFAULT_BORDER = (_A and _A.slotBorder) or { 0.25, 0.25, 0.3, 0.8 }
    button:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 18,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    button:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9)
    button:SetBackdropBorderColor(DEFAULT_BORDER[1], DEFAULT_BORDER[2], DEFAULT_BORDER[3], DEFAULT_BORDER[4])

    -- Inset icon within the rounded border
    local icon = _G[button:GetName() .. "IconTexture"]
    if icon then
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        icon:ClearAllPoints()
        icon:SetPoint("TOPLEFT", button, "TOPLEFT", 4, -4)
        icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4, 4)
    end

    local qualGlow = button:CreateTexture(nil, "OVERLAY")
    qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    qualGlow:SetBlendMode("ADD")
    qualGlow:SetAlpha(0.8)
    qualGlow:SetWidth(SLOT_SIZE * 1.9)
    qualGlow:SetHeight(SLOT_SIZE * 1.9)
    qualGlow:SetPoint("CENTER", button, "CENTER", 0, 0)
    qualGlow:Hide()
    button.qualGlow = qualGlow

    function button:SetBorderColor(r, g, b, a)
        self.qualGlow:SetVertexColor(r, g, b)
        self.qualGlow:Show()
        self._qualityBorder = true
    end
    function button:ShowBorder()
        self._qualityBorder = true
    end
    function button:HideBorder()
        self.qualGlow:Hide()
        self._qualityBorder = false
    end

    -- Hide the ugly default rounded Blizzard border
    local nt = _G[button:GetName() .. "NormalTexture"]
    if nt then nt:SetTexture(nil) nt:Hide() end

    -- Grey item marker (a small coin/junk icon in the corner)
    local junkIcon = button:CreateTexture(nil, "OVERLAY")
    junkIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Coin-Up")
    junkIcon:SetWidth(14)
    junkIcon:SetHeight(14)
    -- Place it on top of everything
    junkIcon:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1)
    junkIcon:Hide()
    button.junkIcon = junkIcon

    local previewGlow = button:CreateTexture(nil, "OVERLAY")
    previewGlow:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
    previewGlow:SetBlendMode("ADD")
    previewGlow:SetAllPoints(button)
    previewGlow:Hide()
    button.previewGlow = previewGlow

    local tradeText = button:CreateFontString(nil, "OVERLAY")
    tradeText:SetFont("Fonts\\Zekton.ttf", 10, "OUTLINE")
    if setglobal then -- check Vanilla or use default font
        tradeText:SetFontObject(GameFontNormal)
    end
    tradeText:SetPoint("CENTER", button, "CENTER", 0, 0)
    tradeText:SetText("交易")
    tradeText:SetTextColor(0, 1, 0, 1)
    tradeText:Hide()
    button.tradeText = tradeText

    button:SetScript("OnEnter", function()
        if this.bagID == nil or this.slotID == nil then return end
        SFrames.Bags._hoveredSlot = this
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:ClearLines()

        if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
            local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
            if data and data.bags[this.bagID] and data.bags[this.bagID].items[this.slotID] then
                local link = data.bags[this.bagID].items[this.slotID].link
                local shown = false
                if link then
                    local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)")
                    local ok = false
                    if itemStr then
                        ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end)
                    else
                        ok = pcall(function() GameTooltip:SetHyperlink(link) end)
                    end
                    shown = ok and HasTooltipText()

                    if not shown then
                        local name = GetItemNameFromLink(link)
                        if name and name ~= "" then
                            GameTooltip:SetText(name, 1, 1, 1)
                            shown = true
                        end
                    end

                    if not shown then
                        GameTooltip:SetText(TEXT_ITEM, 1, 1, 1)
                    end
                else
                    GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
                end
            else
                GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
            end
        else
            local shown = SetTooltipFromContainerItem(this.bagID, this.slotID)
            if not shown then
                GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
            end
        end
        if IsControlKeyDown() then
            ShowInspectCursor()
        else
            ShowMerchantCursorForSlot(this)
        end
        GameTooltip:Show()
    end)

    button:SetScript("OnLeave", function()
        SFrames.Bags._hoveredSlot = nil
        this.controlDownLast = nil
        GameTooltip:Hide()
        if HideContainerSellCursor and this.bagID and this.slotID then
            pcall(function()
                HideContainerSellCursor(this.bagID, this.slotID)
            end)
        end
        ResetCursor()
    end)

    local cooldown = CreateFrame("Model", button:GetName().."Cooldown", button, "CooldownFrameTemplate")
    cooldown:SetAllPoints(button)

    function button:SplitStack(split)
        if not split or split < 1 then return end
        if self.bagID == nil or self.slotID == nil then return end
        SplitContainerItem(self.bagID, self.slotID, split)
    end

    button:SetScript("OnClick", function()
        local bagID  = this.bagID
        local slotID = this.slotID
        local isOffline = SFrames.Bags.Container.isOffline

        -- Helper: get item link for this slot (works both online and offline)
        local function GetSlotLink()
            if isOffline and SFrames.Bags.Container.offlineChar then
                local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
                if data and data.bags[bagID] and data.bags[bagID].items[slotID] then
                    return data.bags[bagID].items[slotID].link
                end
                return nil
            end
            return GetContainerItemLink(bagID, slotID)
        end

        if IsControlKeyDown() and arg1 == "LeftButton" then
            local link = GetSlotLink()
            if link and DressUpItemLink then
                DressUpItemLink(link)
                return
            end
        end
        if IsShiftKeyDown() then
            local eb = ChatFrameEditBox
            if eb and eb.IsVisible and eb:IsVisible() then
                local link = GetSlotLink()
                if link then eb:Insert(link) end
                return
            end
            if not isOffline and arg1 == "LeftButton" and (not CursorHasItem()) and OpenStackSplitFrame then
                local _, itemCount = GetContainerItemInfo(bagID, slotID)
                if itemCount and itemCount > 1 then
                    OpenStackSplitFrame(itemCount, this, "BOTTOMLEFT", "TOPLEFT")
                    return
                end
            end
        end

        -- Block all other actions in offline mode
        if isOffline then return end

        if arg1 == "RightButton" then
            if SFrames.Mail and SFrames.Mail.TryAddItemFromBag then
                if SFrames.Mail.TryAddItemFromBag(bagID, slotID) then return end
            end
            if this.bagID >= 0 and IsLiveBankOpen() and AutoStoreBagItem then
                local ok = pcall(function() AutoStoreBagItem(this.bagID, this.slotID) end)
                if ok then return end
            end
            
            if TradeFrame and TradeFrame:IsVisible() and not IsShiftKeyDown() then
                local _, _, locked = GetContainerItemInfo(this.bagID, this.slotID)
                if locked then
                    local link = GetSlotLink()
                    if link and GetTradePlayerItemLink then
                        local inTradeSlot = nil
                        for i = 1, 6 do
                            if GetTradePlayerItemLink(i) == link then
                                inTradeSlot = i
                                break
                            end
                        end
                        if inTradeSlot then
                            ClearCursor()
                            ClickTradeButton(inTradeSlot)
                            ClearCursor()
                            return
                        end
                        if BagFrame and BagFrame:IsVisible() then
                            SFrames.Bags.Container:UpdateLayout()
                        end
                    end
                else
                    PickupContainerItem(this.bagID, this.slotID)
                    local tradeSlot = TradeFrame_GetAvailableSlot and TradeFrame_GetAvailableSlot()
                    if tradeSlot then ClickTradeButton(tradeSlot) end
                    if CursorHasItem() then ClearCursor() end
                    return
                end
            end

            if AuctionFrame and AuctionFrame:IsShown() and not IsShiftKeyDown() then
                if AuctionFrameBrowse and AuctionFrameBrowse:IsShown() then
                    local link = GetContainerItemLink(this.bagID, this.slotID)
                    if link then
                        local _, _, itemName = string.find(link, "%[(.+)%]")
                        if itemName and BrowseName then
                            BrowseName:SetText(itemName)
                            if AuctionFrameBrowse_Search then AuctionFrameBrowse_Search() end
                        end
                    end
                    return
                elseif AuctionFrameAuctions and AuctionFrameAuctions:IsShown() then
                    PickupContainerItem(this.bagID, this.slotID)
                    if AuctionsItemButton then AuctionsItemButton:Click() end
                    if CursorHasItem() then ClearCursor() end
                    return
                end
            end

            UseContainerItem(this.bagID, this.slotID)
        else
            PickupContainerItem(this.bagID, this.slotID)
        end
    end)

    button:SetScript("OnDragStart", function()
        if SFrames.Bags.Container.isOffline then return end
        if CursorHasItem() then return end
        PickupContainerItem(this.bagID, this.slotID)
    end)

    button:SetScript("OnReceiveDrag", function()
        if SFrames.Bags.Container.isOffline then return end
        if CursorHasItem() then
            PickupContainerItem(this.bagID, this.slotID)
        end
    end)

    return button
end

local function SaveBagFramePosition()
    if not (BagFrame and SFramesDB and SFramesDB.Bags) then return end
    local point, _, relPoint, x, y = BagFrame:GetPoint()
    if not point or not relPoint then return end
    SFramesDB.Bags.bagPosition = {
        point = point,
        relPoint = relPoint,
        x = x or 0,
        y = y or 0,
    }
end

local function ApplyBagFramePosition()
    if not BagFrame then return end
    BagFrame:ClearAllPoints()

    local pos = SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagPosition
    if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then
        BagFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y)
    else
        BagFrame:SetPoint("RIGHT", UIParent, "RIGHT", -20, 0)
    end
end

function SFrames.Bags.Container:PreviewBagSlots(targetBagID)
    for _, btn in ipairs(ItemSlots) do
        if btn and btn:IsShown() then
            local icon = _G[btn:GetName() .. "IconTexture"]
            local isMatch = (btn.bagID == targetBagID)

            if icon then
                icon:SetVertexColor(1, 1, 1)
            end

            if btn.previewGlow then
                if isMatch then
                    btn.previewGlow:Show()
                else
                    btn.previewGlow:Hide()
                end
            end
        end
    end
end

function SFrames.Bags.Container:ClearBagPreview()
    for _, btn in ipairs(ItemSlots) do
        if btn and btn:IsShown() then
            local icon = _G[btn:GetName() .. "IconTexture"]
            if icon then icon:SetVertexColor(1, 1, 1) end
            if btn.previewGlow then btn.previewGlow:Hide() end
        end
    end

    -- Restore search dimming state after preview.
    local query = ""
    if SFramesBagSearchBox and SFramesBagSearchBox.GetText then
        query = SFramesBagSearchBox:GetText() or ""
    end
    if SFrames.Bags.Features and SFrames.Bags.Features.ApplySearch then
        SFrames.Bags.Features:ApplySearch(query)
    end
end

-- Build/Update the item slot grid
function SFrames.Bags.Container:UpdateLayout()
    if not BagFrame then return end

    local cols = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.columns) or 10
    local spacing = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagSpacing) or SPACING
    spacing = tonumber(spacing) or SPACING
    if spacing < 0 then spacing = 0 end
    local slots = {}

    local isOffline = self.isOffline
    local charName = self.offlineChar
    local offlineDB = nil
    if isOffline and charName then
        offlineDB = SFrames.Bags.Offline:GetCharacterData(charName)
        if not offlineDB then
            isOffline = false
            self.isOffline = false
            self.offlineChar = nil
            if BagFrame and BagFrame.RefreshCharacterSelectorText then
                BagFrame.RefreshCharacterSelectorText()
            end
        end
    end

    local tradeLinks = nil
    if not isOffline and TradeFrame and TradeFrame:IsVisible() then
        tradeLinks = BuildPlayerTradeLinkSet()
    end

    -- Collect all slots (bags 0-4)
    for bag = 0, 4 do
        local size = 0
        if isOffline and offlineDB then
            if offlineDB.bags[bag] then size = offlineDB.bags[bag].size end
        else
            size = GetContainerNumSlots(bag)
        end
        for slot = 1, size do
            table.insert(slots, { bag = bag, slot = slot })
        end
    end

    local numSlots = table.getn(slots)
    if numSlots == 0 then numSlots = 1 end
    local rows = math.ceil(numSlots / cols)

    -- Resize frame
    local width  = MARGIN * 2 + (cols * SLOT_SIZE) + math.max(0, (cols - 1)) * spacing
    local height = MARGIN * 2 + TOP_OFFSET + (rows * SLOT_SIZE) + math.max(0, (rows - 1)) * spacing + 24
    BagFrame:SetWidth(math.max(width, 160))
    BagFrame:SetHeight(height)

    -- Position & update slots
    for i, meta in ipairs(slots) do
        local btn = ItemSlots[i]
        if not btn then
            btn = CreateSlot(BagFrame, i)
            ItemSlots[i] = btn
        end

        btn.bagID  = meta.bag
        btn.slotID = meta.slot

        local row = math.floor((i - 1) / cols)
        local col = math.mod((i - 1), cols)
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", BagFrame, "TOPLEFT",
            MARGIN + col * (SLOT_SIZE + spacing),
            -(MARGIN + TOP_OFFSET + row * (SLOT_SIZE + spacing)))

        -- Fetch item info
        local texture, count, quality, link, locked
        if isOffline and offlineDB then
            if offlineDB.bags[meta.bag] and offlineDB.bags[meta.bag].items[meta.slot] then
                local item = offlineDB.bags[meta.bag].items[meta.slot]
                texture = item.texture
                count   = item.count
                quality = item.quality
                link    = item.link
                locked  = false
            end
        else
            local t, c, l, q = GetContainerItemInfo(meta.bag, meta.slot)
            texture = t; count = c; locked = l; quality = q
            link = GetContainerItemLink(meta.bag, meta.slot)
        end

        SetItemButtonTexture(btn, texture)
        SetItemButtonCount(btn, count)
        local iconTex = _G[btn:GetName() .. "IconTexture"]
        
        local isDesaturated = locked and (not tradeLinks)
        if btn.tradeText then btn.tradeText:Hide() end
        if tradeLinks and link then
            local isLockedByTrade = locked and (tradeLinks[link] and true or false)
            if isLockedByTrade then
                if btn.tradeText then btn.tradeText:Show() end
                isDesaturated = true
            else
                -- Ignore stale locked flags for items no longer in trade.
                isDesaturated = false
                SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate")
                SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE")
                SFramesBagsTooltipScanner:ClearLines()
                local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot)
                if hasItem then
                    for i = 1, 10 do
                        local line = _G["SFramesBagsTooltipScannerTextLeft" .. i]
                        if line then
                            local text = line:GetText()
                            if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then
                                isDesaturated = true
                                break
                            end
                        else
                            break
                        end
                    end
                end
            end
        elseif not isOffline and TradeFrame and TradeFrame:IsVisible() and link then
            SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate")
            SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE")
            SFramesBagsTooltipScanner:ClearLines()
            local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot)
            if hasItem then
                for i = 1, 10 do
                    local line = _G["SFramesBagsTooltipScannerTextLeft" .. i]
                    if line then
                        local text = line:GetText()
                        if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then
                            isDesaturated = true
                            break
                        end
                    else
                        break
                    end
                end
            end
        end

        if iconTex then 
            if isDesaturated then
                iconTex:SetVertexColor(0.5, 0.5, 0.5)
            else
                iconTex:SetVertexColor(1, 1, 1)
            end
        end
        if btn.previewGlow then btn.previewGlow:Hide() end

        -- Quality border & Grey marker
        btn:HideBorder()
        btn.junkIcon:Hide()

        if link then
            -- 1st attempt: Safest 1.12 generic approach: Read the color straight out of the hyperlink!
            -- Standard link format: |cffAABBCC|Hitem:...
            local _, _, hex = string.find(link, "|c(%x+)|H")
            local parsedColor = false
            
            if hex and string.len(hex) == 8 then
                local hexLower = string.lower(hex)
                if hexLower == "ff9d9d9d" then
                    -- Poor / Grey Item
                    btn:SetBorderColor(0.5, 0.5, 0.5, 1)
                    btn:ShowBorder()
                    btn.junkIcon:Show()
                    parsedColor = true
                elseif hexLower == "ffffffff" then
                    -- Common / White Item (No special border)
                    parsedColor = true
                else
                    -- Green, Blue, Purple, Orange
                    local r = tonumber(string.sub(hex, 3, 4), 16) / 255
                    local g = tonumber(string.sub(hex, 5, 6), 16) / 255
                    local b = tonumber(string.sub(hex, 7, 8), 16) / 255
                    btn:SetBorderColor(r, g, b, 1)
                    btn:ShowBorder()
                    parsedColor = true
                end
            end
            
            -- 2nd attempt fallback: Use the API quality return values if regex fails
            if not parsedColor then
                local q = quality
                if not q then
                    local _, _, itemString = string.find(link, "item:(%d+)")
                    if itemString then
                        local _, _, scanRarity = GetItemInfo("item:" .. itemString)
                        q = scanRarity
                    end
                end
                
                if q then
                    if q == 0 then
                        btn:SetBorderColor(0.5, 0.5, 0.5, 1)
                        btn:ShowBorder()
                        btn.junkIcon:Show()
                    elseif q > 1 then
                        local r, g, b = GetItemQualityColor(q)
                        btn:SetBorderColor(r, g, b, 1)
                        btn:ShowBorder()
                    end
                end
            end
        end

        -- Cooldowns
        local cooldown = _G[btn:GetName() .. "Cooldown"]
        if cooldown then
            if isOffline then
                cooldown:Hide()
            else
                local start, duration, enable = GetContainerItemCooldown(meta.bag, meta.slot)
                if start and duration and start > 0 and duration > 0 then
                    CooldownFrame_SetTimer(cooldown, start, duration, enable)
                    cooldown:Show()
                else
                    cooldown:Hide()
                end
            end
        end

        btn:Show()
    end

    -- Hide excess buttons
    for i = numSlots + 1, table.getn(ItemSlots) do
        if ItemSlots[i] then ItemSlots[i]:Hide() end
    end

    if BagFrame.UpdateBagSlotIcons then
        BagFrame.UpdateBagSlotIcons()
    end

    -- Update money display
    if BagFrame.moneyFrame then
        local copper = 0
        if isOffline and offlineDB then
            copper = offlineDB.money or 0
        else
            copper = GetMoney()
        end
        SetCoinDisplayMoney(BagFrame.moneyFrame, copper)
    end
end

-- Main frame initialization (called once after PLAYER_LOGIN)
function SFrames.Bags.Container:Initialize()
    if BagFrame then return end

    BagFrame = CreateFrame("Frame", "SFramesBagFrame", UIParent)
    BagFrame:SetWidth(420)
    BagFrame:SetHeight(200)
    BagFrame:SetFrameStrata("HIGH")
    BagFrame:SetToplevel(true)
    BagFrame:EnableMouse(true)
    BagFrame:SetMovable(true)
    BagFrame:SetClampedToScreen(true)
    BagFrame:RegisterForDrag("LeftButton")
    BagFrame:SetScript("OnDragStart", function() this:StartMoving() end)
    BagFrame:SetScript("OnDragStop", function()
        this:StopMovingOrSizing()
        SaveBagFramePosition()
    end)
    ApplyBagFramePosition()
    tinsert(UISpecialFrames, "SFramesBagFrame")

    BagFrame:SetScript("OnUpdate", function()
        local btn = SFrames.Bags._hoveredSlot
        if not btn then return end
        if not GameTooltip:IsOwned(btn) then
            SFrames.Bags._hoveredSlot = nil
            return
        end
        if IsControlKeyDown() then
            if not btn.controlDownLast then
                btn.controlDownLast = true
                ShowInspectCursor()
            end
        else
            if btn.controlDownLast then
                btn.controlDownLast = false
                ResetCursor()
                ShowMerchantCursorForSlot(btn)
            end
        end
    end)

    -- ESC menu style rounded backdrop
    BagFrame:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    local _A = SFrames.ActiveTheme
    if _A and _A.panelBg then
        BagFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95)
        BagFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9)
    else
        BagFrame:SetBackdropColor(0.12, 0.06, 0.10, 0.95)
        BagFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9)
    end
    local bagShadow = CreateFrame("Frame", nil, BagFrame)
    bagShadow:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", -5, 5)
    bagShadow:SetPoint("BOTTOMRIGHT", BagFrame, "BOTTOMRIGHT", 5, -5)
    bagShadow:SetFrameLevel(math.max(BagFrame:GetFrameLevel() - 1, 0))
    bagShadow:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    bagShadow:SetBackdropColor(0, 0, 0, 0.55)
    bagShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
    local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.scale) == "number" and SFramesDB.Bags.scale) or 0.85
    BagFrame:SetScale(scale)
    local bagAlpha = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.alpha) == "number" and SFramesDB.Bags.alpha) or 1
    BagFrame:SetAlpha(bagAlpha)

    local titleIco = SFrames:CreateIcon(BagFrame, "backpack", 14)
    titleIco:SetDrawLayer("OVERLAY")
    titleIco:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -7)
    titleIco:SetVertexColor(_A.title[1], _A.title[2], _A.title[3])

    local titleFS = SFrames:CreateFontString(BagFrame, 12, "LEFT")
    titleFS:SetPoint("LEFT", titleIco, "RIGHT", 4, 0)
    titleFS:SetText(TEXT_BAGS_TITLE)
    titleFS:SetTextColor(_A.title[1], _A.title[2], _A.title[3])
    BagFrame.title = titleFS

    -- Close button
    local closeBtn = CreateFrame("Button", "SFramesBagClose", BagFrame, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", 0, 0)
    closeBtn:SetScript("OnClick", function() SFrames.Bags.Container:Close() end)

    -- Money display
    BagFrame.moneyFrame = CreateCoinDisplay(BagFrame, "SFramesBagMoneyFrame")
    BagFrame.moneyFrame:SetPoint("RIGHT", BagFrame, "BOTTOMRIGHT", -8, 17)
    SetCoinDisplayMoney(BagFrame.moneyFrame, 0)

    -- Bag slot management buttons (5 slots: 0=backpack, 1-4 = equipped bags)
    BagFrame.bagSlotBtns = {}
    local BAG_BTN_SIZE = 22
    ResolvePlayerBagInvSlots()
    local bagInvSlots = playerBagInvSlots
    local bagBar = CreateFrame("Frame", "SFramesBagBar", BagFrame)
    bagBar:SetWidth((BAG_BTN_SIZE * 5) + (3 * 4))
    bagBar:SetHeight(BAG_BTN_SIZE)
    bagBar:SetPoint("BOTTOMLEFT", BagFrame, "BOTTOMLEFT", 8, 6)
    bagBar:SetFrameStrata(BagFrame:GetFrameStrata())
    bagBar:SetFrameLevel(BagFrame:GetFrameLevel() + 40)
    bagBar:EnableMouse(false)
    BagFrame.bagBar = bagBar

    local function PlaceCursorItemInBagSlot(invSlot)
        if not invSlot or not CursorHasItem() then return false end

        local ok = false
        if EquipCursorItem then
            ok = pcall(function() EquipCursorItem(invSlot) end)
            if ok and (not CursorHasItem()) then
                return true
            end
        end

        -- In vanilla this can both place and swap bag items in bag slots.
        ok = pcall(function() PickupBagFromSlot(invSlot) end)
        if ok and (not CursorHasItem()) then
            return true
        end

        if PutItemInBag then
            ok = pcall(function() PutItemInBag(invSlot) end)
            if ok and (not CursorHasItem()) then
                return true
            end
        end

        -- Fallback path if PutItemInBag is unavailable.
        if CursorHasItem() then
            local fallbackOk = pcall(function() PickupInventoryItem(invSlot) end)
            ok = fallbackOk or ok
        end

        return ok and (not CursorHasItem())
    end

    for bagIndex = 0, 4 do
        local bsBtn = CreateFrame("Button", "SFramesBagMgrBtn"..bagIndex, bagBar)
        bsBtn:SetWidth(BAG_BTN_SIZE)
        bsBtn:SetHeight(BAG_BTN_SIZE)
        bsBtn:EnableMouse(true)
        bsBtn:SetFrameStrata(bagBar:GetFrameStrata())
        bsBtn:SetFrameLevel(bagBar:GetFrameLevel() + 1)

        -- Anchor: first one at BOTTOMLEFT, rest chained RIGHT
        if bagIndex == 0 then
            bsBtn:SetPoint("TOPLEFT", bagBar, "TOPLEFT", 0, 0)
        else
            bsBtn:SetPoint("LEFT", _G["SFramesBagMgrBtn" .. (bagIndex-1)], "RIGHT", 3, 0)
        end
        bsBtn.bagID = bagIndex

        -- Rounded backdrop (matching item slot style)
        bsBtn:SetBackdrop({
            bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 12,
            insets = { left = 2, right = 2, top = 2, bottom = 2 }
        })
        bsBtn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9)
        bsBtn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8)

        -- Icon texture inset within border
        local bsIcon = bsBtn:CreateTexture(nil, "OVERLAY")
        bsIcon:SetPoint("TOPLEFT", bsBtn, "TOPLEFT", 2, -2)
        bsIcon:SetPoint("BOTTOMRIGHT", bsBtn, "BOTTOMRIGHT", -2, 2)
        bsIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        bsIcon:SetBlendMode("BLEND")
        bsIcon:SetVertexColor(1, 1, 1, 1)
        bsBtn.icon = bsIcon

        local bsHighlight = bsBtn:CreateTexture(nil, "HIGHLIGHT")
        bsHighlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
        bsHighlight:SetBlendMode("ADD")
        bsHighlight:SetAllPoints(bsBtn)

        bsBtn:SetScript("OnEnter", function()
            GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
            GameTooltip:ClearLines()
            if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
                local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
                if this.bagID == 0 then
                    local slots = data and data.bags and data.bags[0] and data.bags[0].size or 0
                    if slots and slots > 0 then
                        GameTooltip:SetText(string.format("%s (%d%s)", TEXT_BACKPACK, slots, TEXT_BAG_SLOTS), 1, 1, 1)
                    else
                        GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1)
                    end
                else
                    local shown = false
                    local slots = data and data.bags and data.bags[this.bagID] and data.bags[this.bagID].size or 0
                    local bagMeta = data and data.equippedBags and data.equippedBags[this.bagID]

                    if bagMeta and bagMeta.link then
                        local ok = pcall(function() GameTooltip:SetHyperlink(bagMeta.link) end)
                        shown = ok and HasTooltipText()

                        if not shown then
                            local name = GetItemNameFromLink(bagMeta.link)
                            if name and name ~= "" then
                                GameTooltip:SetText(name, 1, 1, 1)
                                shown = true
                            end
                        end
                    end

                    if not shown then
                        if slots and slots > 0 then
                            GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1)
                        else
                            GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6)
                        end
                    end
                end
                GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75)
            else
                if this.bagID == 0 then
                    GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1)
                else
                    local invSlot = playerBagInvSlots[this.bagID]
                    local shown = false
                    if invSlot then
                        if GameTooltip.SetInventoryItem then
                            local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end)
                            if ok then
                                local left1 = _G["GameTooltipTextLeft1"]
                                if left1 and left1:GetText() and left1:GetText() ~= "" then
                                    shown = true
                                end
                            end
                        end

                        if not shown then
                            local bagLink = GetInventoryItemLink("player", invSlot)
                            if bagLink then
                                local ok = pcall(function() GameTooltip:SetHyperlink(bagLink) end)
                                if ok then
                                    shown = true
                                else
                                    local name = GetItemInfo(bagLink) or (TEXT_EQUIPPED_BAG.." "..this.bagID)
                                    GameTooltip:SetText(name, 1, 1, 1)
                                    shown = true
                                end
                            end
                        end

                        if not shown then
                            local slots = GetContainerNumSlots(this.bagID) or 0
                            if slots > 0 then
                                GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1)
                            else
                                GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6)
                            end
                        end
                    else
                        GameTooltip:SetText(string.format("%s %d", TEXT_BAG_SLOT, this.bagID), 0.8, 0.8, 0.8)
                    end
                end
            end
            if BagFrame:IsVisible() then
                SFrames.Bags.Container:PreviewBagSlots(this.bagID)
            end
            GameTooltip:Show()
        end)
        bsBtn:SetScript("OnLeave", function()
            GameTooltip:Hide()
            if BagFrame:IsVisible() then
                SFrames.Bags.Container:ClearBagPreview()
            end
        end)

        bsBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        bsBtn:RegisterForDrag("LeftButton")

        bsBtn:SetScript("OnDragStart", function()
            if this.bagID <= 0 then return end
            local invSlot = playerBagInvSlots[this.bagID]
            if invSlot then
                pcall(function() PickupBagFromSlot(invSlot) end)
            end
        end)

        bsBtn:SetScript("OnReceiveDrag", function()
            if not CursorHasItem() then return end

            if this.bagID == 0 then
                if PutItemInBackpack then
                    pcall(function() PutItemInBackpack() end)
                end
            else
                local invSlot = playerBagInvSlots[this.bagID]
                PlaceCursorItemInBagSlot(invSlot)
            end

            if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
            if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end
        end)

        bsBtn:SetScript("OnClick", function()
            if CursorHasItem() then
                if this.bagID == 0 then
                    if PutItemInBackpack then
                        pcall(function() PutItemInBackpack() end)
                    end
                else
                    local invSlot = playerBagInvSlots[this.bagID]
                    PlaceCursorItemInBagSlot(invSlot)
                end
                if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
                if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end
                return
            end

            if this.bagID > 0 then
                local invSlot = playerBagInvSlots[this.bagID]
                if invSlot then
                    pcall(function() PickupBagFromSlot(invSlot) end)
                end
            elseif arg1 == "RightButton" then
                -- Backpack slot has no equip slot to pick up from.
            else
                SFrames.Bags.Container:Toggle()
            end
        end)

        BagFrame.bagSlotBtns[bagIndex] = bsBtn
    end
    -- Update bag slot textures whenever bag contents change
    BagFrame.UpdateBagSlotIcons = function()
        local isOffline = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar
        local offlineDB = nil
        if isOffline then
            offlineDB = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
        end

        ResolvePlayerBagInvSlots()
        for bagIndex = 0, 4 do
            local btn = BagFrame.bagSlotBtns[bagIndex]
            if btn then
                if bagIndex == 0 then
                    btn.icon:SetTexture("Interface\\Buttons\\Button-Backpack-Up")
                elseif isOffline and offlineDB then
                    local slots = offlineDB.bags and offlineDB.bags[bagIndex] and offlineDB.bags[bagIndex].size or 0
                    local bagMeta = offlineDB.equippedBags and offlineDB.equippedBags[bagIndex]
                    local tex = nil

                    if bagMeta and bagMeta.link then
                        tex = GetIconFromItemLink(bagMeta.link)
                    end
                    if (not tex) and bagMeta and IsUsableBagIconTexture(bagMeta.texture) then
                        tex = bagMeta.texture
                    end

                    if (not tex) and slots and slots > 0 then
                        tex = "Interface\\Icons\\INV_Misc_Bag_08"
                    end
                    if not tex then
                        tex = "Interface\\Icons\\INV_Misc_Bag_08"
                    end
                    btn.icon:SetTexture(tex)
                else
                    local invSlot = playerBagInvSlots[bagIndex]
                    local tex = nil
                    local slots = GetContainerNumSlots(bagIndex) or 0

                    if invSlot then
                        local link = GetInventoryItemLink("player", invSlot)
                        tex = GetIconFromItemLink(link)

                        if (not tex) and slots > 0 then
                            local rawTex = GetInventoryItemTexture("player", invSlot)
                            if type(rawTex) ~= "string" then
                                tex = rawTex
                            elseif IsUsableBagIconTexture(rawTex) then
                                tex = rawTex
                            end
                        end
                    end

                    if (not tex) and slots > 0 then
                        local liveTex = GetLivePlayerBagIconTexture(bagIndex)
                        if IsUsableBagIconTexture(liveTex) then
                            tex = liveTex
                        end
                    end

                    if tex then
                        btn.icon:SetTexture(tex)
                    else
                        btn.icon:SetTexture("Interface\\Icons\\INV_Misc_Bag_08")
                    end
                end
                btn:Enable()
                if btn.icon.SetDesaturated then
                    pcall(function() btn.icon:SetDesaturated(false) end)
                end
                btn.icon:SetVertexColor(1, 1, 1, 1)
                btn.icon:SetAlpha(1)
            end
        end
    end
    local function QueueBagSlotIconRefreshes()
        if not BagFrame or not BagFrame.UpdateBagSlotIcons then return end
        local elapsed = 0
        local nextRefresh = 1
        local refreshPoints = { 0.08, 0.25, 0.55 }
        local refreshTimer = CreateFrame("Frame")
        refreshTimer:SetScript("OnUpdate", function()
            elapsed = elapsed + arg1
            if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then
                if BagFrame.UpdateBagSlotIcons then
                    BagFrame.UpdateBagSlotIcons()
                end
                if BagFrame:IsVisible() then
                    SFrames.Bags.Container:UpdateLayout()
                end
                nextRefresh = nextRefresh + 1
            end
            if nextRefresh > table.getn(refreshPoints) then
                this:SetScript("OnUpdate", nil)
            end
        end)
    end
    BagFrame.UpdateBagSlotIcons()
    QueueBagSlotIconRefreshes()

    -- Search bar
    local eb = CreateFrame("EditBox", "SFramesBagSearchBox", BagFrame, "InputBoxTemplate")
    eb:SetWidth(120)
    eb:SetHeight(18)
    eb:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -26)
    eb:SetAutoFocus(false)
    eb:SetScript("OnEnterPressed", function() this:ClearFocus() end)
    eb:SetScript("OnEscapePressed", function() this:ClearFocus(); this:SetText("") end)
    eb:SetScript("OnTextChanged", function()
        SFrames.Bags.Features:ApplySearch(this:GetText())
    end)

    local function CreateHeaderIconButton(name, parent, iconPath)
        local btn = CreateFrame("Button", name, parent)
        btn:SetWidth(18)
        btn:SetHeight(18)
        btn:SetFrameStrata(parent:GetFrameStrata())
        btn:SetFrameLevel(parent:GetFrameLevel() + 45)

        btn:SetBackdrop({
            bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 12,
            insets = { left = 2, right = 2, top = 2, bottom = 2 }
        })
        btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.86)
        btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8)

        local icon = btn:CreateTexture(nil, "ARTWORK")
        icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark")
        icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
        icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        btn.icon = icon

        local hl = btn:CreateTexture(nil, "HIGHLIGHT")
        hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
        hl:SetBlendMode("ADD")
        hl:SetAllPoints(btn)

        return btn
    end

    -- Character selector: icon button-triggered dropdown.
    local function GetCurrentCharacterName()
        local live = UnitName("player")
        if type(live) == "string" and live ~= "" then
            return live
        end
        local cached = SFrames.Bags.Offline:GetCurrentPlayerName()
        if type(cached) == "string" and cached ~= "" then
            return cached
        end
        return TEXT_CHARACTER
    end

    local function GetOfflineBankTargetCharacter()
        if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
            return SFrames.Bags.Container.offlineChar
        end
        return GetCurrentCharacterName()
    end

    local charBtn = CreateHeaderIconButton("SFramesBagCharBtn", BagFrame, CHARACTER_SELECTOR_ICON)
    charBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", -8, -26)
    BagFrame.charSelectBtn = charBtn

    local cfgBtn = CreateHeaderIconButton("SFramesBagConfigBtn", BagFrame, "Interface\\Icons\\INV_Misc_Gear_01")
    cfgBtn:SetPoint("RIGHT", charBtn, "LEFT", -4, 0)
    SFrames:SetIcon(cfgBtn.icon, "settings")
    cfgBtn:SetScript("OnClick", function()
        if SFrames.ConfigUI and SFrames.ConfigUI.Build then
            SFrames.ConfigUI:Build("bags")
        end
    end)
    cfgBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText(TEXT_SETTINGS, 1, 1, 1)
        GameTooltip:Show()
    end)
    cfgBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    local bankBtn = CreateHeaderIconButton("SFramesBagOfflineBankBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_05")
    bankBtn:SetPoint("RIGHT", cfgBtn, "LEFT", -4, 0)
    SFrames:SetIcon(bankBtn.icon, "gold")
    bankBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText(TEXT_OFFLINE .. TEXT_BANK_TITLE, 1, 1, 1)
        local target = GetOfflineBankTargetCharacter()
        if target and target ~= "" and target ~= TEXT_CHARACTER then
            GameTooltip:AddLine(target, 0.8, 0.8, 0.8)
        end
        GameTooltip:Show()
    end)
    bankBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    bankBtn:SetScript("OnClick", function()
        if not (SFrames.Bags and SFrames.Bags.Bank) then return end
        local target = GetOfflineBankTargetCharacter()
        if target == TEXT_CHARACTER then
            target = nil
        end
        if SFrames.Bags.Bank.OpenOffline then
            SFrames.Bags.Bank:OpenOffline(target)
        else
            SFrames.Bags.Bank:Open()
        end
    end)

    -- Sort button (icon)
    local sortBtn = CreateHeaderIconButton("SFramesBagSortBtn", BagFrame, "Interface\\Icons\\INV_Misc_Note_05")
    sortBtn:SetPoint("LEFT", eb, "RIGHT", 6, 0)
    SFrames:SetIcon(sortBtn.icon, "backpack")
    sortBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText(TEXT_SORT, 1, 1, 1)
        GameTooltip:Show()
    end)
    sortBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    sortBtn:SetScript("OnClick", function()
        if SFrames.Bags.Sort then SFrames.Bags.Sort:Start() end
    end)

    local hsBtn = CreateHeaderIconButton("SFramesBagHSBtn", BagFrame, "Interface\\Icons\\INV_Misc_Rune_01")
    hsBtn:SetPoint("LEFT", sortBtn, "RIGHT", 4, 0)
    SFrames:SetIcon(hsBtn.icon, "hearthstone")
    hsBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText(TEXT_HS_USE, 1, 1, 1)
        GameTooltip:Show()
    end)
    hsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    hsBtn:SetScript("OnClick", function()
        for bag = 0, 4 do
            for slot = 1, GetContainerNumSlots(bag) do
                local link = GetContainerItemLink(bag, slot)
                if link and (string.find(link, "6948") or string.find(link, "Hearthstone")) then
                    UseContainerItem(bag, slot)
                    return
                end
            end
        end
        SFrames:Print(TEXT_HS_NOT_FOUND)
    end)

    local keyBtn = CreateHeaderIconButton("SFramesBagKeyBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_04")
    keyBtn:SetPoint("LEFT", hsBtn, "RIGHT", 4, 0)
    SFrames:SetIcon(keyBtn.icon, "key")
    keyBtn:RegisterForClicks("LeftButtonUp")
    keyBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText("\233\146\165\229\140\153\233\147\190", 1, 0.82, 0)
        GameTooltip:Show()
    end)
    keyBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    keyBtn:SetScript("OnClick", function()
        if SFrames.Bags._origToggleKeyRing then
            SFrames.Bags._origToggleKeyRing()
        elseif ToggleKeyRing then
            ToggleKeyRing()
        end
    end)

    local dbMenu = CreateFrame("Frame", "SFramesBagDropdown", BagFrame, "UIDropDownMenuTemplate")
    dbMenu:Hide()
    dbMenu:SetPoint("TOPRIGHT", charBtn, "BOTTOMRIGHT", 0, 0)

    local function RefreshCharacterSelectorText()
        if not BagFrame or not BagFrame.charSelectBtn then return end
        if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
            BagFrame.charSelectorLabel = SFrames.Bags.Container.offlineChar .. " (" .. TEXT_OFFLINE .. ")"
        else
            BagFrame.charSelectorLabel = GetCurrentCharacterName() .. " (" .. TEXT_ONLINE .. ")"
        end
    end
    BagFrame.RefreshCharacterSelectorText = RefreshCharacterSelectorText

    local function OnDropdownClick()
        local char = this.value
        if char == "ONLINE" then
            SFrames.Bags.Container.isOffline = false
            SFrames.Bags.Container.offlineChar = nil
        else
            SFrames.Bags.Container.isOffline = true
            SFrames.Bags.Container.offlineChar = char
        end
        RefreshCharacterSelectorText()
        SFrames.Bags.Container:UpdateLayout()
    end

    UIDropDownMenu_Initialize(dbMenu, function()
        local info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {}
        local currentName = GetCurrentCharacterName()

        info.text  = currentName .. " (" .. TEXT_ONLINE .. ")"
        info.value = "ONLINE"
        info.func  = OnDropdownClick
        info.checked = (not SFrames.Bags.Container.isOffline)
        UIDropDownMenu_AddButton(info)

        local chars = SFrames.Bags.Offline:GetCharacterList()
        table.sort(chars)
        for _, char in ipairs(chars) do
            if char ~= currentName then
                info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {}
                info.text  = char .. " (" .. TEXT_OFFLINE .. ")"
                info.value = char
                info.func  = OnDropdownClick
                info.checked = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar == char
                UIDropDownMenu_AddButton(info)
            end
        end
    end)

    charBtn:SetScript("OnClick", function()
        ToggleDropDownMenu(1, nil, dbMenu, this, 0, 0)
    end)
    charBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText(BagFrame.charSelectorLabel or TEXT_CHARACTER, 1, 1, 1)
        GameTooltip:Show()
    end)
    charBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    RefreshCharacterSelectorText()

    -- React to bag updates while open
    BagFrame:RegisterEvent("BAG_UPDATE")
    BagFrame:RegisterEvent("PLAYER_MONEY")
    BagFrame:RegisterEvent("BAG_UPDATE_COOLDOWN")
    BagFrame:RegisterEvent("UNIT_INVENTORY_CHANGED")
    BagFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    BagFrame:RegisterEvent("TRADE_SHOW")
    BagFrame:RegisterEvent("TRADE_CLOSED")
    BagFrame:RegisterEvent("TRADE_UPDATE")
    BagFrame:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED")
    BagFrame:RegisterEvent("TRADE_TARGET_ITEM_CHANGED")
    BagFrame:RegisterEvent("TRADE_ACCEPT_UPDATE")
    BagFrame:SetScript("OnEvent", function()
        if BagFrame:IsVisible() then
            SFrames.Bags.Container:UpdateLayout()
        end
        if event == "PLAYER_ENTERING_WORLD" then
            if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
            QueueBagSlotIconRefreshes()
        elseif event == "BAG_UPDATE" or event == "UNIT_INVENTORY_CHANGED" then
            if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
        elseif event == "TRADE_SHOW" or event == "TRADE_CLOSED" or event == "TRADE_UPDATE"
            or event == "TRADE_PLAYER_ITEM_CHANGED" or event == "TRADE_TARGET_ITEM_CHANGED"
            or event == "TRADE_ACCEPT_UPDATE" then
            if event == "TRADE_CLOSED" then
                for _, btn in ipairs(ItemSlots) do
                    if btn.tradeText then btn.tradeText:Hide() end
                    local iconTex = _G[btn:GetName() .. "IconTexture"]
                    if iconTex and iconTex.SetDesaturated then
                        pcall(function() iconTex:SetDesaturated(false) end)
                    end
                    if iconTex then iconTex:SetVertexColor(1, 1, 1, 1) end
                end
            end
            if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
            QueueBagSlotIconRefreshes()
        end
    end)

    BagFrame:Hide()
end

function SFrames.Bags.Container:Toggle()
    if not BagFrame then return end
    if BagFrame:IsVisible() then self:Close() else self:Open() end
end

function SFrames.Bags.Container:Open()
    if not BagFrame then return end
    self.isOffline = false
    self.offlineChar = nil
    if BagFrame.RefreshCharacterSelectorText then BagFrame.RefreshCharacterSelectorText() end
    -- Clear search
    if SFramesBagSearchBox then SFramesBagSearchBox:SetText("") end
    self:UpdateLayout()
    BagFrame:Show()
    local elapsed = 0
    local nextRefresh = 1
    local refreshPoints = { 0.05, 0.18, 0.40 }
    local refreshTimer = CreateFrame("Frame")
    refreshTimer:SetScript("OnUpdate", function()
        elapsed = elapsed + arg1
        if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then
            if BagFrame:IsVisible() then
                if BagFrame.UpdateBagSlotIcons then
                    BagFrame.UpdateBagSlotIcons()
                end
                SFrames.Bags.Container:UpdateLayout()
            end
            nextRefresh = nextRefresh + 1
        end
        if nextRefresh > table.getn(refreshPoints) then
            this:SetScript("OnUpdate", nil)
        end
    end)
    PlaySound("igBackPackOpen")
end

function SFrames.Bags.Container:Close()
    if not BagFrame then return end
    BagFrame:Hide()
    PlaySound("igBackPackClose")
end
