-- Materials List: standalone floating window for crafting queue reagents

local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide
local L = HDG_L or {}

HDG_MaterialsList = {}

local widget, scrollFrame, scrollContent, created
local titleSummary, costText
local toggleBtn, toggleLabel, toggleBar
local rows = {}
local trackedSeparator, trackedHeaderText  -- cached between refreshes
local CONFIG

-- Backdrop constant — built lazily on first use (CONFIG is populated at render time)
local BACKDROP_SOLID
local function GetBackdropSolid()
    if not BACKDROP_SOLID then
        BACKDROP_SOLID = { bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false }
    end
    return BACKDROP_SOLID
end

-- My Materials mode state
local popout, popoutScrollFrame, popoutContent, popoutRows = nil, nil, nil, {}
local activeReagentID = nil
local searchBox, searchClearBtn, sortBtn, searchToolbar
local currentSort = "name" -- "name" | "count" | "recipes"
local myMatsBtn, myMatsLabel

local function GetScheme() return HDG_UIHelpers and HDG_UIHelpers.GetScheme() end
local function RegisterWidget(w, t) HDG_UIHelpers.RegisterWidget(w, t) end
local function ApplyFont(fs, s, sz) if HDG.Theme then HDG.Theme.ApplyFont(fs, s, sz) end end
local function Data() return HDG_UIHelpers.Data() end

local function GetConfig(key)
    local store = HDG and HDG.Store
    local state = store and store:GetState()
    return state and state.config and state.config[key]
end

-- ============================================================================
-- ROW POOL
-- ============================================================================

local function ReleaseAllRows()
    for _, row in ipairs(rows) do
        row:Hide()
        row:ClearAllPoints()
    end
end

local function GetPooledRow(parent, height)
    for _, row in ipairs(rows) do
        if not row:IsShown() then
            row:SetParent(parent)
            row:SetHeight(height)
            row:Show()
            return row
        end
    end
    local row = CreateFrame("StatusBar", nil, parent, "BackdropTemplate")
    row:SetHeight(height)
    row:Show()
    table.insert(rows, row)
    return row
end

-- ============================================================================
-- MATERIAL ROW (mirrors LumberTracker CreateMaterialRow)
-- ============================================================================

