-- 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
local rows = {}
local trackedSeparator, trackedHeaderText  -- cached between refreshes
local CONFIG

local function GetScheme() return HDG_UIHelpers and HDG_UIHelpers.GetScheme() end
local function RegisterWidget(w, t) if HDG_UIHelpers then HDG_UIHelpers.RegisterWidget(w, t) end 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)
    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({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        row:SetBackdropColor(expColor[1], expColor[2], expColor[3], 0.15)
    else
        row:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        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:SetPoint("LEFT", 4, 0)
        row.nameText:SetJustifyH("LEFT")
        row.nameText:SetWordWrap(false)
    end
    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")
        row.countText:SetPoint("RIGHT", -4, 0)
    end
    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)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetItemByID(reagentId)
        GameTooltip:AddLine(" ")
        GameTooltip:AddDoubleLine("Source:", matInfo.source or "Unknown", 1, 1, 1, 0.7, 0.7, 0.7)
        GameTooltip:AddDoubleLine("Have:", BreakUpLargeNumbers(matInfo.have), 1, 1, 1, 1, 1, 1)
        GameTooltip:AddDoubleLine("Need:", BreakUpLargeNumbers(matInfo.needed), 1, 1, 1, 1, 1, 1)
        if matInfo.have >= matInfo.needed then
            GameTooltip:AddLine("|cFF88FF88Have enough!|r")
        else
            local shortfall = matInfo.needed - matInfo.have
            GameTooltip:AddLine(string.format("|cFFFF5555Need %s more|r", 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("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("Est. Cost:", cStr, 1, 1, 1, 1, 0.82, 0.2)
            end
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("|cFF00FF00Click|r to set AH search text", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("|cFF00FF00Shift-Click|r to search Auctionator", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("|cFF00FF00Right-Click|r to add to Auctionator list", 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({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    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(0.08, 0.72, 0.65)
    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(0.6, 0.6, 0.6, 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("Click to remove from tracked reagents", 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(0.6, 0.6, 0.6, 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)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetItemByID(reagentId)
        GameTooltip:AddLine(" ")
        GameTooltip:AddDoubleLine("Have:", BreakUpLargeNumbers(matInfo.have), 1, 1, 1, 1, 1, 1)
        if matInfo.needed > 0 then
            GameTooltip:AddDoubleLine("Target:", BreakUpLargeNumbers(matInfo.needed), 1, 1, 1, 1, 1, 1)
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("|cFF00FF00Click|r to set AH search text", 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)
            if HDG.Store then
                HDG.Store.state.ui.materialsListPosition = { point, relativePoint, x, y }
                HDG.Store:QueueSave()
            end
        end,
    })

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

    -- Restore saved position
    if HDG.Store then
        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
    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() frame:StartSizing("BOTTOM") end)
    grip:SetScript("OnMouseUp", function()
        frame:StopMovingOrSizing()
        if HDG.Store then
            HDG.Store.state.ui.materialsListHeight = math.floor(frame:GetHeight() + 0.5)
            HDG.Store:QueueSave()
        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(0.08, 0.08, 0.10, 0.97)
    frame:SetBackdropBorderColor(0.45, 0.35, 0.2, 0.9)

    -- 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("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.6 or 0.6, 1)
    end
    UpdatePinVisual()

    pinBtn:SetScript("OnClick", function()
        local isPinned = HDG.Store.state.ui.materialsListPinned
        local newPinned = not isPinned
        HDG.Store.state.ui.materialsListPinned = newPinned
        HDG.Store:QueueSave()
        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("Pin Window", 1, 1, 1)
        if isPinned then
            GameTooltip:AddLine("Pinned -- stays open on Escape and reload", 0.7, 0.7, 0.7)
        else
            GameTooltip:AddLine("Click to keep window open on Escape and reload", 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() frame:Hide() end)
    closeBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText("Close", 1, 1, 1)
        GameTooltip:Show()
    end)
    closeBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Direct/Raw toggle bar
    local 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 "Raw Materials" or "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")
        if HDG.Store then
            HDG.Store:Dispatch("SET_CONFIG", { key = "showRawMaterials", value = not isRaw })
        end
        UpdateToggleLabel()
        HDG_MaterialsList.Refresh()
    end)
    toggleBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local isRaw = GetConfig("showRawMaterials")
        if isRaw then
            GameTooltip:SetText("Showing Raw Materials", 1, 1, 1)
            GameTooltip:AddLine("Click to show direct (immediate) reagents", 0.7, 0.7, 0.7)
        else
            GameTooltip:SetText("Showing Direct Materials", 1, 1, 1)
            GameTooltip:AddLine("Click to show fully-expanded raw reagents", 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)

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

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

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

    local scheme = GetScheme()
    ReleaseAllRows()
    ReleaseTrackedRows()
    if trackedSeparator then trackedSeparator:Hide() end
    if trackedHeaderText then trackedHeaderText:Hide() 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 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