local function CreateMaterialRow(parent, reagentId, matInfo, yOffset, rowWidth)
    local ROW_HEIGHT = 18
    local scheme = GetScheme()

    local row = GetPooledRow(parent, ROW_HEIGHT)
    row:SetSize(rowWidth, ROW_HEIGHT)
    row:SetPoint("TOPLEFT", 0, yOffset)
    row:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    if row.sourceTag then row.sourceTag:Hide() end -- hide My Materials elements when reused
    if row.chipBag then row.chipBag:Hide() end
    if row.chipBank then row.chipBank:Hide() end
    if row.chipWarband then row.chipWarband:Hide() end
    row:SetMinMaxValues(0, matInfo.needed)
    row:SetValue(math.min(matInfo.have, matInfo.needed))

    local percentage = matInfo.needed > 0 and (matInfo.have / matInfo.needed * 100) or 0
    row:SetStatusBarColor(HDG_UIHelpers.GetProgressBarColor(percentage))

    local expColor = HDG_ExpansionData.GetColor(matInfo.expansion)
    if expColor then
        row:SetBackdrop(GetBackdropSolid())
        row:SetBackdropColor(expColor[1], expColor[2], expColor[3], 0.15)
    else
        row:SetBackdrop(GetBackdropSolid())
        row:SetBackdropColor(0.05, 0.05, 0.08, 0.8)
    end

    -- Name
    local matName = HDG_Data.GetLocalizedName(reagentId, matInfo.name) or matInfo.name or "Unknown"
    if not row.nameText then
        row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.nameText:SetJustifyH("LEFT")
        row.nameText:SetWordWrap(false)
    end
    row.nameText:ClearAllPoints()
    row.nameText:SetPoint("LEFT", 4, 0)
    row.nameText:SetWidth(rowWidth - 70)
    if scheme then ApplyFont(row.nameText, scheme, "small") end
    row.nameText:SetText(matName)
    if matInfo.have >= matInfo.needed then
        if scheme then row.nameText:SetTextColor(scheme.success.r, scheme.success.g, scheme.success.b)
        else row.nameText:SetTextColor(0.53, 1, 0.53) end
    else
        if scheme then row.nameText:SetTextColor(scheme.warning.r, scheme.warning.g, scheme.warning.b)
        else row.nameText:SetTextColor(1, 0.82, 0.5) end
    end

    -- Count
    local countStr
    if matInfo.have > matInfo.needed then
        local excess = matInfo.have - matInfo.needed
        countStr = string.format("%s/%s+|cFF88FF88%s|r",
            BreakUpLargeNumbers(matInfo.needed),
            BreakUpLargeNumbers(matInfo.needed),
            BreakUpLargeNumbers(excess))
    else
        countStr = string.format("%s/%s",
            BreakUpLargeNumbers(matInfo.have),
            BreakUpLargeNumbers(matInfo.needed))
    end
    if not row.countText then
        row.countText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    end
    row.countText:ClearAllPoints()
    row.countText:SetPoint("RIGHT", -4, 0)
    if scheme then ApplyFont(row.countText, scheme, "small") end
    row.countText:SetText(countStr)

    -- Click overlay
    if not row.clickButton then
        row.clickButton = CreateFrame("Button", nil, row)
        row.clickButton:SetAllPoints(row)
        row.clickButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    end
    row.clickButton:SetScript("OnClick", function(_, button)
        if button == "LeftButton" and not IsShiftKeyDown() and not IsControlKeyDown() then
            if not AuctionHouseFrame or not AuctionHouseFrame:IsShown() then
                print("|cFFfcd34d[Housing Decor Guide]|r Please open the Auction House first")
            elseif AuctionHouseFrame.SearchBar and AuctionHouseFrame.SearchBar.SearchBox then
                AuctionHouseFrame.SearchBar.SearchBox:SetText(matName)
                print("|cFFfcd34d[Housing Decor Guide]|r Set AH search to: " .. matName)
            end
        elseif button == "LeftButton" and IsShiftKeyDown() then
            if not AuctionHouseFrame or not AuctionHouseFrame:IsShown() then
                print("|cFFfcd34d[Housing Decor Guide]|r Please open the Auction House first")
            elseif C_AddOns.IsAddOnLoaded("Auctionator") and Auctionator and Auctionator.API and Auctionator.API.v1 and Auctionator.API.v1.MultiSearch then
                Auctionator.API.v1.MultiSearch("HousingDecorGuide", {matName})
                print("|cFFfcd34d[Housing Decor Guide]|r Searching Auctionator for: " .. matName)
            end
        elseif button == "RightButton" then
            if C_AddOns.IsAddOnLoaded("Auctionator") and Auctionator and Auctionator.API and Auctionator.API.v1 then
                local callerID = "HousingDecorGuide"
                local listName = "Housing Decor"
                local existingItems = {}
                if Auctionator.API.v1.GetCraftingQueueItems then
                    local success, items = pcall(Auctionator.API.v1.GetCraftingQueueItems, callerID, listName)
                    if success and items then existingItems = items end
                end
                local exists = false
                for _, existingItem in ipairs(existingItems) do
                    if existingItem == matName then exists = true; break end
                end
                if not exists then table.insert(existingItems, matName) end
                if Auctionator.API.v1.CreateShoppingList then
                    Auctionator.API.v1.CreateShoppingList(callerID, listName, existingItems)
                    print("|cFFfcd34d[Housing Decor Guide]|r Added '" .. matName .. "' to Auctionator list")
                end
            else
                print("|cFFfcd34d[Housing Decor Guide]|r Install Auctionator for search list integration")
            end
        end
    end)
    row.clickButton:SetScript("OnEnter", function(self)
        -- Anchor to right of materials frame, vertically aligned with the row.
        -- If the recipe popout is open in that slot, anchor to its right instead
        -- so the tooltip doesn't cover it.
        GameTooltip:SetOwner(self, "ANCHOR_NONE")
        GameTooltip:ClearAllPoints()
        if popout and popout:IsShown() then
            GameTooltip:SetPoint("TOPLEFT", popout, "TOPRIGHT", 4, 0)
        elseif widget and widget.frame and self:GetTop() and widget.frame:GetTop() then
            GameTooltip:SetPoint("TOPLEFT", widget.frame, "TOPRIGHT", 4, self:GetTop() - widget.frame:GetTop())
        else
            GameTooltip:SetPoint("CENTER", UIParent, "CENTER")
        end
        GameTooltip:SetItemByID(reagentId)
        GameTooltip:AddLine(" ")
        GameTooltip:AddDoubleLine(L["TIP_SOURCE_LABEL"], matInfo.source or "Unknown", 1, 1, 1, 0.7, 0.7, 0.7)
        GameTooltip:AddDoubleLine(L["TIP_HAVE_LABEL"], BreakUpLargeNumbers(matInfo.have), 1, 1, 1, 1, 1, 1)
        GameTooltip:AddDoubleLine(L["TIP_NEED_LABEL"], BreakUpLargeNumbers(matInfo.needed), 1, 1, 1, 1, 1, 1)
        if matInfo.have >= matInfo.needed then
            GameTooltip:AddLine(L["TIP_HAVE_ENOUGH"])
        else
            local shortfall = matInfo.needed - matInfo.have
            GameTooltip:AddLine(string.format(L["TIP_NEED_N_MORE"], BreakUpLargeNumbers(shortfall)))
        end
        local price = Data().GetItemPrice and Data().GetItemPrice(reagentId)
        if price and price > 0 then
            GameTooltip:AddLine(" ")
            local priceStr = Data().FormatGold and Data().FormatGold(price) or tostring(price)
            GameTooltip:AddDoubleLine(L["TIP_UNIT_PRICE"], priceStr, 1, 1, 1, 1, 0.82, 0.2)
            if matInfo.have < matInfo.needed then
                local stillNeeded = matInfo.needed - matInfo.have
                local totalCost = price * stillNeeded
                local cStr = Data().FormatGold and Data().FormatGold(totalCost) or tostring(totalCost)
                GameTooltip:AddDoubleLine(L["TIP_EST_COST"], cStr, 1, 1, 1, 1, 0.82, 0.2)
            end
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(L["TIP_CLICK_AH_SEARCH"], 0.7, 0.7, 0.7)
        GameTooltip:AddLine(L["TIP_SHIFTCLICK_AUCTIONATOR"], 0.7, 0.7, 0.7)
        GameTooltip:AddLine(L["TIP_RIGHTCLICK_AUCTIONATOR"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    row.clickButton:SetScript("OnLeave", function() GameTooltip:Hide() end)

    return yOffset - (ROW_HEIGHT + 1)
end

-- ============================================================================
-- TRACKED REAGENT ROW
-- ============================================================================

local trackedRows = {}

local function ReleaseTrackedRows()
    for _, row in ipairs(trackedRows) do
        row:Hide()
        row:ClearAllPoints()
    end
end

local function GetTrackedRow(parent, height)
    for _, row in ipairs(trackedRows) do
        if not row:IsShown() then
            row:SetParent(parent)
            row:SetHeight(height)
            row:Show()
            return row
        end
    end
    local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    row:SetHeight(height)
    row:Show()
    table.insert(trackedRows, row)
    return row
end

local function CreateTrackedReagentRow(parent, reagentId, matInfo, yOffset, rowWidth)
    local ROW_HEIGHT = 18
    local scheme = GetScheme()

    local row = GetTrackedRow(parent, ROW_HEIGHT)
    row:SetSize(rowWidth, ROW_HEIGHT)
    row:SetPoint("TOPLEFT", 0, yOffset)
    row:SetBackdrop(GetBackdropSolid())
    if scheme then
        row:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
    else
        row:SetBackdropColor(0.08, 0.35, 0.33, 0.3)
    end

    local matName = matInfo.name or "Unknown"
    if not row.nameText then
        row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.nameText:SetPoint("LEFT", 4, 0)
        row.nameText:SetJustifyH("LEFT")
        row.nameText:SetWordWrap(false)
    end
    row.nameText:SetWidth(rowWidth - 85)
    if scheme then ApplyFont(row.nameText, scheme, "small") end
    row.nameText:SetText(matName)
    if scheme then
        row.nameText:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b)
    else
        row.nameText:SetTextColor(unpack(CONFIG.COLORS.PANEL_DARK_BORDER_TEAL))
    end

    -- Count
    local countStr
    if matInfo.needed > 0 then
        if matInfo.have >= matInfo.needed then
            countStr = string.format("|cFF88FF88%s|r/%s", BreakUpLargeNumbers(matInfo.have), BreakUpLargeNumbers(matInfo.needed))
        else
            countStr = string.format("%s/%s", BreakUpLargeNumbers(matInfo.have), BreakUpLargeNumbers(matInfo.needed))
        end
    else
        countStr = string.format("|cFFe2e8f0%s|r", BreakUpLargeNumbers(matInfo.have))
    end
    if not row.countText then
        row.countText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.countText:SetPoint("RIGHT", -20, 0)
    end
    if scheme then ApplyFont(row.countText, scheme, "small") end
    row.countText:SetText(countStr)

    -- X button
    if not row.removeBtn then
        row.removeBtn = CreateFrame("Button", nil, row)
        row.removeBtn:SetSize(14, 14)
        row.removeBtn:SetPoint("RIGHT", -3, 0)
        row.removeTex = row.removeBtn:CreateTexture(nil, "ARTWORK")
        row.removeTex:SetAllPoints()
        row.removeTex:SetTexture("Interface\\Buttons\\UI-StopButton")
    end
    local removeTex = row.removeTex
    if scheme then
        removeTex:SetVertexColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 0.8)
    else
        removeTex:SetVertexColor(CONFIG.COLORS.TEXT_INACTIVE[1], CONFIG.COLORS.TEXT_INACTIVE[2], CONFIG.COLORS.TEXT_INACTIVE[3], 0.8)
    end
    row.removeBtn:SetScript("OnEnter", function(self)
        local s = GetScheme()
        if s then removeTex:SetVertexColor(s.error.r, s.error.g, s.error.b, 1)
        else removeTex:SetVertexColor(1, 0.3, 0.3, 1) end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(L["BTN_STOP_TRACKING"] or "Stop Tracking", 1, 0.3, 0.3)
        GameTooltip:AddLine(L["TIP_CLICK_REMOVE_REAGENT"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    row.removeBtn:SetScript("OnLeave", function()
        local s = GetScheme()
        if s then removeTex:SetVertexColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, 0.8)
        else removeTex:SetVertexColor(CONFIG.COLORS.TEXT_INACTIVE[1], CONFIG.COLORS.TEXT_INACTIVE[2], CONFIG.COLORS.TEXT_INACTIVE[3], 0.8) end
        GameTooltip:Hide()
    end)
    row.removeBtn:SetScript("OnClick", function()
        HDG_LumberTracker.UntrackReagent(reagentId)
    end)

    -- Click overlay (left of X button)
    if not row.clickButton then
        row.clickButton = CreateFrame("Button", nil, row)
        row.clickButton:SetPoint("TOPLEFT", 0, 0)
        row.clickButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    end
    row.clickButton:SetPoint("BOTTOMRIGHT", row.removeBtn, "BOTTOMLEFT", -2, 0)
    row.clickButton:SetScript("OnClick", function(_, button)
        if button == "LeftButton" and not IsShiftKeyDown() then
            if not AuctionHouseFrame or not AuctionHouseFrame:IsShown() then
                print("|cFFfcd34d[Housing Decor Guide]|r Please open the Auction House first")
            elseif AuctionHouseFrame.SearchBar and AuctionHouseFrame.SearchBar.SearchBox then
                AuctionHouseFrame.SearchBar.SearchBox:SetText(matName)
                print("|cFFfcd34d[Housing Decor Guide]|r Set AH search to: " .. matName)
            end
        end
    end)
    row.clickButton:SetScript("OnEnter", function(self)
        -- Anchor to right of materials frame, vertically aligned with the row.
        -- If the recipe popout is open in that slot, anchor to its right instead
        -- so the tooltip doesn't cover it.
        GameTooltip:SetOwner(self, "ANCHOR_NONE")
        GameTooltip:ClearAllPoints()
        if popout and popout:IsShown() then
            GameTooltip:SetPoint("TOPLEFT", popout, "TOPRIGHT", 4, 0)
        elseif widget and widget.frame and self:GetTop() and widget.frame:GetTop() then
            GameTooltip:SetPoint("TOPLEFT", widget.frame, "TOPRIGHT", 4, self:GetTop() - widget.frame:GetTop())
        else
            GameTooltip:SetPoint("CENTER", UIParent, "CENTER")
        end
        GameTooltip:SetItemByID(reagentId)
        GameTooltip:AddLine(" ")
        GameTooltip:AddDoubleLine(L["TIP_HAVE_LABEL"], BreakUpLargeNumbers(matInfo.have), 1, 1, 1, 1, 1, 1)
        if matInfo.needed > 0 then
            GameTooltip:AddDoubleLine(L["TIP_TARGET_LABEL"], BreakUpLargeNumbers(matInfo.needed), 1, 1, 1, 1, 1, 1)
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(L["TIP_CLICK_AH_SEARCH"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    row.clickButton:SetScript("OnLeave", function() GameTooltip:Hide() end)

    return yOffset - (ROW_HEIGHT + 1)
end

-- ============================================================================
-- PANEL CREATION
-- ============================================================================

local function CreatePanel()
    if created then return end
    created = true

    local PANEL_W = 300
    local PANEL_H = 400
    local INSET = 4
    local TITLE_H = 28

    widget = HDG.UI:FloatingWidget({
        name = "HDG_MaterialsListFrame",
        width = PANEL_W,
        height = PANEL_H,
        strata = "DIALOG",
        onPositionSaved = function(point, relativePoint, x, y)
            HDG.Store:SetUI("materialsListPosition", { point, relativePoint, x, y })
        end,
    })

    local frame = widget.frame
    local scheme = GetScheme()

    -- Restore saved position
    local pos = HDG.Store:GetState().ui.materialsListPosition
    if pos and pos[1] then
        frame:ClearAllPoints()
        frame:SetPoint(pos[1], UIParent, pos[2] or pos[1], pos[3] or 0, pos[4] or 0)
    end

    frame:Hide()

    -- Vertical resize
    frame:SetResizable(true)
    frame:SetResizeBounds(PANEL_W, 300, PANEL_W, 800)
    local savedH = HDG.Store and HDG.Store:GetState().ui.materialsListHeight
    if savedH then frame:SetHeight(savedH) end

    local grip = CreateFrame("Button", nil, frame)
    grip:SetHeight(16)
    grip:SetPoint("BOTTOMLEFT", 0, -6)
    grip:SetPoint("BOTTOMRIGHT", 0, -6)
    grip:EnableMouse(true)
    grip:SetScript("OnMouseDown", function()
        -- Pin TOPLEFT to current screen position before resizing. Without this,
        -- StartSizing("BOTTOM") on a singly-anchored frame (RIGHT/saved-anchor)
        -- snaps the frame to an extreme height before settling — visible as
        -- "full height jump" before the user's actual drag takes effect.
        local left, top = frame:GetLeft(), frame:GetTop()
        if left and top then
            frame:ClearAllPoints()
            frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
        end
        frame:StartSizing("BOTTOM")
    end)
    grip:SetScript("OnMouseUp", function()
        frame:StopMovingOrSizing()
        HDG.Store:SetUI("materialsListHeight", math.floor(frame:GetHeight() + 0.5))
        -- Persist the new top-left position too, since we re-anchored.
        local point, _, relativePoint, xOfs, yOfs = frame:GetPoint()
        if point then
            HDG.Store:SetUI("materialsListPosition", { point, relativePoint, xOfs, yOfs })
        end
    end)
    local gripLine = grip:CreateTexture(nil, "ARTWORK")
    gripLine:SetHeight(2)
    gripLine:SetPoint("LEFT", grip, "LEFT", 4, 3)
    gripLine:SetPoint("RIGHT", grip, "RIGHT", -4, 3)
    gripLine:SetTexture("Interface\\Buttons\\WHITE8x8")
    gripLine:SetVertexColor(1, 1, 1, 0.1)
    local gripIcon = grip:CreateTexture(nil, "ARTWORK")
    gripIcon:SetSize(36, 12)
    gripIcon:SetPoint("CENTER", grip, "CENTER", 0, -1)
    gripIcon:SetAtlas("lootroll-resizehandle")
    grip:SetScript("OnEnter", function() gripLine:SetVertexColor(1, 1, 1, 0.3) end)
    grip:SetScript("OnLeave", function() gripLine:SetVertexColor(1, 1, 1, 0.1) end)

    -- Backdrop
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 2,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    frame:SetBackdropColor(unpack(CONFIG.COLORS.PANEL_DARK))
    frame:SetBackdropBorderColor(unpack(CONFIG.COLORS.PANEL_DARK_BORDER_WARM))

    -- ESC key closes (unless pinned)
    if not (HDG.Store and HDG.Store.state and HDG.Store.state.ui and HDG.Store.state.ui.materialsListPinned) then
        table.insert(UISpecialFrames, "HDG_MaterialsListFrame")
    end

    -- Title bar
    local titleBar = CreateFrame("Frame", nil, frame)
    titleBar:SetPoint("TOPLEFT", INSET, -INSET)
    titleBar:SetPoint("TOPRIGHT", -INSET, -INSET)
    titleBar:SetHeight(TITLE_H)
    titleBar:SetFrameLevel(frame:GetFrameLevel() + 5)

    local titleBg = titleBar:CreateTexture(nil, "BACKGROUND")
    titleBg:SetAtlas("catalog-nav-bg-primary")
    titleBg:SetAllPoints()
    titleBg:SetAlpha(0.8)

    local bagIcon = titleBar:CreateTexture(nil, "ARTWORK")
    bagIcon:SetSize(16, 16)
    bagIcon:SetPoint("LEFT", 8, 0)
    bagIcon:SetTexture("Interface\\MINIMAP\\TRACKING\\Banker")
    if scheme then bagIcon:SetVertexColor(scheme.accent.r, scheme.accent.g, scheme.accent.b) end

    local titleText = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    titleText:SetPoint("LEFT", bagIcon, "RIGHT", 6, 0)
    if scheme then
        ApplyFont(titleText, scheme, "body")
        titleText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
    end
    titleText:SetText(L["HEADER_MATERIALS"])

    titleSummary = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    titleSummary:SetPoint("RIGHT", -48, 0)
    titleSummary:SetJustifyH("RIGHT")
    if scheme then ApplyFont(titleSummary, scheme, "small") end
    titleSummary:SetText("")

    -- Pin button (persist through Escape + reload)
    local pinBtn = CreateFrame("Button", nil, titleBar)
    pinBtn:SetSize(16, 16)
    pinBtn:SetPoint("RIGHT", -26, 0)
    pinBtn:SetFrameLevel(titleBar:GetFrameLevel() + 2)
    local pinIcon = pinBtn:CreateTexture(nil, "ARTWORK")
    pinIcon:SetAllPoints()
    pinIcon:SetAtlas("friendslist-recentallies-Pin-yellow")
    pinBtn.icon = pinIcon

    local function UpdatePinVisual()
        local isPinned = HDG.Store and HDG.Store.state and HDG.Store.state.ui and HDG.Store.state.ui.materialsListPinned
        pinIcon:SetDesaturated(not isPinned)
        pinIcon:SetVertexColor(isPinned and 1 or 0.6, isPinned and 1 or 0.6, isPinned and 0 or 0.6, 1)
    end
    UpdatePinVisual()

    pinBtn:SetScript("OnClick", function()
        local isPinned = HDG.Store:GetState().ui.materialsListPinned
        local newPinned = not isPinned
        HDG.Store:SetUI("materialsListPinned", newPinned)
        UpdatePinVisual()
        local frameName = "HDG_MaterialsListFrame"
        if newPinned then
            for i = #UISpecialFrames, 1, -1 do
                if UISpecialFrames[i] == frameName then table.remove(UISpecialFrames, i); break end
            end
        else
            local found = false
            for _, v in ipairs(UISpecialFrames) do if v == frameName then found = true; break end end
            if not found then table.insert(UISpecialFrames, frameName) end
        end
    end)
    pinBtn:SetScript("OnEnter", function(self)
        local isPinned = HDG.Store.state.ui.materialsListPinned
        pinIcon:SetVertexColor(isPinned and 1 or 0.85, isPinned and 1 or 0.85, isPinned and 0.7 or 0.85, 1)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(L["TIP_PIN_WINDOW"], 1, 1, 1)
        if isPinned then
            GameTooltip:AddLine(L["TIP_PINNED_DESC"], 0.7, 0.7, 0.7)
        else
            GameTooltip:AddLine(L["TIP_CLICK_PIN"], 0.7, 0.7, 0.7)
        end
        GameTooltip:Show()
    end)
    pinBtn:SetScript("OnLeave", function() UpdatePinVisual(); GameTooltip:Hide() end)

    local closeBtn = CreateFrame("Button", nil, titleBar)
    closeBtn:SetSize(16, 16)
    closeBtn:SetPoint("RIGHT", -6, 0)
    closeBtn:SetFrameLevel(titleBar:GetFrameLevel() + 2)
    local closeTex = closeBtn:CreateTexture(nil, "ARTWORK")
    closeTex:SetSize(16, 16)
    closeTex:SetPoint("CENTER")
    closeTex:SetAtlas("XMarksTheSpot")
    closeBtn:SetScript("OnClick", function()
        if popout then popout:Hide() end
        activeReagentID = nil
        frame:Hide()
    end)
    HDG_UIHelpers.SetupTooltip(closeBtn, {
        title = L["TIP_CLOSE"],
        titleColor = {1, 1, 1},
        anchor = "ANCHOR_RIGHT",
    })

    -- Direct/Raw toggle bar
    toggleBar = CreateFrame("Frame", nil, frame)
    toggleBar:SetPoint("TOPLEFT", titleBar, "BOTTOMLEFT", 0, -2)
    toggleBar:SetPoint("TOPRIGHT", titleBar, "BOTTOMRIGHT", 0, -2)
    toggleBar:SetHeight(20)

    toggleBtn = CreateFrame("Button", nil, toggleBar)
    toggleBtn:SetSize(14, 14)
    toggleBtn:SetPoint("LEFT", 8, 0)
    local toggleTex = toggleBtn:CreateTexture(nil, "ARTWORK")
    toggleTex:SetAllPoints()
    toggleTex:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
    toggleBtn.tex = toggleTex

    toggleLabel = toggleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    toggleLabel:SetPoint("LEFT", toggleBtn, "RIGHT", 4, 0)
    if scheme then ApplyFont(toggleLabel, scheme, "small") end

    costText = toggleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    costText:SetPoint("RIGHT", -8, 0)
    costText:SetJustifyH("RIGHT")
    if scheme then ApplyFont(costText, scheme, "small") end

    local function UpdateToggleLabel()
        local isRaw = GetConfig("showRawMaterials")
        toggleLabel:SetText(isRaw and L["HEADER_RAW_MATERIALS"] or L["HEADER_DIRECT_MATERIALS"])
        toggleTex:SetDesaturated(not isRaw)
        if isRaw then
            toggleTex:SetVertexColor(0.3, 0.9, 0.3, 1)
        else
            toggleTex:SetVertexColor(0.7, 0.7, 0.7, 0.6)
        end
    end
    UpdateToggleLabel()

    toggleBtn:SetScript("OnClick", function()
        local isRaw = GetConfig("showRawMaterials")
        HDG.Store:Dispatch("SET_CONFIG", { key = "showRawMaterials", value = not isRaw })
        UpdateToggleLabel()
        HDG_MaterialsList.Refresh()
    end)
    toggleBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local isRaw = GetConfig("showRawMaterials")
        if isRaw then
            GameTooltip:SetText(L["TIP_SHOWING_RAW"], 1, 1, 1)
            GameTooltip:AddLine(L["TIP_CLICK_SHOW_DIRECT"], 0.7, 0.7, 0.7)
        else
            GameTooltip:SetText(L["TIP_SHOWING_DIRECT"], 1, 1, 1)
            GameTooltip:AddLine(L["TIP_CLICK_SHOW_RAW"], 0.7, 0.7, 0.7)
        end
        GameTooltip:Show()
    end)
    toggleBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Make the label clickable too
    toggleLabel:EnableMouse(true)
    toggleLabel:SetScript("OnMouseUp", function() toggleBtn:Click() end)

    -- "My Materials" mode button (right side of toggle bar)
    myMatsBtn = CreateFrame("Button", nil, toggleBar, "BackdropTemplate")
    myMatsBtn:SetSize(90, 16)
    myMatsBtn:SetPoint("RIGHT", costText, "LEFT", -6, 0)
    myMatsBtn:SetBackdrop(GetBackdropSolid())
    myMatsLabel = myMatsBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    myMatsLabel:SetPoint("CENTER")
    if scheme then ApplyFont(myMatsLabel, scheme, "small") end

    local function UpdateMyMatsVisual()
        local isMyMats = GetConfig("myMaterialsMode")
        if isMyMats then
            if scheme then
                myMatsBtn:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
                myMatsLabel:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b)
            else
                myMatsBtn:SetBackdropColor(CONFIG.COLORS.PANEL_DARK_BORDER_TEAL[1], CONFIG.COLORS.PANEL_DARK_BORDER_TEAL[2], CONFIG.COLORS.PANEL_DARK_BORDER_TEAL[3], 0.3)
                myMatsLabel:SetTextColor(unpack(CONFIG.COLORS.PANEL_DARK_BORDER_TEAL))
            end
            myMatsLabel:SetText(L["BTN_ALL_MY_MATS"])
        else
            myMatsBtn:SetBackdropColor(0.2, 0.2, 0.2, 0.4)
            if scheme then myMatsLabel:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
            else myMatsLabel:SetTextColor(unpack(CONFIG.COLORS.TEXT_INACTIVE)) end
            myMatsLabel:SetText(L["BTN_ALL_MY_MATS"])
        end
    end
    UpdateMyMatsVisual()

    myMatsBtn:SetScript("OnClick", function()
        local isMyMats = GetConfig("myMaterialsMode")
        HDG.Store:SetConfig("myMaterialsMode", not isMyMats)
        UpdateMyMatsVisual()
        if popout then popout:Hide() end
        activeReagentID = nil
        if searchBox then searchBox:SetText("") end
        currentSort = "name"
        HDG_MaterialsList.Refresh()
    end)
    myMatsBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local isMyMats = GetConfig("myMaterialsMode")
        if isMyMats then
            GameTooltip:SetText(L["TIP_SHOWING_MY_MATS"], 1, 1, 1)
            GameTooltip:AddLine(L["TIP_CLICK_RETURN_QUEUE"], 0.7, 0.7, 0.7)
        else
            GameTooltip:SetText(L["TIP_MY_MATERIALS"], 1, 1, 1)
            GameTooltip:AddLine(L["TIP_CLICK_BROWSE_REAGENTS"], 0.7, 0.7, 0.7)
        end
        GameTooltip:Show()
    end)
    myMatsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Search/sort toolbar (only visible in My Materials mode)
    searchToolbar = CreateFrame("Frame", nil, frame)
    searchToolbar:SetPoint("TOPLEFT", toggleBar, "BOTTOMLEFT", 0, -2)
    searchToolbar:SetPoint("TOPRIGHT", toggleBar, "BOTTOMRIGHT", 0, -2)
    searchToolbar:SetHeight(22)
    searchToolbar:Hide()

    searchBox = CreateFrame("EditBox", nil, searchToolbar, "InputBoxTemplate")
    searchBox:SetSize(200, 18)
    searchBox:SetPoint("LEFT", 8, 0)
    searchBox:SetPoint("RIGHT", -50, 0)
    searchBox:SetAutoFocus(false)
    searchBox:SetMaxLetters(40)
    if scheme then ApplyFont(searchBox, scheme, "small") end
    local searchTimer
    searchBox:SetScript("OnTextChanged", function()
        if searchTimer then searchTimer:Cancel() end
        searchTimer = C_Timer.NewTimer(0.25, function() HDG_MaterialsList.Refresh() end)
    end)
    searchBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)

    -- Placeholder text
    local placeholder = searchBox:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    placeholder:SetPoint("LEFT", 4, 0)
    placeholder:SetTextColor(CONFIG.COLORS.TEXT_DIM[1], CONFIG.COLORS.TEXT_DIM[2], CONFIG.COLORS.TEXT_DIM[3], 0.7)
    placeholder:SetText(L["PLACEHOLDER_SEARCH_MATS"])
    if scheme then ApplyFont(placeholder, scheme, "small") end
    searchBox:SetScript("OnEditFocusGained", function() placeholder:Hide() end)
    searchBox:SetScript("OnEditFocusLost", function()
        if searchBox:GetText() == "" then placeholder:Show() end
    end)
    searchBox.placeholder = placeholder

    searchClearBtn = CreateFrame("Button", nil, searchBox)
    searchClearBtn:SetSize(12, 12)
    searchClearBtn:SetPoint("RIGHT", -2, 0)
    local clearTex = searchClearBtn:CreateTexture(nil, "ARTWORK")
    clearTex:SetAllPoints()
    clearTex:SetAtlas("XMarksTheSpot")
    searchClearBtn:SetScript("OnClick", function()
        searchBox:SetText("")
        searchBox:ClearFocus()
    end)
    searchClearBtn:Hide()

    -- Sort cycle button
    sortBtn = CreateFrame("Button", nil, searchToolbar, "BackdropTemplate")
    sortBtn:SetSize(36, 18)
    sortBtn:SetPoint("RIGHT", -6, 0)
    sortBtn:SetBackdrop(GetBackdropSolid())
    sortBtn:SetBackdropColor(0.15, 0.15, 0.18, 0.8)
    local sortLabel = sortBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    sortLabel:SetPoint("CENTER")
    if scheme then ApplyFont(sortLabel, scheme, "small") end
    sortLabel:SetText("A-Z")
    sortBtn.label = sortLabel

    local SORT_MODES = { "name", "count", "recipes" }
    local SORT_LABELS = { name = "A-Z", count = "#", recipes = "R" }

    sortBtn:SetScript("OnClick", function()
        for i, mode in ipairs(SORT_MODES) do
            if mode == currentSort then
                currentSort = SORT_MODES[(i % #SORT_MODES) + 1]
                break
            end
        end
        sortBtn.label:SetText(SORT_LABELS[currentSort] or "A-Z")
        HDG_MaterialsList.Refresh()
    end)
    sortBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local labels = { name = "Name (A-Z)", count = "Count (most owned first)", recipes = "Recipes (most versatile first)" }
        GameTooltip:SetText(string.format(L["TIP_SORT_PREFIX"], labels[currentSort] or "Name"), 1, 1, 1)
        GameTooltip:AddLine(L["TIP_CLICK_CYCLE_SORT"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    sortBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Scroll frame
    scrollFrame = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
    scrollFrame:SetFrameLevel(frame:GetFrameLevel() + 3)
    scrollFrame:SetPoint("TOPLEFT", toggleBar, "BOTTOMLEFT", INSET, -4)
    scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -(INSET + 16), INSET + 10)

    scrollContent = CreateFrame("Frame", nil, scrollFrame)
    scrollContent:SetWidth(PANEL_W - 2 * INSET - 20)
    scrollContent:SetHeight(1)
    scrollFrame:SetScrollChild(scrollContent)

    -- Minimal scrollbar styling
    local scrollBar = scrollFrame.ScrollBar
    if scrollBar then
        scrollBar:SetWidth(8)
        if scrollBar.ScrollUpButton then scrollBar.ScrollUpButton:SetAlpha(0); scrollBar.ScrollUpButton:SetSize(1, 1) end
        if scrollBar.ScrollDownButton then scrollBar.ScrollDownButton:SetAlpha(0); scrollBar.ScrollDownButton:SetSize(1, 1) end
        local trackBg = scrollBar:CreateTexture(nil, "BACKGROUND")
        trackBg:SetAtlas("!minimal-scrollbar-track-middle")
        trackBg:SetPoint("TOPLEFT", 0, 0)
        trackBg:SetPoint("BOTTOMRIGHT", 0, 0)
        trackBg:SetAlpha(0.3)
        local thumb = scrollBar:GetThumbTexture()
        if thumb then
            thumb:SetAtlas("minimal-scrollbar-thumb-middle")
            thumb:SetWidth(8)
            thumb:SetAlpha(0.7)
        end
    end
end

-- ============================================================================
-- MY MATERIALS: Data Gatherer
-- ============================================================================

local function GetMyMaterials()
    if not HDG_PowerCrafting or not HDG_PowerCrafting.GetAllHousingReagentIDs then return {} end
    local reagentIDs = HDG_PowerCrafting.GetAllHousingReagentIDs()
    local results = {}

    for _, itemID in ipairs(reagentIDs) do
        local have = HDG_Data and HDG_Data.GetItemCount(itemID) or 0
        if have > 0 then
            local recipes = HDG_PowerCrafting.GetRecipesForReagent(itemID)
            local recipeCount = recipes and #recipes or 0
            local source = HDG_ReagentsDB and HDG_ReagentsDB[itemID] and HDG_ReagentsDB[itemID][1] or "Unknown"
            local name = (HDG_PowerCrafting.GetItemName and HDG_PowerCrafting.GetItemName(itemID))
                      or (HDG_Data and HDG_Data.GetCachedItemName and HDG_Data.GetCachedItemName(itemID))
                      or "Unknown"
            results[itemID] = {
                id = itemID,
                name = name,
                have = have,
                recipeCount = recipeCount,
                source = source,
            }
        end
    end
    return results
end

-- ============================================================================
-- MY MATERIALS: Row Factory
-- ============================================================================

local SOURCE_TAGS = { Gathering = "G", Vendor = "V", Crafted = "C" }

local function CreateMyMaterialsRow(parent, itemID, matData, yOffset, rowWidth)
    local ROW_HEIGHT = 18
    local scheme = GetScheme()

    local row = GetPooledRow(parent, ROW_HEIGHT)
    row:SetSize(rowWidth, ROW_HEIGHT)
    row:SetPoint("TOPLEFT", 0, yOffset)
    row:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    row:SetMinMaxValues(0, 1)
    row:SetValue(0) -- no progress bar in browse mode
    row:SetStatusBarColor(0, 0, 0, 0)

    row:SetBackdrop(GetBackdropSolid())
    if activeReagentID == itemID then
        if scheme then row:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.2)
        else row:SetBackdropColor(0.08, 0.72, 0.65, 0.2) end
    else
        row:SetBackdropColor(0.05, 0.05, 0.08, 0.8)
    end

    -- Source tag (dim, left)
    local sourceBase = matData.source and matData.source:match("^(%a+)") or "Unknown"
    local tag = SOURCE_TAGS[sourceBase] or "?"
    if not row.sourceTag then
        row.sourceTag = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row.sourceTag:SetPoint("LEFT", 4, 0)
        row.sourceTag:SetWidth(12)
        row.sourceTag:SetJustifyH("LEFT")
    end
    row.sourceTag:Show()
    if scheme then ApplyFont(row.sourceTag, scheme, "small") end
    row.sourceTag:SetText(tag)
    if scheme then row.sourceTag:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
    else row.sourceTag:SetTextColor(unpack(CONFIG.COLORS.TEXT_DIM)) end

    -- Name + recipe count badge
    local displayName = matData.name or "Unknown"
    local badge = matData.recipeCount > 0 and string.format(" |cFF888888(%d)|r", matData.recipeCount) or ""
    if not row.nameText then
        row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.nameText:SetJustifyH("LEFT")
        row.nameText:SetWordWrap(false)
    end
    row.nameText:ClearAllPoints()
    row.nameText:SetPoint("LEFT", 18, 0)
    row.nameText:SetWidth(rowWidth - 80)
    if scheme then ApplyFont(row.nameText, scheme, "small") end
    row.nameText:SetText(displayName .. badge)
    if scheme then row.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
    else row.nameText:SetTextColor(unpack(CONFIG.COLORS.TEXT_BODY)) end

    -- Count (right-aligned, green) — shifted left to make room for location chips
    if not row.countText then
        row.countText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    end
    row.countText:ClearAllPoints()
    row.countText:SetPoint("RIGHT", -50, 0)
    if scheme then ApplyFont(row.countText, scheme, "small") end
    row.countText:SetText("|cFF88FF88x" .. BreakUpLargeNumbers(matData.have) .. "|r")

    -- Location chips (bag / bank / warband icons)
    local CHIP_SIZE = 14
    local bagCount = C_Item.GetItemCount(itemID, false, false, false, false) or 0
    local personalCount = C_Item.GetItemCount(itemID, true, false, true, false) or 0
    local bankCount = personalCount - bagCount
    local warbandCount = matData.have - personalCount
    -- Cache counts on the row so OnEnter tooltip can read them without re-querying
    row._cachedBag = bagCount
    row._cachedBank = bankCount
    row._cachedWarband = warbandCount

    -- Create chip textures lazily, reuse across refreshes
    if not row.chipBag then
        row.chipBag = row:CreateTexture(nil, "OVERLAY")
        row.chipBag:SetSize(CHIP_SIZE, CHIP_SIZE)
    end
    if not row.chipBank then
        row.chipBank = row:CreateTexture(nil, "OVERLAY")
        row.chipBank:SetSize(CHIP_SIZE, CHIP_SIZE)
    end
    if not row.chipWarband then
        row.chipWarband = row:CreateTexture(nil, "OVERLAY")
        row.chipWarband:SetSize(CHIP_SIZE, CHIP_SIZE)
    end

    -- Position chips right-aligned in a compact strip
    local chipX = -3
    row.chipBag:Hide(); row.chipBank:Hide(); row.chipWarband:Hide()

    if warbandCount > 0 then
        row.chipWarband:ClearAllPoints()
        row.chipWarband:SetPoint("RIGHT", chipX, 0)
        row.chipWarband:SetAtlas("warbands-icon")
        row.chipWarband:Show()
        chipX = chipX - (CHIP_SIZE + 1)
    end
    if bankCount > 0 then
        row.chipBank:ClearAllPoints()
        row.chipBank:SetPoint("RIGHT", chipX, 0)
        row.chipBank:SetAtlas("Banker")
        row.chipBank:Show()
        chipX = chipX - (CHIP_SIZE + 1)
    end
    if bagCount > 0 then
        row.chipBag:ClearAllPoints()
        row.chipBag:SetPoint("RIGHT", chipX, 0)
        row.chipBag:SetAtlas("plunderstorm-icon-item")
        row.chipBag:Show()
    end

    -- Click overlay
    if not row.clickButton then
        row.clickButton = CreateFrame("Button", nil, row)
        row.clickButton:SetAllPoints(row)
        row.clickButton:RegisterForClicks("LeftButtonUp")
    end
    row.clickButton:SetScript("OnClick", function()
        if activeReagentID == itemID then
            -- Toggle off
            activeReagentID = nil
            if popout then popout:Hide() end
        else
            activeReagentID = itemID
            HDG_MaterialsList.ShowPopout(itemID, matData.name, row)
        end
        HDG_MaterialsList.Refresh()
    end)
    row.clickButton:SetScript("OnEnter", function(self)
        -- Anchor to right of materials frame, vertically aligned with the row.
        -- If the recipe popout is open in that slot, anchor to its right instead
        -- so the tooltip doesn't cover it.
        GameTooltip:SetOwner(self, "ANCHOR_NONE")
        GameTooltip:ClearAllPoints()
        if popout and popout:IsShown() then
            GameTooltip:SetPoint("TOPLEFT", popout, "TOPRIGHT", 4, 0)
        elseif widget and widget.frame and self:GetTop() and widget.frame:GetTop() then
            GameTooltip:SetPoint("TOPLEFT", widget.frame, "TOPRIGHT", 4, self:GetTop() - widget.frame:GetTop())
        else
            GameTooltip:SetPoint("CENTER", UIParent, "CENTER")
        end
        GameTooltip:SetItemByID(itemID)
        GameTooltip:AddLine(" ")
        GameTooltip:AddDoubleLine(L["TIP_SOURCE_LABEL"], matData.source or "Unknown", 1, 1, 1, 0.7, 0.7, 0.7)
        GameTooltip:AddDoubleLine(L["TIP_HAVE_LABEL"], BreakUpLargeNumbers(matData.have), 1, 1, 1, 0.53, 1, 0.53)
        local ttBag = row._cachedBag or 0
        local ttBank = row._cachedBank or 0
        local ttWarband = row._cachedWarband or 0
        if ttBag > 0 then GameTooltip:AddDoubleLine("  " .. L["TIP_BAGS_LABEL"], BreakUpLargeNumbers(ttBag), 0.7, 0.9, 0.7, 0.7, 0.9, 0.7) end
        if ttBank > 0 then GameTooltip:AddDoubleLine("  " .. L["TIP_BANK_LABEL"], BreakUpLargeNumbers(ttBank), 0.9, 0.8, 0.5, 0.9, 0.8, 0.5) end
        if ttWarband > 0 then GameTooltip:AddDoubleLine("  " .. L["TIP_WARBAND_LABEL"], BreakUpLargeNumbers(ttWarband), 0.7, 0.85, 1, 0.7, 0.85, 1) end
        GameTooltip:AddLine(string.format("Used in %d recipe%s", matData.recipeCount, matData.recipeCount == 1 and "" or "s"), 0.7, 0.7, 0.7)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(L["TIP_CLICK_SEE_RECIPES"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    row.clickButton:SetScript("OnLeave", function() GameTooltip:Hide() end)

    return yOffset - (ROW_HEIGHT + 1)
end

-- ============================================================================
-- MY MATERIALS: Recipe Pop-out Panel
-- ============================================================================

local function ReleasePopoutRows()
    for _, row in ipairs(popoutRows) do
        row:Hide()
        row:ClearAllPoints()
    end
end

local function GetPopoutRow(parent, height)
    for _, row in ipairs(popoutRows) do
        if not row:IsShown() then
            row:SetParent(parent)
            row:SetHeight(height)
            row:Show()
            return row
        end
    end
    local row = CreateFrame("Button", nil, parent, "BackdropTemplate")
    row:SetHeight(height)
    row:RegisterForClicks("LeftButtonUp")
    row:Show()
    table.insert(popoutRows, row)
    return row
end

local function CreatePopout()
    if popout then return end

    popout = CreateFrame("Frame", "HDG_MatPopout", UIParent, "BackdropTemplate")
    popout:SetWidth(280)
    popout:SetHeight(200)
    popout:SetFrameStrata("DIALOG")
    popout:SetFrameLevel(100)
    popout:SetClampedToScreen(true)
    popout:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 2,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    popout:SetBackdropColor(0.06, 0.06, 0.08, 0.97) -- slightly darker than PANEL_DARK, intentional
    popout:SetBackdropBorderColor(unpack(CONFIG.COLORS.PANEL_DARK_BORDER_WARM))
    popout:Hide()

    -- Header
    popout.header = popout:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    popout.header:SetPoint("TOPLEFT", 8, -8)
    popout.header:SetPoint("TOPRIGHT", -24, -8)
    popout.header:SetJustifyH("LEFT")
    popout.header:SetWordWrap(false)

    -- Close button
    local closeBtn = CreateFrame("Button", nil, popout)
    closeBtn:SetSize(14, 14)
    closeBtn:SetPoint("TOPRIGHT", -4, -6)
    local closeTex = closeBtn:CreateTexture(nil, "ARTWORK")
    closeTex:SetAllPoints()
    closeTex:SetAtlas("XMarksTheSpot")
    closeBtn:SetScript("OnClick", function()
        popout:Hide()
        activeReagentID = nil
        HDG_MaterialsList.Refresh()
    end)

    -- Scroll frame for recipe rows
    popoutScrollFrame = CreateFrame("ScrollFrame", nil, popout, "UIPanelScrollFrameTemplate")
    popoutScrollFrame:SetPoint("TOPLEFT", 4, -26)
    popoutScrollFrame:SetPoint("BOTTOMRIGHT", -20, 4)

    popoutContent = CreateFrame("Frame", nil, popoutScrollFrame)
    popoutContent:SetWidth(250)
    popoutContent:SetHeight(1)
    popoutScrollFrame:SetScrollChild(popoutContent)

    -- Slim scrollbar
    local sb = popoutScrollFrame.ScrollBar
    if sb then
        sb:SetWidth(6)
        if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0); sb.ScrollUpButton:SetSize(1, 1) end
        if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0); sb.ScrollDownButton:SetSize(1, 1) end
        local thumb = sb:GetThumbTexture()
        if thumb then
            thumb:SetAtlas("minimal-scrollbar-thumb-middle")
            thumb:SetWidth(6)
            thumb:SetAlpha(0.6)
        end
    end
end

function HDG_MaterialsList.ShowPopout(reagentItemID, reagentName, anchorRow)
    CreatePopout()
    ReleasePopoutRows()

    local scheme = GetScheme()

    -- Header
    popout.header:SetText("|cFFfcd34d" .. (reagentName or "Unknown") .. "|r")
    if scheme then ApplyFont(popout.header, scheme, "body") end

    -- Anchor to right of main frame, vertically aligned with the hovered row.
    -- BUG FIX: previously used select(5, anchorRow:GetPoint(1)) which returns the
    -- row's Y in its content-frame coords (e.g. -300 deep into a scroll). With
    -- widget.frame as the anchor target, that landed the popout below screen and
    -- SetClampedToScreen snapped it to bottom-right. Compute the row's top in
    -- widget.frame's local coords using actual screen positions.
    popout:ClearAllPoints()
    if widget and widget.frame then
        local rowY = 0
        if anchorRow and anchorRow:GetTop() and widget.frame:GetTop() then
            rowY = anchorRow:GetTop() - widget.frame:GetTop()
        end
        popout:SetPoint("TOPLEFT", widget.frame, "TOPRIGHT", 4, rowY)
    else
        popout:SetPoint("CENTER")
    end

    -- Get recipes
    local recipes = HDG_PowerCrafting.GetRecipesForReagent(reagentItemID)
    if not recipes or #recipes == 0 then
        popout.header:SetText("|cFFfcd34d" .. (reagentName or "Unknown") .. "|r\n|cFF888888No recipes found|r")
        popoutContent:SetHeight(1)
        popout:SetHeight(50)
        popout:Show()
        return
    end

    -- Calculate readiness for each recipe and sort
    local recipeData = {}
    for _, recipe in ipairs(recipes) do
        local readiness = HDG_PowerCrafting.GetCraftReadiness(recipe)
        table.insert(recipeData, {
            recipe = recipe,
            readiness = readiness,
            ratio = readiness.total > 0 and (readiness.owned / readiness.total) or 0,
        })
    end
    table.sort(recipeData, function(a, b)
        if a.readiness.ready ~= b.readiness.ready then return a.readiness.ready end
        if a.ratio ~= b.ratio then return a.ratio > b.ratio end
        return (a.recipe.name or "") < (b.recipe.name or "")
    end)

    -- Build rows
    local ROW_HEIGHT = 20
    local rowWidth = popoutContent:GetWidth()
    local yOffset = 0

    for _, entry in ipairs(recipeData) do
        local recipe = entry.recipe
        local readiness = entry.readiness

        local row = GetPopoutRow(popoutContent, ROW_HEIGHT)
        row:SetSize(rowWidth, ROW_HEIGHT)
        row:SetPoint("TOPLEFT", 0, yOffset)
        row:SetBackdrop(GetBackdropSolid())

        -- Green tint for 100% ready
        if readiness.ready then
            row:SetBackdropColor(0.1, 0.4, 0.15, 0.3)
        else
            row:SetBackdropColor(CONFIG.COLORS.PANEL_DARK[1], CONFIG.COLORS.PANEL_DARK[2], CONFIG.COLORS.PANEL_DARK[3], 0.6)
        end

        -- Recipe name (left)
        if not row.nameText then
            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetPoint("LEFT", 4, 0)
            row.nameText:SetJustifyH("LEFT")
            row.nameText:SetWordWrap(false)
        end
        row.nameText:SetWidth(rowWidth - 100)
        if scheme then ApplyFont(row.nameText, scheme, "small") end
        row.nameText:SetText(recipe.name or "Unknown")
        if scheme then row.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        else row.nameText:SetTextColor(unpack(CONFIG.COLORS.TEXT_BODY)) end

        -- Profession (dim, middle-right)
        if not row.profText then
            row.profText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            row.profText:SetJustifyH("RIGHT")
        end
        row.profText:ClearAllPoints()
        row.profText:SetPoint("RIGHT", -40, 0)
        if scheme then ApplyFont(row.profText, scheme, "small") end
        local profShort = recipe.profession and recipe.profession:sub(1, 5) or ""
        row.profText:SetText(profShort)
        if scheme then row.profText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
        else row.profText:SetTextColor(unpack(CONFIG.COLORS.TEXT_DIM)) end

        -- Craft readiness indicator (far right)
        if not row.readyText then
            row.readyText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.readyText:SetPoint("RIGHT", -4, 0)
        end
        if scheme then ApplyFont(row.readyText, scheme, "small") end
        local readyStr = string.format("%d/%d", readiness.owned, readiness.total)
        if readiness.ready then
            row.readyText:SetText("|cFF88FF88" .. readyStr .. "|r")
        elseif entry.ratio >= 0.5 then
            row.readyText:SetText("|cFFfcd34d" .. readyStr .. "|r")
        else
            row.readyText:SetText(readyStr)
            if scheme then row.readyText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
            else row.readyText:SetTextColor(unpack(CONFIG.COLORS.TEXT_INACTIVE)) end
        end

        -- Click: shift-click adds to queue
        local capturedRecipe = recipe
        row:SetScript("OnClick", function()
            if IsShiftKeyDown() and HDG_UI and HDG_UI.AddToCraftingQueue then
                HDG_UI.AddToCraftingQueue(capturedRecipe)
                -- Brief teal flash
                row:SetBackdropColor(CONFIG.COLORS.PANEL_DARK_BORDER_TEAL[1], CONFIG.COLORS.PANEL_DARK_BORDER_TEAL[2], CONFIG.COLORS.PANEL_DARK_BORDER_TEAL[3], 0.5)
                C_Timer.After(0.3, function()
                    if row:IsShown() then
                        if readiness.ready then
                            row:SetBackdropColor(0.1, 0.4, 0.15, 0.3)
                        else
                            row:SetBackdropColor(CONFIG.COLORS.PANEL_DARK[1], CONFIG.COLORS.PANEL_DARK[2], CONFIG.COLORS.PANEL_DARK[3], 0.6)
                        end
                    end
                end)
            end
        end)

        -- Tooltip: full reagent breakdown
        row:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetItemByID(capturedRecipe.itemID)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(L["TIP_REAGENTS_LABEL"], 1, 0.82, 0.2)
            if capturedRecipe.reagents then
                for _, reagent in ipairs(capturedRecipe.reagents) do
                    local rHave = HDG_Data and HDG_Data.GetItemCount(reagent.id) or 0
                    local rNeed = reagent.qty or 1
                    local color = rHave >= rNeed and "|cFF88FF88" or "|cFFFF5555"
                    GameTooltip:AddDoubleLine(
                        "  " .. (reagent.name or "?"),
                        color .. rHave .. "/" .. rNeed .. "|r",
                        0.8, 0.8, 0.8, 1, 1, 1)
                end
            end
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(L["TIP_SHIFTCLICK_ADD_QUEUE"], 0.7, 0.7, 0.7)
            GameTooltip:Show()
        end)
        row:SetScript("OnLeave", function() GameTooltip:Hide() end)

        yOffset = yOffset - (ROW_HEIGHT + 1)
    end

    local totalH = math.abs(yOffset) + 30
    popoutContent:SetHeight(math.max(totalH, 1))
    popout:SetHeight(math.min(math.max(totalH + 4, 50), 350))
    popout:Show()
end

-- ============================================================================
-- REFRESH
-- ============================================================================

function HDG_MaterialsList.Refresh()
    if not widget or not scrollContent or not widget.frame:IsShown() then return end

    local scheme = GetScheme()
    ReleaseAllRows()
    ReleaseTrackedRows()
    if trackedSeparator then trackedSeparator:Hide() end
    if trackedHeaderText then trackedHeaderText:Hide() end

    local isMyMats = GetConfig("myMaterialsMode")

    -- Update search clear button visibility
    if searchClearBtn and searchBox then
        if searchBox:GetText() ~= "" then searchClearBtn:Show() else searchClearBtn:Hide() end
    end

    -- Adjust scroll frame anchor and toolbar visibility
    if isMyMats then
        if searchToolbar then searchToolbar:Show() end
        if scrollFrame and searchToolbar then
            scrollFrame:SetPoint("TOPLEFT", searchToolbar, "BOTTOMLEFT", 4, -4)
        end
    else
        if searchToolbar then searchToolbar:Hide() end
        if scrollFrame and toggleBar then
            scrollFrame:SetPoint("TOPLEFT", toggleBar, "BOTTOMLEFT", 4, -4)
        end
    end

    -- ========== MY MATERIALS MODE ==========
    if isMyMats then
        local allMats = GetMyMaterials()

        -- Filter by search query
        local query = searchBox and searchBox:GetText():lower() or ""
        local filtered = {}
        local totalCount = 0
        for itemID, data in pairs(allMats) do
            totalCount = totalCount + 1
            if query == "" or (data.name and data.name:lower():find(query, 1, true)) then
                table.insert(filtered, { id = itemID, data = data })
            end
        end

        -- Sort
        if currentSort == "count" then
            table.sort(filtered, function(a, b)
                if a.data.have ~= b.data.have then return a.data.have > b.data.have end
                return (a.data.name or "") < (b.data.name or "")
            end)
        elseif currentSort == "recipes" then
            table.sort(filtered, function(a, b)
                if a.data.recipeCount ~= b.data.recipeCount then return a.data.recipeCount > b.data.recipeCount end
                return (a.data.name or "") < (b.data.name or "")
            end)
        else -- "name"
            table.sort(filtered, function(a, b) return (a.data.name or "") < (b.data.name or "") end)
        end

        local rowWidth = scrollContent:GetWidth()
        local yOffset = 0
        for _, entry in ipairs(filtered) do
            yOffset = CreateMyMaterialsRow(scrollContent, entry.id, entry.data, yOffset, rowWidth)
        end

        scrollContent:SetHeight(math.max(math.abs(yOffset) + 4, 1))

        -- Title summary
        if #filtered == 0 then
            titleSummary:SetText("|cFF888888No materials|r")
        elseif query ~= "" then
            titleSummary:SetText(string.format("%d of %d mats", #filtered, totalCount))
        else
            titleSummary:SetText(string.format("%d mats", #filtered))
        end
        costText:SetText("")
        return
    end

    -- ========== QUEUE MODE (existing logic) ==========
    if not HDG_LumberTracker or not HDG_LumberTracker.GetNonLumberMaterials then return end

    local directMats, rawMats = HDG_LumberTracker.GetNonLumberMaterials()
    local isRaw = GetConfig("showRawMaterials")
    local materials = isRaw and rawMats or directMats

    -- Sort: incomplete first, then alphabetical
    local sorted = {}
    for id, info in pairs(materials) do
        table.insert(sorted, { id = id, info = info })
    end
    table.sort(sorted, function(a, b)
        local aComplete = a.info.have >= a.info.needed
        local bComplete = b.info.have >= b.info.needed
        if aComplete ~= bComplete then return not aComplete end
        return (a.info.name or "") < (b.info.name or "")
    end)

    local rowWidth = scrollContent:GetWidth()
    local yOffset = 0
    local totalCost = 0
    local matCount = 0
    local incompleteCount = 0

    for _, entry in ipairs(sorted) do
        yOffset = CreateMaterialRow(scrollContent, entry.id, entry.info, yOffset, rowWidth)
        matCount = matCount + 1
        if entry.info.have < entry.info.needed then
            incompleteCount = incompleteCount + 1
            local price = Data().GetItemPrice and Data().GetItemPrice(entry.id)
            if price and price > 0 then
                totalCost = totalCost + price * (entry.info.needed - entry.info.have)
            end
        end
    end

    -- Tracked reagents section
    local trackedReagents = HDG_LumberTracker.GetAllTrackedReagents and HDG_LumberTracker.GetAllTrackedReagents() or {}
    if next(trackedReagents) then
        yOffset = yOffset - 6

        -- Reuse cached separator/header (create once, reposition each refresh)
        if not trackedSeparator then
            trackedSeparator = scrollContent:CreateTexture(nil, "ARTWORK")
        end
        if scheme then
            trackedSeparator:SetColorTexture(scheme.border.r, scheme.border.g, scheme.border.b, 0.6)
        else
            trackedSeparator:SetColorTexture(0.4, 0.4, 0.4, 0.6)
        end
        trackedSeparator:SetSize(rowWidth, 1)
        trackedSeparator:ClearAllPoints()
        trackedSeparator:SetPoint("TOPLEFT", 4, yOffset)
        trackedSeparator:Show()
        yOffset = yOffset - 6

        if not trackedHeaderText then
            trackedHeaderText = scrollContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        end
        trackedHeaderText:ClearAllPoints()
        trackedHeaderText:SetPoint("TOPLEFT", 4, yOffset)
        if scheme then ApplyFont(trackedHeaderText, scheme, "small") end
        trackedHeaderText:SetText("|cFF14b8a6" .. (L["HEADER_TRACKED_REAGENTS"] or "Tracked Reagents") .. "|r")
        trackedHeaderText:Show()
        yOffset = yOffset - 14

        local sortedTracked = {}
        for reagentId, trackInfo in pairs(trackedReagents) do
            local have = Data().GetItemCount and Data().GetItemCount(reagentId) or 0
            table.insert(sortedTracked, {
                id = reagentId,
                info = {
                    name = trackInfo.name,
                    have = have,
                    needed = trackInfo.targetQty or 0,
                    source = "Tracked",
                    isTracked = true
                }
            })
        end
        table.sort(sortedTracked, function(a, b) return (a.info.name or "") < (b.info.name or "") end)

        for _, tracked in ipairs(sortedTracked) do
            yOffset = CreateTrackedReagentRow(scrollContent, tracked.id, tracked.info, yOffset, rowWidth)
        end
    end

    scrollContent:SetHeight(math.max(math.abs(yOffset) + 4, 1))

    -- Title summary
    if matCount == 0 then
        titleSummary:SetText("|cFF888888No materials|r")
    else
        local summaryParts = {}
        table.insert(summaryParts, string.format("%d item%s", matCount, matCount == 1 and "" or "s"))
        if incompleteCount > 0 then
            table.insert(summaryParts, string.format("|cFFFF5555%d need|r", incompleteCount))
        end
        titleSummary:SetText(table.concat(summaryParts, "  "))
    end

    -- Cost in toggle bar
    if totalCost > 0 then
        local costStr = Data().FormatGold and Data().FormatGold(totalCost) or tostring(totalCost)
        costText:SetText("Est: " .. costStr)
    else
        costText:SetText("")
    end
end

-- ============================================================================
-- PUBLIC API
-- ============================================================================

function HDG_MaterialsList.Toggle()
    if not CONFIG then CONFIG = privateDB.CONFIG end
    if not created then CreatePanel() end
    if widget.frame:IsShown() then
        widget.frame:Hide()
    else
        widget.frame:Show()
        HDG_MaterialsList.Refresh()
    end
end

function HDG_MaterialsList.Show()
    if not CONFIG then CONFIG = privateDB.CONFIG end
    if not created then CreatePanel() end
    widget.frame:Show()
    HDG_MaterialsList.Refresh()
end

function HDG_MaterialsList.Hide()
    if popout then popout:Hide() end
    activeReagentID = nil
    if widget and widget.frame then widget.frame:Hide() end
end

function HDG_MaterialsList.IsShown()
    return widget and widget.frame and widget.frame:IsShown()
end

function HDG_MaterialsList:Init(configRef)
    CONFIG = configRef
end
