-- ============================================================================
-- HDG_GoblinTab.lua - Profit Analysis Tab
-- ============================================================================
-- Shows all decor items ranked by profit potential with lumber value analysis.
-- Helps gold-makers identify the most profitable crafts.
-- ============================================================================

local addonName, privateDB = ...

HDG_GoblinTab = {}

-- ============================================================================
-- THEME HELPERS (delegating to ThemeEngine canonical helpers)
-- ============================================================================

local HDG = _G.HousingDecorGuide
local function GetScheme() return HDG.Theme.GetCurrentScheme() end
local function RegisterWidget(w, t) HDG.Theme.RegisterWidget(w, t) end
local ApplyFont = function(fs, s, ft) HDG.Theme.ApplyFont(fs, s, ft) end


-- Local references
local CONFIG
local L = HDG_L or {}  -- Localization
local mainFrame
local goblinFrame

-- State
local profitData = {}           -- Calculated profit data for all items
local filteredData = {}         -- After applying filters
local currentSort = "profit"    -- profit, margin, lumber, sell, name
local sortAscending = false     -- false = descending (highest first)
local professionFilter = "All"
local expansionFilter = "All"
local hideUnprofitable = false
local onlyKnownRecipes = false
local knownByAlts = false
local expandedRowKey = nil      -- Which row is currently expanded (recipeKey)
local searchFilter = ""         -- Search filter text
local scanState = "idle"        -- "idle" | "scanning" | "complete"
local scanAnimTicker = nil      -- Animated dots ticker during scan
local scanAnimStep = 0          -- 0-2 for dot animation
local freshnessTimer = nil      -- C_Timer ticker for updating freshness text

-- Row pool for efficient scrolling
local rowPool

-- UI elements
local scrollFrame, scrollContent
local scanButton, priceSourceText, itemCountText, exportButton
local expansionPanel            -- The expandable material breakdown panel
local activeDropdownMenu = nil  -- Track active dropdown menu for cleanup
local columnHeaders = {}        -- Track column header buttons for sort indicators

-- Forward declarations
local UpdateScanButton
local ShowExpansionPanel
local pendingItemRequests = {}
local expansionPanelItem = nil
local expansionPanelRow = nil

-- ============================================================================
-- INITIALIZATION
-- ============================================================================

function HDG_GoblinTab.Init(frame)
    mainFrame = frame
    CONFIG = privateDB.CONFIG

    -- Register for AH open/close events to auto-update scan button
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("AUCTION_HOUSE_SHOW")
    eventFrame:RegisterEvent("AUCTION_HOUSE_CLOSED")
    eventFrame:RegisterEvent("ITEM_DATA_LOAD_RESULT")
    eventFrame:SetScript("OnEvent", function(self, event, ...)
        if event == "ITEM_DATA_LOAD_RESULT" then
            -- Item data loaded - refresh expansion panel if we were waiting for this item
            local itemID, success = ...
            if success and pendingItemRequests[itemID] then
                pendingItemRequests[itemID] = nil
                -- Refresh the expansion panel if it's showing
                if expansionPanel and expansionPanel:IsShown() and expansionPanelItem and expansionPanelRow then
                    ShowExpansionPanel(expansionPanelRow, expansionPanelItem)
                end
            end
        else
            -- Update scan button state when AH opens or closes
            if goblinFrame and goblinFrame:IsShown() then
                UpdateScanButton()
            end
        end
    end)
end

-- ============================================================================
-- PROFIT CALCULATION
-- ============================================================================

-- Check if any alt (including current char) knows a recipe by spellID
local function AnyCharacterKnowsRecipe(spellID, profession)
    if not spellID or not HDG_Settings or not HDG_Settings.characters then return false end

    for _, charData in pairs(HDG_Settings.characters) do
        if charData.professions then
            -- Check the specific profession if provided, otherwise check all
            if profession and charData.professions[profession] then
                local profData = charData.professions[profession]
                if profData.knownRecipes and profData.knownRecipes[spellID] then
                    return true
                end
            elseif not profession then
                -- Check all professions
                for _, profData in pairs(charData.professions) do
                    if profData.knownRecipes and profData.knownRecipes[spellID] then
                        return true
                    end
                end
            end
        end
    end
    return false
end

-- Calculate profit data for all decor items
-- Get correct itemID, preferring ProfessionsDB (more accurate) over DecorDB
local function GetCorrectItemID(recipe)
    -- First try ProfessionsDB using spellID (most accurate source)
    if recipe.spellID and HDG_ProfessionsDB then
        local profData = HDG_ProfessionsDB[recipe.spellID]
        if profData and profData.itemID then
            return profData.itemID
        end
    end
    -- Fallback to DecorDB itemID
    return recipe.itemID
end

local function CalculateProfitData()
    profitData = {}

    if not HDG_DecorDB then return end

    for recipeKey, recipe in pairs(HDG_DecorDB) do
        local itemID = GetCorrectItemID(recipe)
        if itemID then
            local sellPrice, priceSource = HDG_PriceIntegration.GetItemPrice(itemID)

            -- Calculate material cost
            local materialCost = 0
            local lumberQty = 0
            local lumberCost = 0
            local lumberType = nil  -- Store which lumber type this recipe uses
            local hasAllPrices = true

            if recipe.reagents then
                for reagentID, reagentData in pairs(recipe.reagents) do
                    local qty = reagentData.qty or 1

                    -- Check if this is lumber (BoP, so it has 0 AH price)
                    if HDG_ExpansionData.IsLumber(reagentID) then
                        lumberQty = lumberQty + qty
                        lumberType = reagentID  -- Remember which lumber type
                        -- Lumber is BoP, so we don't add its AH cost
                    elseif HDG_ExpansionData.IsBoPReagent(reagentID) then
                        -- Other BoP reagents (e.g., Primal Spirit) - skip AH cost
                    else
                        local matPrice = HDG_PriceIntegration.GetItemPrice(reagentID)
                        if matPrice then
                            materialCost = materialCost + (matPrice * qty)
                        else
                            hasAllPrices = false
                        end
                    end
                end
            end

            -- Calculate profit metrics
            local profit = sellPrice and (sellPrice - materialCost) or nil
            local margin = (sellPrice and sellPrice > 0) and (profit / sellPrice * 100) or nil

            -- Lumber value: what you "earn" per lumber by crafting this item
            -- Formula: (sell_price - non_lumber_material_cost) / lumber_qty
            local lumberValue = nil
            if lumberQty > 0 and sellPrice then
                lumberValue = (sellPrice - materialCost) / lumberQty
            end

            -- Get normalized expansion name for filtering
            local expansion = HDG_ExpansionData.NormalizeToDisplayName(recipe.expansion)

            -- Check if player knows this recipe
            local isKnown = false
            if _G.HDG_Data and _G.HDG_Data.PlayerKnowsRecipeBySpellID then
                isKnown = _G.HDG_Data.PlayerKnowsRecipeBySpellID(recipe.spellID)
            end

            -- Check if any alt knows this recipe (uses stored knownRecipes data)
            local isKnownByAlts = AnyCharacterKnowsRecipe(recipe.spellID, recipe.profession)

            -- Get TSM region sale average (TSM only)
            local regionSaleAvg = HDG_PriceIntegration.GetRegionSaleAvg(itemID)

            -- Calculate TSM margin based on region avg instead of sell price
            local tsmMargin = nil
            if regionSaleAvg and regionSaleAvg > 0 and materialCost then
                tsmMargin = ((regionSaleAvg - materialCost) / regionSaleAvg) * 100
            end

            -- Get item quality for themed name coloring
            local quality = C_Item.GetItemQualityByID(itemID) or recipe.quality or 1

            table.insert(profitData, {
                recipeKey = recipeKey,
                recipe = recipe,
                itemID = itemID,
                name = HDG_Data.GetLocalizedName(itemID, recipe.name),
                profession = recipe.profession or "Unknown",
                expansion = expansion,
                quality = quality,
                sellPrice = sellPrice,
                materialCost = materialCost,
                profit = profit,
                margin = margin,
                lumberQty = lumberQty,
                lumberType = lumberType,
                lumberValue = lumberValue,
                hasAllPrices = hasAllPrices,
                isKnown = isKnown,
                isKnownByAlts = isKnownByAlts,
                priceSource = priceSource,
                regionSaleAvg = regionSaleAvg,
                tsmMargin = tsmMargin,
            })
        end
    end
end

-- Apply filters and sorting to profit data
local function ApplyFiltersAndSort()
    filteredData = {}

    for _, item in ipairs(profitData) do
        local include = true

        -- Filter by search text
        if searchFilter and searchFilter ~= "" then
            local searchLower = searchFilter:lower()
            if not string.find(item.name:lower(), searchLower, 1, true) then
                include = false
            end
        end

        -- Filter by profession
        if include and professionFilter ~= "All" and item.profession ~= professionFilter then
            include = false
        end

        -- Filter by expansion
        if include and expansionFilter ~= "All" and item.expansion ~= expansionFilter then
            include = false
        end

        -- Filter out unprofitable
        if include and hideUnprofitable and (not item.profit or item.profit <= 0) then
            include = false
        end

        -- Filter to only known recipes (current character)
        if include and onlyKnownRecipes and not item.isKnown then
            include = false
        end

        -- Filter to known by alts (any character including current)
        if include and knownByAlts and not item.isKnownByAlts then
            include = false
        end

        if include then
            table.insert(filteredData, item)
        end
    end

    -- Sort
    local sortKey = currentSort
    table.sort(filteredData, function(a, b)
        local aVal, bVal

        if sortKey == "profit" then
            aVal = a.profit or -999999999
            bVal = b.profit or -999999999
        elseif sortKey == "margin" then
            aVal = a.margin or -999999999
            bVal = b.margin or -999999999
        elseif sortKey == "lumber" then
            aVal = a.lumberValue or -999999999
            bVal = b.lumberValue or -999999999
        elseif sortKey == "lumberTotal" then
            aVal = (a.lumberValue or 0) * (a.lumberQty or 0)
            bVal = (b.lumberValue or 0) * (b.lumberQty or 0)
            if aVal == 0 then aVal = -999999999 end
            if bVal == 0 then bVal = -999999999 end
        elseif sortKey == "lumberType" then
            -- Sort by lumber type name
            local aInfo = a.lumberType and HDG_ExpansionData.GetLumberInfo(a.lumberType)
            local bInfo = b.lumberType and HDG_ExpansionData.GetLumberInfo(b.lumberType)
            aVal = aInfo and aInfo.name or "zzz"
            bVal = bInfo and bInfo.name or "zzz"
            if sortAscending then
                return aVal < bVal
            else
                return aVal > bVal
            end
        elseif sortKey == "sell" then
            aVal = a.sellPrice or 0
            bVal = b.sellPrice or 0
        elseif sortKey == "cost" then
            aVal = a.materialCost or 0
            bVal = b.materialCost or 0
        elseif sortKey == "name" then
            aVal = a.name or ""
            bVal = b.name or ""
            if sortAscending then
                return aVal < bVal
            else
                return aVal > bVal
            end
        elseif sortKey == "regionSaleAvg" then
            aVal = a.regionSaleAvg or -999999999
            bVal = b.regionSaleAvg or -999999999
        elseif sortKey == "tsmMargin" then
            aVal = a.tsmMargin or -999999999
            bVal = b.tsmMargin or -999999999
        end

        if sortAscending then
            return aVal < bVal
        else
            return aVal > bVal
        end
    end)
end

-- ============================================================================
-- CSV EXPORT
-- ============================================================================

-- Format copper value as gold string for CSV (no color codes)
local function FormatGoldForCSV(copper)
    if not copper then return "" end
    local gold = math.floor(copper / 10000)
    local silver = math.floor((copper % 10000) / 100)
    local copperVal = copper % 100
    return string.format("%d.%02d.%02d", gold, silver, copperVal)
end

-- Generate CSV content from filtered data
local function GenerateCSV()
    -- Header row with all columns
    local csv = "Name,ItemID,Profession,Expansion,Quality,LumberType,LumberQty,LumberValuePer,LumberValueTotal,MaterialCost,SellPrice,Profit,Margin%,RegionAvg,TSMMargin%,KnownByPlayer,KnownByAlts,PriceSource\n"

    for _, item in ipairs(filteredData) do
        -- Escape commas and quotes in name
        local name = item.name or "Unknown"
        name = name:gsub('"', '""')
        if name:find(",") then
            name = '"' .. name .. '"'
        end

        -- Get lumber type name
        local lumberTypeName = ""
        if item.lumberType then
            local lumberInfo = HDG_ExpansionData.GetLumberInfo(item.lumberType)
            lumberTypeName = lumberInfo and lumberInfo.name or ("Lumber " .. item.lumberType)
        end

        -- Calculate total lumber value
        local lumberTotal = ""
        if item.lumberValue and item.lumberQty then
            lumberTotal = FormatGoldForCSV(item.lumberValue * item.lumberQty)
        end

        -- Build CSV row
        local row = string.format("%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s",
            name,
            item.itemID or "",
            item.profession or "",
            item.expansion or "",
            item.quality or 1,
            lumberTypeName,
            item.lumberQty or "",
            item.lumberValue and FormatGoldForCSV(item.lumberValue) or "",
            lumberTotal,
            item.materialCost and FormatGoldForCSV(item.materialCost) or "",
            item.sellPrice and FormatGoldForCSV(item.sellPrice) or "",
            item.profit and FormatGoldForCSV(item.profit) or "",
            item.margin and string.format("%.1f", item.margin) or "",
            item.regionSaleAvg and FormatGoldForCSV(item.regionSaleAvg) or "",
            item.tsmMargin and string.format("%.1f", item.tsmMargin) or "",
            item.isKnown and "Yes" or "No",
            item.isKnownByAlts and "Yes" or "No",
            item.priceSource or ""
        )
        csv = csv .. row .. "\n"
    end

    return csv
end

-- Show export window with CSV content
local function ShowCSVExportWindow()
    if #filteredData == 0 then
        print("|cFFFFD100[HDG]|r No data to export. Adjust filters or scan prices first.")
        return
    end

    local csvText = GenerateCSV()
    local rowCount = #filteredData
    local scheme = GetScheme()

    -- Reuse or create window
    if not HDG.csvExportWindow then
        local window = CreateFrame("Frame", "HDG_CSVExportWindow", UIParent, "BackdropTemplate")
        window:SetSize(600, 450)
        window:SetPoint("CENTER")
        window:SetFrameStrata("DIALOG")
        window:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        window:SetMovable(true)
        window:EnableMouse(true)
        window:RegisterForDrag("LeftButton")
        window:SetScript("OnDragStart", window.StartMoving)
        window:SetScript("OnDragStop", window.StopMovingOrSizing)
        window:SetClampedToScreen(true)

        -- Title bar
        local titleBar = CreateFrame("Frame", nil, window)
        titleBar:SetHeight(24)
        titleBar:SetPoint("TOPLEFT", 0, 0)
        titleBar:SetPoint("TOPRIGHT", 0, 0)
        titleBar:EnableMouse(true)
        titleBar:RegisterForDrag("LeftButton")
        titleBar:SetScript("OnDragStart", function() window:StartMoving() end)
        titleBar:SetScript("OnDragStop", function() window:StopMovingOrSizing() end)

        local titleText = titleBar:CreateFontString(nil, "OVERLAY")
        titleText:SetPoint("LEFT", 10, 0)
        titleText:SetFont(STANDARD_TEXT_FONT, 12, "")
        window.titleText = titleText

        -- Close button
        local closeBtn = CreateFrame("Button", nil, titleBar)
        closeBtn:SetSize(16, 16)
        closeBtn:SetPoint("RIGHT", -4, 0)
        closeBtn:SetNormalTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up")
        closeBtn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight")
        closeBtn:SetScript("OnClick", function() window:Hide() end)

        -- Instructions
        local instructions = window:CreateFontString(nil, "OVERLAY")
        instructions:SetPoint("TOPLEFT", 10, -30)
        instructions:SetFont(STANDARD_TEXT_FONT, 10, "")
        instructions:SetText("Select all (Ctrl+A) and copy (Ctrl+C) to paste into Excel or a text file:")
        window.instructions = instructions

        -- Scroll frame for edit box
        local scrollFrame = CreateFrame("ScrollFrame", nil, window, "UIPanelScrollFrameTemplate")
        scrollFrame:SetPoint("TOPLEFT", 10, -50)
        scrollFrame:SetPoint("BOTTOMRIGHT", -30, 10)

        -- Edit box (copyable text)
        local editBox = CreateFrame("EditBox", nil, scrollFrame)
        editBox:SetMultiLine(true)
        editBox:SetAutoFocus(false)
        editBox:SetFontObject(ChatFontNormal)
        editBox:SetWidth(540)
        editBox:EnableMouse(true)
        editBox:SetScript("OnEscapePressed", function() window:Hide() end)
        scrollFrame:SetScrollChild(editBox)
        window.editBox = editBox

        HDG.csvExportWindow = window
        RegisterWidget(window, "Panel")
    end

    local window = HDG.csvExportWindow

    -- Apply theme colors
    if scheme then
        window:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.98)
        window:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 1)
        window.titleText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        window.instructions:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
    end

    window.titleText:SetText("Goblin Export (" .. rowCount .. " items)")
    window.editBox:SetText(csvText)
    window.editBox:HighlightText()
    window.editBox:SetFocus()

    window:Show()
end

-- ============================================================================
-- EXPANSION PANEL (Material Breakdown)
-- ============================================================================

-- Calculate direct and raw materials for a recipe
local function CalculateMaterialBreakdown(item)
    local directMaterials = {}
    local rawMaterials = {}
    local directTotal = 0
    local rawTotal = 0

    if not item or not item.recipe or not item.recipe.reagents then
        return directMaterials, rawMaterials, directTotal, rawTotal
    end

    -- Direct materials from the recipe
    for reagentID, reagentData in pairs(item.recipe.reagents) do
        local qty = reagentData.qty or 1
        local price = HDG_PriceIntegration.GetItemPrice(reagentID)
        local totalPrice = price and (price * qty) or 0
        local name = reagentData.name or ("Item " .. reagentID)

        -- Get name from ReagentsDB if available
        if HDG_ReagentsDB and HDG_ReagentsDB[reagentID] then
            name = HDG_ReagentsDB[reagentID].name or name
        end

        table.insert(directMaterials, {
            itemID = reagentID,
            name = name,
            qty = qty,
            unitPrice = price,
            totalPrice = totalPrice,
            isLumber = HDG_ExpansionData.IsLumber(reagentID),
        })

        if not HDG_ExpansionData.IsLumber(reagentID) then
            directTotal = directTotal + totalPrice
        end
    end

    -- Raw materials using PowerCrafting if available
    if HDG_PowerCrafting and HDG_PowerCrafting.BuildDependencyGraph then
        local graph = HDG_PowerCrafting.BuildDependencyGraph(item.recipe)
        if graph and graph.rawMaterials then
            for matID, matData in pairs(graph.rawMaterials) do
                local qty = matData.qty or 1
                local price = HDG_PriceIntegration.GetItemPrice(matID)
                local totalPrice = price and (price * qty) or 0
                local name = matData.name or ("Item " .. matID)

                table.insert(rawMaterials, {
                    itemID = matID,
                    name = name,
                    qty = qty,
                    unitPrice = price,
                    totalPrice = totalPrice,
                    isLumber = HDG_ExpansionData.IsLumber(matID),
                })

                if not HDG_ExpansionData.IsLumber(matID) then
                    rawTotal = rawTotal + totalPrice
                end
            end
        end
    end

    -- Sort by total price descending
    table.sort(directMaterials, function(a, b) return (a.totalPrice or 0) > (b.totalPrice or 0) end)
    table.sort(rawMaterials, function(a, b) return (a.totalPrice or 0) > (b.totalPrice or 0) end)

    return directMaterials, rawMaterials, directTotal, rawTotal
end

-- Request item info and schedule refresh if needed
local function RequestItemInfo(itemID)
    if not itemID then return end
    -- Check if we already have the name
    local name = C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(itemID)
    if not name then
        name = C_Item.GetItemInfo(itemID)
    end
    if name then return end  -- Already cached

    -- Request the item data
    if C_Item and C_Item.RequestLoadItemDataByID then
        pendingItemRequests[itemID] = true
        C_Item.RequestLoadItemDataByID(itemID)
    end
end

-- Create or update the expansion panel
ShowExpansionPanel = function(parentRow, item)
    if not expansionPanel then
        expansionPanel = CreateFrame("Frame", nil, scrollContent, "BackdropTemplate")
        expansionPanel:SetBackdrop({
            bgFile = CONFIG.TEXTURES.WHITE_BG,
            edgeFile = CONFIG.TEXTURES.WHITE_BG,
            edgeSize = 1,
        })
        local expPanelScheme = GetScheme()
        if expPanelScheme then
            expansionPanel:SetBackdropColor(expPanelScheme.panel.r, expPanelScheme.panel.g, expPanelScheme.panel.b, 0.95)
            expansionPanel:SetBackdropBorderColor(expPanelScheme.accent.r, expPanelScheme.accent.g, expPanelScheme.accent.b, 0.8)
            RegisterWidget(expansionPanel, "Panel")
        else
            expansionPanel:SetBackdropColor(0.08, 0.08, 0.10, 0.95)
            expansionPanel:SetBackdropBorderColor(0.2, 0.6, 0.5, 0.8)
        end

        -- Create text elements for materials
        expansionPanel.directTitle = expansionPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        expansionPanel.directTitle:SetPoint("TOPLEFT", 10, -8)
        ApplyFont(expansionPanel.directTitle, expPanelScheme, "small")
        expansionPanel.directTitle:SetText(HDG.Theme.C_Warning() .. "Direct Materials|r")

        expansionPanel.directText = expansionPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        expansionPanel.directText:SetPoint("TOPLEFT", 10, -24)
        expansionPanel.directText:SetWidth(260)
        expansionPanel.directText:SetJustifyH("LEFT")
        ApplyFont(expansionPanel.directText, expPanelScheme, "small")

        expansionPanel.rawTitle = expansionPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        expansionPanel.rawTitle:SetPoint("TOPLEFT", 290, -8)
        ApplyFont(expansionPanel.rawTitle, expPanelScheme, "small")
        expansionPanel.rawTitle:SetText(HDG.Theme.C_Warning() .. "Raw Materials|r")

        expansionPanel.rawText = expansionPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        expansionPanel.rawText:SetPoint("TOPLEFT", 290, -24)
        expansionPanel.rawText:SetWidth(260)
        expansionPanel.rawText:SetJustifyH("LEFT")
        ApplyFont(expansionPanel.rawText, expPanelScheme, "small")
    end

    -- Store current item and row for refresh
    expansionPanelItem = item
    expansionPanelRow = parentRow

    -- Calculate materials
    local directMats, rawMats, directTotal, rawTotal = CalculateMaterialBreakdown(item)

    -- Request item info for any uncached items
    pendingItemRequests = {}
    for _, mat in ipairs(directMats) do
        RequestItemInfo(mat.itemID)
    end
    for _, mat in ipairs(rawMats) do
        RequestItemInfo(mat.itemID)
    end

    -- Format direct materials
    local directLines = {}
    for _, mat in ipairs(directMats) do
        local priceStr = mat.unitPrice and HDG_PriceIntegration.FormatGoldOnly(mat.totalPrice) or "?"
        local color = mat.isLumber and HDG.Theme.C_Warning() or HDG.Theme.C_Text()
        table.insert(directLines, string.format("%s%dx %s|r  %s", color, mat.qty, mat.name, priceStr))
    end
    table.insert(directLines, HDG.Theme.C_Dim() .. "Total:|r " .. HDG.Theme.C_Success() .. HDG_PriceIntegration.FormatGoldOnly(directTotal) .. "|r")
    expansionPanel.directText:SetText(table.concat(directLines, "\n"))

    -- Format raw materials
    local rawLines = {}
    for _, mat in ipairs(rawMats) do
        local priceStr = mat.unitPrice and HDG_PriceIntegration.FormatGoldOnly(mat.totalPrice) or "?"
        local color = mat.isLumber and HDG.Theme.C_Warning() or HDG.Theme.C_Text()
        table.insert(rawLines, string.format("%s%dx %s|r  %s", color, mat.qty, mat.name, priceStr))
    end
    table.insert(rawLines, HDG.Theme.C_Dim() .. "Total:|r " .. HDG.Theme.C_Success() .. HDG_PriceIntegration.FormatGoldOnly(rawTotal) .. "|r")
    expansionPanel.rawText:SetText(table.concat(rawLines, "\n"))

    -- Calculate height based on content
    local numLines = math.max(#directMats, #rawMats) + 2  -- +2 for title and total
    local panelHeight = 16 + (numLines * 14)
    panelHeight = math.max(panelHeight, 60)
    panelHeight = math.min(panelHeight, 200)

    expansionPanel:SetHeight(panelHeight)
    expansionPanel:SetPoint("TOPLEFT", parentRow, "BOTTOMLEFT", 0, -2)
    expansionPanel:SetPoint("RIGHT", parentRow, "RIGHT", 0, 0)
    expansionPanel:Show()

    return panelHeight + 4  -- Return height for offset calculation
end

local function HideExpansionPanel()
    expandedRowKey = nil  -- Clear key first
    expansionPanelItem = nil
    expansionPanelRow = nil
    pendingItemRequests = {}
    if expansionPanel then
        expansionPanel:Hide()
        expansionPanel:ClearAllPoints()
    end
end

-- ============================================================================
-- AUCTIONATOR SCAN INTEGRATION
-- ============================================================================

-- Check if we can trigger an Auctionator scan
function HDG_GoblinTab.CanTriggerScan()
    if not Auctionator or not Auctionator.State then return false, "No Auctionator" end
    if not AuctionHouseFrame or not AuctionHouseFrame:IsShown() then return false, "AH Closed" end
    if not Auctionator.State.FullScanFrameRef then return false, "Not Ready" end

    if Auctionator.State.FullScanFrameRef.CanInitiate then
        if Auctionator.State.FullScanFrameRef:CanInitiate() then
            return true, "Ready"
        else
            -- On cooldown - try to get time remaining
            if Auctionator.State.FullScanFrameRef.NextScanMessage then
                return false, Auctionator.State.FullScanFrameRef:NextScanMessage()
            end
            return false, "Cooldown"
        end
    end

    return false, "Unknown"
end

-- Stop scan animation ticker
local function StopScanAnim()
    if scanAnimTicker then
        scanAnimTicker:Cancel()
        scanAnimTicker = nil
    end
    scanAnimStep = 0
end

-- Start scan animation ticker (cycling dots every 0.5s)
local function StartScanAnim()
    StopScanAnim()
    local dots = { ".", "..", "..." }
    scanAnimTicker = C_Timer.NewTicker(0.5, function()
        scanAnimStep = (scanAnimStep % 3) + 1
        if priceSourceText and scanState == "scanning" then
            priceSourceText:SetText(HDG.Theme.C_Accent() .. "Scanning" .. dots[scanAnimStep] .. "|r")
        end
    end)
end

-- Trigger a full AH scan
function HDG_GoblinTab.TriggerScan()
    local canScan, reason = HDG_GoblinTab.CanTriggerScan()
    if canScan then
        scanState = "scanning"
        Auctionator.State.FullScanFrameRef:InitiateScan()
        StartScanAnim()
        UpdateScanButton()
    else
        print(HDG.Theme.C_Success() .. "Housing Decor Guide:|r Cannot scan: " .. (reason or "Unknown"))
    end
end

-- Register for scan completion callback
local function RegisterScanCallback()
    if Auctionator and Auctionator.API and Auctionator.API.v1 and Auctionator.API.v1.RegisterForDBUpdate then
        Auctionator.API.v1.RegisterForDBUpdate("HousingDecorGuide", function()
            StopScanAnim()
            scanState = "complete"
            UpdateScanButton()
            C_Timer.After(3, function()
                scanState = "idle"
                UpdateScanButton()
            end)
            if goblinFrame and goblinFrame:IsShown() then
                HDG_GoblinTab.Refresh()
            end
        end)
    end
end

-- Get time since last Auctionator scan
local function GetAuctionatorScanTime()
    if not Auctionator or not Auctionator.SavedState then
        return nil
    end

    local lastScan = Auctionator.SavedState.TimeOfLastReplicateScan
    if not lastScan then
        return nil
    end

    return time() - lastScan
end

-- Get time since last TSM scan (if available)
local function GetTSMScanTime()
    -- TSM doesn't expose scan time the same way, but we can check if data exists
    -- For now, return nil to indicate "unknown" - TSM syncs data differently
    return nil
end

-- Get time since last scan for the active price addon
local function GetTimeSinceLastScan()
    local preferred = HDG_Settings and HDG_Settings.preferredPriceAddon

    if preferred == "TSM" then
        return GetTSMScanTime()
    elseif preferred == "Auctionator" then
        return GetAuctionatorScanTime()
    end

    -- Default: check based on which addon is available (TSM first)
    if HDG_PriceIntegration.IsTSMAvailable() then
        return GetTSMScanTime()
    end
    return GetAuctionatorScanTime()
end

-- Get the currently active price addon name (respecting user preference)
local function GetActivePriceAddonName()
    local preferred = HDG_Settings and HDG_Settings.preferredPriceAddon

    if preferred == "TSM" and HDG_PriceIntegration.IsTSMAvailable() then
        return "TSM"
    elseif preferred == "Auctionator" and HDG_PriceIntegration.IsAuctionatorAvailable() then
        return "Auctionator"
    end

    -- Default: TSM first if available
    if HDG_PriceIntegration.IsTSMAvailable() then
        return "TSM"
    elseif HDG_PriceIntegration.IsAuctionatorAvailable() then
        return "Auctionator"
    end

    return nil
end

-- Format seconds into human-readable string
local function FormatTimeSince(seconds)
    if not seconds then return "Never" end

    if seconds < 60 then
        return "< 1m ago"
    elseif seconds < 3600 then
        return string.format("%dm ago", math.floor(seconds / 60))
    elseif seconds < 86400 then
        local hours = math.floor(seconds / 3600)
        return string.format("%dh ago", hours)
    else
        local days = math.floor(seconds / 86400)
        return string.format("%dd ago", days)
    end
end

-- Update scan button + price freshness text (state machine)
UpdateScanButton = function()
    if not scanButton or not priceSourceText then return end

    local addonName = GetActivePriceAddonName()
    local scheme = GetScheme()

    -- No price addon at all
    if not addonName then
        scanButton:Hide()
        priceSourceText:SetText(HDG.Theme.C_Error() .. "No price addon|r")
        return
    end

    -- Scanning in progress
    if scanState == "scanning" then
        scanButton:Hide()
        priceSourceText:SetText(HDG.Theme.C_Accent() .. "Scanning...|r")
        return
    end

    -- Just completed (3s flash)
    if scanState == "complete" then
        scanButton:Hide()
        priceSourceText:SetText(HDG.Theme.C_Success() .. "Updated!|r")
        return
    end

    -- TSM or no Auctionator: show name, no scan button
    if addonName == "TSM" or not Auctionator or not Auctionator.State then
        scanButton:Hide()
        priceSourceText:SetText(HDG.Theme.C_Accent() .. addonName .. "|r")
        return
    end

    -- Auctionator idle: show freshness text + scan button
    local timeSince = GetTimeSinceLastScan()
    if not timeSince then
        priceSourceText:SetText(HDG.Theme.C_Error() .. "No AH data|r")
    else
        local timeStr = FormatTimeSince(timeSince)
        local timeColor
        if timeSince < 3600 then
            timeColor = HDG.Theme.C_Success()
        elseif timeSince < 86400 then
            timeColor = HDG.Theme.C_Warning()
        else
            timeColor = HDG.Theme.C_Error()
        end
        priceSourceText:SetText(timeColor .. addonName .. ": " .. timeStr .. "|r")
    end

    -- Scan button state
    scanButton:Show()
    local canScan, reason = HDG_GoblinTab.CanTriggerScan()
    if canScan then
        scanButton.text:SetText(HDG.Theme.C_Text() .. "Scan|r")
        scanButton:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.9)
    elseif reason == "AH Closed" then
        scanButton.text:SetText(HDG.Theme.C_Dim() .. "Open AH|r")
        scanButton:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.9)
    else
        scanButton.text:SetText(HDG.Theme.C_Dim() .. "Cooldown|r")
        scanButton:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.9)
    end
end

-- Update TSM-only column visibility (Region Avg and TSM Margin headers)
local function UpdateTSMColumnVisibility()
    local isTSMActive = GetActivePriceAddonName() == "TSM"

    -- Show/hide the Region Avg column header (TSM only)
    if columnHeaders["regionSaleAvg"] then
        if isTSMActive then
            columnHeaders["regionSaleAvg"]:Show()
        else
            columnHeaders["regionSaleAvg"]:Hide()
        end
    end

    -- Show/hide the TSM Margin column header (TSM only)
    if columnHeaders["tsmMargin"] then
        if isTSMActive then
            columnHeaders["tsmMargin"]:Show()
        else
            columnHeaders["tsmMargin"]:Hide()
        end
    end
end

-- ============================================================================
-- UI CREATION
-- ============================================================================

-- Create a row from the pool or new
local function CreateRowFactory(scrollContent)
    return HDG_UIHelpers.CreateRowPool(function(parent)
        local rowScheme = GetScheme()
        local row = CreateFrame("Button", nil, parent, "BackdropTemplate")
        row:SetHeight(24)
        row:SetBackdrop({
            bgFile = CONFIG.TEXTURES.WHITE_BG,
        })

        -- Item name (left-aligned)
        row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.nameText:SetPoint("LEFT", 8, 0)
        row.nameText:SetWidth(180)
        row.nameText:SetJustifyH("LEFT")
        row.nameText:SetWordWrap(false)
        ApplyFont(row.nameText, rowScheme, "small")

        -- Lumber type (left-aligned, expansion-colored)
        row.lumberTypeText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.lumberTypeText:SetPoint("LEFT", 192, 0)
        row.lumberTypeText:SetWidth(75)
        row.lumberTypeText:SetJustifyH("LEFT")
        ApplyFont(row.lumberTypeText, rowScheme, "small")

        -- Per-lumber value ($/Lum)
        row.lumberText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.lumberText:SetPoint("LEFT", 271, 0)
        row.lumberText:SetWidth(65)
        row.lumberText:SetJustifyH("RIGHT")
        ApplyFont(row.lumberText, rowScheme, "small")

        -- Total lumber value (LumTot)
        row.lumberTotalText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.lumberTotalText:SetPoint("LEFT", 340, 0)
        row.lumberTotalText:SetWidth(65)
        row.lumberTotalText:SetJustifyH("RIGHT")
        ApplyFont(row.lumberTotalText, rowScheme, "small")

        -- Cost
        row.costText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.costText:SetPoint("LEFT", 409, 0)
        row.costText:SetWidth(65)
        row.costText:SetJustifyH("RIGHT")
        ApplyFont(row.costText, rowScheme, "small")

        -- Sell price
        row.sellText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.sellText:SetPoint("LEFT", 478, 0)
        row.sellText:SetWidth(65)
        row.sellText:SetJustifyH("RIGHT")
        ApplyFont(row.sellText, rowScheme, "small")

        -- Profit
        row.profitText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.profitText:SetPoint("LEFT", 547, 0)
        row.profitText:SetWidth(70)
        row.profitText:SetJustifyH("RIGHT")
        ApplyFont(row.profitText, rowScheme, "small")

        -- Margin
        row.marginText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.marginText:SetPoint("LEFT", 621, 0)
        row.marginText:SetWidth(55)
        row.marginText:SetJustifyH("RIGHT")
        ApplyFont(row.marginText, rowScheme, "small")

        -- Region Avg (TSM only - visibility controlled by UpdateTSMColumnVisibility)
        row.regionAvgText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.regionAvgText:SetPoint("LEFT", 680, 0)
        row.regionAvgText:SetWidth(90)
        row.regionAvgText:SetJustifyH("RIGHT")
        ApplyFont(row.regionAvgText, rowScheme, "small")

        -- TSM Margin (TSM only - visibility controlled by UpdateTSMColumnVisibility)
        row.tsmMarginText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.tsmMarginText:SetPoint("LEFT", 774, 0)
        row.tsmMarginText:SetWidth(55)
        ApplyFont(row.tsmMarginText, rowScheme, "small")
        row.tsmMarginText:SetJustifyH("RIGHT")

        -- Click handler for expand/collapse, add to queue, AH search
        row:SetScript("OnClick", function(self)
            if IsShiftKeyDown() then
                -- Shift-click: Search AH for this item
                if AuctionHouseFrame and AuctionHouseFrame:IsShown() then
                    local searchBox = AuctionHouseFrame.SearchBar and AuctionHouseFrame.SearchBar.SearchBox
                    if searchBox then
                        local itemName = self.item and self.item.name or ""
                        searchBox:SetText(itemName)
                        -- Trigger the search
                        if AuctionHouseFrame.SearchBar.SearchButton then
                            AuctionHouseFrame.SearchBar.SearchButton:Click()
                        end
                    end
                else
                    print(HDG.Theme.C_Success() .. "Housing Decor Guide:|r Open the Auction House to search")
                end
            elseif IsControlKeyDown() then
                -- Ctrl-click: Add to crafting queue
                if self.item and HDG_UI and HDG_UI.AddToShoppingList then
                    local item = self.item
                    local recipe = {
                        id = item.itemID,
                        name = item.name,
                        spell = item.recipe.spellID,
                        expansion = item.expansion,
                        profession = item.profession,
                        reagents = {},
                    }
                    if item.recipe.reagents then
                        for reagentID, reagentData in pairs(item.recipe.reagents) do
                            table.insert(recipe.reagents, {
                                id = reagentID,
                                amount = reagentData.qty,
                            })
                        end
                    end
                    HDG_UI.AddToShoppingList(recipe)
                    print(HDG.Theme.C_Success() .. "Housing Decor Guide:|r Added " .. item.name .. " to crafting queue")
                end
            else
                -- Normal click: Toggle material breakdown expansion
                if self.item then
                    if expandedRowKey == self.item.recipeKey then
                        -- Collapse if clicking the same row
                        HideExpansionPanel()
                        HDG_GoblinTab.Refresh()  -- Re-render to remove expansion space
                    else
                        -- Expand this row
                        expandedRowKey = self.item.recipeKey
                        HDG_GoblinTab.Refresh()  -- Re-render to show expansion
                    end
                end
            end
        end)

        -- Hover effects
        row:SetScript("OnEnter", function(self)
            local hoverScheme = GetScheme()
            if hoverScheme then
                self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, 0.8)
            else
                self:SetBackdropColor(0.25, 0.25, 0.27, 0.8)
            end
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if self.itemID then
                GameTooltip:SetItemByID(self.itemID)
            end
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Click|r to expand material breakdown", 0.7, 0.7, 0.7)
            GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Ctrl-Click|r to add to crafting queue", 0.7, 0.7, 0.7)
            GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Shift-Click|r to search Auction House", 0.7, 0.7, 0.7)
            GameTooltip:Show()
        end)
        row:SetScript("OnLeave", function(self)
            local leaveScheme = GetScheme()
            if self.bgColor then
                self:SetBackdropColor(unpack(self.bgColor))
            elseif leaveScheme then
                self:SetBackdropColor(leaveScheme.panel.r, leaveScheme.panel.g, leaveScheme.panel.b, 0.5)
            else
                self:SetBackdropColor(0.12, 0.12, 0.14, 0.5)
            end
            GameTooltip:Hide()
        end)

        return row
    end)
end

-- Close any active dropdown menu
local function CloseActiveDropdown()
    if activeDropdownMenu and activeDropdownMenu:IsShown() then
        activeDropdownMenu:Hide()
    end
    activeDropdownMenu = nil
end

-- Create filter dropdown
local function CreateDropdown(parent, label, items, width, onChange)
    local container = CreateFrame("Frame", nil, parent)
    container:SetSize(width + 50, 22)

    local ddScheme = GetScheme()
    local labelText = container:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    labelText:SetPoint("LEFT", 0, 0)
    ApplyFont(labelText, ddScheme, "small")
    labelText:SetText(label)

    local dropdown = CreateFrame("Button", nil, container, "BackdropTemplate")
    dropdown:SetSize(width, 20)
    dropdown:SetPoint("LEFT", labelText, "RIGHT", 4, 0)
    dropdown:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
    })
    if ddScheme then
        dropdown:SetBackdropColor(ddScheme.panel.r, ddScheme.panel.g, ddScheme.panel.b, 0.9)
        dropdown:SetBackdropBorderColor(ddScheme.border.r, ddScheme.border.g, ddScheme.border.b, 1)
    else
        dropdown:SetBackdropColor(0.15, 0.15, 0.16, 0.9)
        dropdown:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
    end
    RegisterWidget(dropdown, "Panel")

    dropdown.text = dropdown:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    dropdown.text:SetPoint("LEFT", 6, 0)
    ApplyFont(dropdown.text, ddScheme, "small")
    dropdown.text:SetText(items[1] or "All")

    dropdown.arrow = dropdown:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    dropdown.arrow:SetPoint("RIGHT", -6, 0)
    ApplyFont(dropdown.arrow, ddScheme, "small")
    dropdown.arrow:SetText(HDG.Theme.C_Dim() .. "v|r")

    dropdown.items = items
    dropdown.selected = items[1] or "All"
    dropdown.onChange = onChange

    dropdown:SetScript("OnClick", function(self)
        -- Close any existing dropdown first
        CloseActiveDropdown()

        -- Create dropdown menu
        local menu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
        menu:SetFrameStrata("FULLSCREEN_DIALOG")  -- Above dialogs, not conflicting with GameTooltip
        menu:SetBackdrop({
            bgFile = CONFIG.TEXTURES.WHITE_BG,
            edgeFile = CONFIG.TEXTURES.WHITE_BG,
            edgeSize = 1,
        })
        local menuScheme = GetScheme()
        if menuScheme then
            menu:SetBackdropColor(menuScheme.bg.r, menuScheme.bg.g, menuScheme.bg.b, 0.98)
            menu:SetBackdropBorderColor(menuScheme.border.r, menuScheme.border.g, menuScheme.border.b, 1)
        else
            menu:SetBackdropColor(0.12, 0.12, 0.14, 0.98)
            menu:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
        end

        local menuHeight = #self.items * 20 + 4
        menu:SetSize(width, menuHeight)
        menu:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)

        -- Track this as the active dropdown
        activeDropdownMenu = menu

        for i, item in ipairs(self.items) do
            local btn = CreateFrame("Button", nil, menu)
            btn:SetSize(width - 4, 18)
            btn:SetPoint("TOPLEFT", 2, -2 - (i-1) * 20)

            btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            btn.text:SetPoint("LEFT", 4, 0)
            ApplyFont(btn.text, menuScheme, "small")
            btn.text:SetText(item)

            btn:SetScript("OnEnter", function(b)
                local btnScheme = GetScheme()
                if btnScheme then
                    b.text:SetTextColor(btnScheme.warning.r, btnScheme.warning.g, btnScheme.warning.b)
                else
                    b.text:SetTextColor(1, 0.82, 0.2)
                end
            end)
            btn:SetScript("OnLeave", function(b)
                local btnScheme = GetScheme()
                if btnScheme then
                    b.text:SetTextColor(btnScheme.text.r, btnScheme.text.g, btnScheme.text.b)
                else
                    b.text:SetTextColor(1, 1, 1)
                end
            end)
            btn:SetScript("OnClick", function()
                self.selected = item
                self.text:SetText(item)
                menu:Hide()
                activeDropdownMenu = nil
                if self.onChange then
                    self.onChange(item)
                end
            end)
        end

        -- Close menu when clicking elsewhere
        menu:SetScript("OnShow", function()
            menu:SetScript("OnUpdate", function()
                if not MouseIsOver(menu) and not MouseIsOver(self) then
                    if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") then
                        menu:Hide()
                        activeDropdownMenu = nil
                    end
                end
            end)
        end)

        menu:SetScript("OnHide", function()
            if activeDropdownMenu == menu then
                activeDropdownMenu = nil
            end
        end)
    end)

    return container, dropdown
end

-- Create checkbox
local function CreateCheckbox(parent, label, initialValue, onChange)
    local container = CreateFrame("Button", nil, parent)
    container:SetSize(120, 18)

    local box = container:CreateTexture(nil, "ARTWORK")
    box:SetSize(14, 14)
    box:SetPoint("LEFT", 0, 0)
    box:SetTexture(CONFIG.TEXTURES.WHITE_BG)
    local cbScheme = GetScheme()
    if cbScheme then
        box:SetVertexColor(cbScheme.panel.r, cbScheme.panel.g, cbScheme.panel.b, 1)
    else
        box:SetVertexColor(0.2, 0.2, 0.22, 1)
    end
    container.box = box

    -- Border around checkbox box
    local borderSize = 1
    local border = {}
    for _, side in ipairs({"TOP", "BOTTOM", "LEFT", "RIGHT"}) do
        local tex = container:CreateTexture(nil, "BORDER")
        tex:SetTexture(CONFIG.TEXTURES.WHITE_BG)
        if cbScheme then
            tex:SetVertexColor(cbScheme.border.r, cbScheme.border.g, cbScheme.border.b, 1)
        else
            tex:SetVertexColor(0.4, 0.4, 0.4, 1)
        end
        border[side] = tex
    end
    border.TOP:SetPoint("TOPLEFT", box, "TOPLEFT", -borderSize, borderSize)
    border.TOP:SetPoint("TOPRIGHT", box, "TOPRIGHT", borderSize, borderSize)
    border.TOP:SetHeight(borderSize)
    border.BOTTOM:SetPoint("BOTTOMLEFT", box, "BOTTOMLEFT", -borderSize, -borderSize)
    border.BOTTOM:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", borderSize, -borderSize)
    border.BOTTOM:SetHeight(borderSize)
    border.LEFT:SetPoint("TOPLEFT", box, "TOPLEFT", -borderSize, 0)
    border.LEFT:SetPoint("BOTTOMLEFT", box, "BOTTOMLEFT", -borderSize, 0)
    border.LEFT:SetWidth(borderSize)
    border.RIGHT:SetPoint("TOPRIGHT", box, "TOPRIGHT", borderSize, 0)
    border.RIGHT:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", borderSize, 0)
    border.RIGHT:SetWidth(borderSize)
    container.boxBorder = border

    local check = container:CreateTexture(nil, "OVERLAY")
    check:SetSize(12, 12)
    check:SetPoint("CENTER", box, "CENTER", 0, 0)
    check:SetTexture(CONFIG.TEXTURES.CHECKMARK)
    if cbScheme then
        check:SetVertexColor(cbScheme.success.r, cbScheme.success.g, cbScheme.success.b, 1)
    else
        check:SetVertexColor(0.2, 0.8, 0.5, 1)
    end
    container.check = check

    local text = container:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    text:SetPoint("LEFT", box, "RIGHT", 4, 0)
    ApplyFont(text, cbScheme, "small")
    text:SetText(label)
    container.text = text  -- Store for theme engine

    container.checked = initialValue
    check:SetShown(initialValue)

    function container:SetChecked(value)
        self.checked = value
        self.check:SetShown(value)
    end

    container:SetScript("OnClick", function(self)
        self.checked = not self.checked
        self.check:SetShown(self.checked)
        if onChange then
            onChange(self.checked)
        end
    end)

    -- Register with theme engine
    RegisterWidget(container, "Checkbox")

    return container
end

-- Update sort indicator (text color) on column headers
local function UpdateSortIndicators()
    local sortScheme = GetScheme()
    for _, header in pairs(columnHeaders) do
        if header.sortKey == currentSort then
            -- Active sort column: white/text color
            if sortScheme then
                header.text:SetTextColor(sortScheme.text.r, sortScheme.text.g, sortScheme.text.b)
            else
                header.text:SetTextColor(1, 1, 1)
            end
        else
            -- Inactive columns: neutral column header color (Base01 for Solarized)
            header.text:SetTextColor(HDG.Theme.GetColumnHeaderColor())
        end
    end
end

-- Create column header button
local function CreateColumnHeader(parent, text, sortKey, x, width, isLeftAligned)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(width, 20)
    btn:SetPoint("LEFT", x, 0)

    local headerScheme = GetScheme()
    btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    ApplyFont(btn.text, headerScheme, "small")
    btn.text:SetText(text)
    -- Use neutral column header color (Base01 for Solarized)
    btn.text:SetTextColor(HDG.Theme.GetColumnHeaderColor())

    if isLeftAligned then
        btn.text:SetPoint("LEFT", 0, 0)
    else
        btn.text:SetPoint("RIGHT", 0, 0)
    end

    btn.sortKey = sortKey

    btn:SetScript("OnClick", function(self)
        if currentSort == self.sortKey then
            sortAscending = not sortAscending
        else
            currentSort = self.sortKey
            sortAscending = false
        end
        UpdateSortIndicators()
        HDG_GoblinTab.Refresh()
    end)

    btn:SetScript("OnEnter", function(self)
        if self.sortKey ~= currentSort then
            local enterScheme = GetScheme()
            if enterScheme then
                self.text:SetTextColor(enterScheme.text_dim.r, enterScheme.text_dim.g, enterScheme.text_dim.b)
            else
                self.text:SetTextColor(0.8, 0.8, 0.8)  -- Light gray on hover (inactive columns)
            end
        end
    end)
    btn:SetScript("OnLeave", function(self)
        local leaveScheme = GetScheme()
        if self.sortKey == currentSort then
            if leaveScheme then
                self.text:SetTextColor(leaveScheme.text.r, leaveScheme.text.g, leaveScheme.text.b)
            else
                self.text:SetTextColor(1, 1, 1)  -- White for active sort
            end
        else
            -- Use neutral column header color (Base01 for Solarized)
            self.text:SetTextColor(HDG.Theme.GetColumnHeaderColor())
        end
    end)

    -- Track this header for sort indicator updates
    columnHeaders[sortKey] = btn

    return btn
end

-- Create the Goblin tab UI
function HDG_GoblinTab.Create()
    if goblinFrame then return end

    -- Use factory for panel (no header/scroll - custom filter rows above scroll)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    goblinFrame = layout.frame
    goblinFrame:Hide()
    mainFrame.goblinFrame = goblinFrame

    local scheme = GetScheme()

    -- Row 1: Profession filter buttons
    local profRow = CreateFrame("Frame", nil, goblinFrame)
    profRow:SetPoint("TOPLEFT", 8, -6)
    profRow:SetPoint("TOPRIGHT", -8, -6)
    profRow:SetHeight(20)
    goblinFrame.profRow = profRow

    local profLabel = profRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    profLabel:SetPoint("LEFT", 0, 0)
    ApplyFont(profLabel, scheme, "small")
    profLabel:SetText(HDG.Theme.C_Dim() .. "Prof:|r")

    -- Profession buttons container (will be populated by RefreshFilterButtons)
    goblinFrame.profButtons = {}

    -- Row 2: Expansion filter buttons
    local expRow = CreateFrame("Frame", nil, goblinFrame)
    expRow:SetPoint("TOPLEFT", 8, -26)
    expRow:SetPoint("TOPRIGHT", -8, -26)
    expRow:SetHeight(20)
    goblinFrame.expRow = expRow

    local expLabel = expRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    expLabel:SetPoint("LEFT", 0, 0)
    ApplyFont(expLabel, scheme, "small")
    expLabel:SetText(HDG.Theme.C_Dim() .. "Exp:|r")

    -- Expansion buttons container (will be populated by RefreshFilterButtons)
    goblinFrame.expButtons = {}

    -- Function to refresh filter buttons (creates on first call, updates state on subsequent)
    local filterButtonsCreated = false

    local function RefreshFilterButtons()
        if not filterButtonsCreated then
            -- First call: create all buttons once
            filterButtonsCreated = true

            local professions = {"All", "Alchemy", "Blacksmithing", "Cooking", "Enchanting", "Engineering", "Inscription", "Jewelcrafting", "Leatherworking", "Tailoring"}
            local profLocaleKeys = {
                ["All"] = "PROF_ALL",
                ["Alchemy"] = "PROF_ALCHEMY",
                ["Blacksmithing"] = "PROF_BLACKSMITHING",
                ["Cooking"] = "PROF_COOKING",
                ["Enchanting"] = "PROF_ENCHANTING",
                ["Engineering"] = "PROF_ENGINEERING",
                ["Inscription"] = "PROF_INSCRIPTION",
                ["Jewelcrafting"] = "PROF_JEWELCRAFTING",
                ["Leatherworking"] = "PROF_LEATHERWORKING",
                ["Tailoring"] = "PROF_TAILORING",
            }
            local profShortNames = { Blacksmithing = "Blacksmith", Jewelcrafting = "Jewelcraft", Leatherworking = "Leatherwork" }
            local profWidths = {42, 66, 62, 58, 90, 74, 76, 62, 68, 66}
            local xOffset = 30
            for i, prof in ipairs(professions) do
                local isActive = (professionFilter == prof)
                local displayName = profShortNames[prof] or L[profLocaleKeys[prof]] or prof
                local btn = HDG_UIHelpers.CreateChipButton(profRow, displayName, profWidths[i], isActive, function()
                    professionFilter = prof
                    RefreshFilterButtons()
                    HDG_GoblinTab.Refresh()
                end)
                btn:SetPoint("LEFT", xOffset, 0)
                btn.fullName = prof
                table.insert(goblinFrame.profButtons, btn)
                xOffset = xOffset + profWidths[i] + 4
            end

            -- Expansion buttons
            local EXP_BTN_WIDTH = 41
            xOffset = 30
            local allBtn = HDG_UIHelpers.CreateChipButton(expRow, "All", 37, (expansionFilter == "All"), function()
                expansionFilter = "All"
                RefreshFilterButtons()
                HDG_GoblinTab.Refresh()
            end)
            allBtn:SetPoint("LEFT", xOffset, 0)
            allBtn.fullName = "All"
            table.insert(goblinFrame.expButtons, allBtn)
            xOffset = xOffset + 39

            for _, exp in ipairs(HDG_ExpansionData.EXPANSION_ORDER) do
                local abbrev = HDG_ExpansionData.GetShortName(exp) or exp:sub(1,3)
                local isActive = (expansionFilter == exp)
                local expColor = HDG_ExpansionData.GetColor(exp)

                local btn = HDG_UIHelpers.CreateChipButton(expRow, abbrev, EXP_BTN_WIDTH, isActive, function()
                    expansionFilter = exp
                    RefreshFilterButtons()
                    HDG_GoblinTab.Refresh()
                end)
                btn:SetPoint("LEFT", xOffset, 0)
                btn.fullName = exp
                btn.expColor = expColor

                -- Tooltip with full expansion name
                btn:SetScript("OnEnter", function(self)
                    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                    GameTooltip:AddLine(exp, expColor[1], expColor[2], expColor[3])
                    GameTooltip:Show()
                end)
                btn:SetScript("OnLeave", function(self)
                    GameTooltip:Hide()
                end)

                table.insert(goblinFrame.expButtons, btn)
                xOffset = xOffset + EXP_BTN_WIDTH + 4
            end
        end

        -- Update visual state for all profession buttons
        for _, btn in ipairs(goblinFrame.profButtons) do
            local isActive = (professionFilter == btn.fullName)
            HDG_UIHelpers.UpdateChipButtonState(btn, isActive)
        end

        -- Update visual state for all expansion buttons
        for _, btn in ipairs(goblinFrame.expButtons) do
            local isActive = (expansionFilter == btn.fullName)
            HDG_UIHelpers.UpdateChipButtonState(btn, isActive)

            -- Re-apply expansion color text (not handled by generic state update)
            if btn.expColor then
                local expColor = btn.expColor
                local abbrev = btn.text:GetText():gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "") -- strip existing color codes
                local colorHex = string.format("%02x%02x%02x", expColor[1] * 255, expColor[2] * 255, expColor[3] * 255)
                if isActive then
                    btn.text:SetText("|cFF" .. colorHex .. abbrev .. "|r")
                else
                    local dimColor = string.format("%02x%02x%02x", math.min(255, expColor[1] * 220), math.min(255, expColor[2] * 220), math.min(255, expColor[3] * 220))
                    btn.text:SetText("|cFF" .. dimColor .. abbrev .. "|r")
                end
            end
        end
    end

    -- Store reference and call initial refresh
    goblinFrame.RefreshFilterButtons = RefreshFilterButtons
    RefreshFilterButtons()

    -- Price addon toggle (TSM / Auctionator) - on right side of prof row
    local priceToggleContainer = CreateFrame("Frame", nil, profRow, "BackdropTemplate")
    priceToggleContainer:SetSize(100, 20)
    priceToggleContainer:SetPoint("RIGHT", -8, 0)
    priceToggleContainer:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    local ptScheme = GetScheme()
    if ptScheme then
        priceToggleContainer:SetBackdropColor(ptScheme.bg.r, ptScheme.bg.g, ptScheme.bg.b, 0.8)
    else
        priceToggleContainer:SetBackdropColor(0.05, 0.05, 0.05, 0.8)
    end

    local tsmBtn = CreateFrame("Button", nil, priceToggleContainer, "BackdropTemplate")
    tsmBtn:SetSize(48, 18)
    tsmBtn:SetPoint("LEFT", 1, 0)
    tsmBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    local tsmText = tsmBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    tsmText:SetPoint("CENTER", 0, 0)
    ApplyFont(tsmText, ptScheme, "small")
    tsmText:SetText("TSM")

    local aucBtn = CreateFrame("Button", nil, priceToggleContainer, "BackdropTemplate")
    aucBtn:SetSize(48, 18)
    aucBtn:SetPoint("LEFT", tsmBtn, "RIGHT", 1, 0)
    aucBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    local aucText = aucBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    aucText:SetPoint("CENTER", 0, 0)
    ApplyFont(aucText, ptScheme, "small")
    aucText:SetText("Auc")

    -- Update toggle appearance based on current preference
    local function UpdatePriceToggle()
        local availableAddons = HDG_PriceIntegration.GetAvailableAddons()
        local hasTSM = HDG_PriceIntegration.IsTSMAvailable()
        local hasAuc = HDG_PriceIntegration.IsAuctionatorAvailable()

        -- Hide toggle if only one or no addons available
        if #availableAddons < 2 then
            priceToggleContainer:Hide()
            return
        end
        priceToggleContainer:Show()

        -- Determine active addon
        local preferred = HDG_Settings and HDG_Settings.preferredPriceAddon
        local active = preferred
        if not active then
            -- Default: TSM first
            active = hasTSM and "TSM" or (hasAuc and "Auctionator" or nil)
        end

        -- Update button appearance
        local toggleScheme = GetScheme()
        if active == "TSM" then
            if toggleScheme then
                tsmBtn:SetBackdropColor(toggleScheme.accent.r, toggleScheme.accent.g, toggleScheme.accent.b, 0.9)
                tsmText:SetTextColor(toggleScheme.text.r, toggleScheme.text.g, toggleScheme.text.b)
                aucBtn:SetBackdropColor(toggleScheme.bg.r, toggleScheme.bg.g, toggleScheme.bg.b, 0.8)
                aucText:SetTextColor(toggleScheme.text_dim.r, toggleScheme.text_dim.g, toggleScheme.text_dim.b)
            else
                tsmBtn:SetBackdropColor(0.08, 0.45, 0.42, 0.9)
                tsmText:SetTextColor(1, 1, 1)
                aucBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
                aucText:SetTextColor(0.6, 0.6, 0.6)
            end
        else
            if toggleScheme then
                tsmBtn:SetBackdropColor(toggleScheme.bg.r, toggleScheme.bg.g, toggleScheme.bg.b, 0.8)
                tsmText:SetTextColor(toggleScheme.text_dim.r, toggleScheme.text_dim.g, toggleScheme.text_dim.b)
                aucBtn:SetBackdropColor(toggleScheme.accent.r, toggleScheme.accent.g, toggleScheme.accent.b, 0.9)
                aucText:SetTextColor(toggleScheme.text.r, toggleScheme.text.g, toggleScheme.text.b)
            else
                tsmBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
                tsmText:SetTextColor(0.6, 0.6, 0.6)
                aucBtn:SetBackdropColor(0.08, 0.45, 0.42, 0.9)
                aucText:SetTextColor(1, 1, 1)
            end
        end
    end

    tsmBtn:SetScript("OnClick", function()
        if HDG_PriceIntegration.IsTSMAvailable() then
            HDG_Settings.preferredPriceAddon = "TSM"
            UpdatePriceToggle()
            UpdateTSMColumnVisibility()
            HDG_GoblinTab.Refresh()
        end
    end)
    tsmBtn:SetScript("OnEnter", function(self)
        if HDG_Settings.preferredPriceAddon ~= "TSM" then
            local hoverScheme = GetScheme()
            if hoverScheme then
                self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, 0.9)
            else
                self:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
            end
        end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Use TSM Prices")
        GameTooltip:AddLine("TradeSkillMaster market data", 1, 1, 1)
        GameTooltip:Show()
    end)
    tsmBtn:SetScript("OnLeave", function()
        UpdatePriceToggle()
        GameTooltip:Hide()
    end)

    aucBtn:SetScript("OnClick", function()
        if HDG_PriceIntegration.IsAuctionatorAvailable() then
            HDG_Settings.preferredPriceAddon = "Auctionator"
            UpdatePriceToggle()
            UpdateTSMColumnVisibility()
            HDG_GoblinTab.Refresh()
        end
    end)
    aucBtn:SetScript("OnEnter", function(self)
        if HDG_Settings.preferredPriceAddon ~= "Auctionator" then
            local hoverScheme = GetScheme()
            if hoverScheme then
                self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, 0.9)
            else
                self:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
            end
        end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Use Auctionator Prices")
        GameTooltip:AddLine("Auctionator auction data", 1, 1, 1)
        GameTooltip:Show()
    end)
    aucBtn:SetScript("OnLeave", function()
        UpdatePriceToggle()
        GameTooltip:Hide()
    end)

    -- Store reference for refresh
    goblinFrame.UpdatePriceToggle = UpdatePriceToggle

    -- Initial update
    UpdatePriceToggle()

    -- Scan button (for Auctionator) - positioned at right of expRow
    local scanScheme = GetScheme()
    scanButton = CreateFrame("Button", nil, expRow, "BackdropTemplate")
    scanButton:SetSize(80, 18)
    scanButton:SetPoint("RIGHT", expRow, "RIGHT", -8, 0)
    scanButton:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
    })
    scanButton:SetBackdropColor(scanScheme.button_normal.r, scanScheme.button_normal.g, scanScheme.button_normal.b, 0.9)
    scanButton:SetBackdropBorderColor(scanScheme.accent.r, scanScheme.accent.g, scanScheme.accent.b, 1)
    RegisterWidget(scanButton, "Button")
    scanButton.text = scanButton:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    scanButton.text:SetPoint("CENTER", 0, 0)
    ApplyFont(scanButton.text, scanScheme, "small")
    scanButton.text:SetText(HDG.Theme.C_Text() .. "Scan|r")

    -- Price freshness text - to the left of scan button (normal size, not small)
    priceSourceText = expRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    priceSourceText:SetPoint("RIGHT", scanButton, "LEFT", -6, 0)
    priceSourceText:SetJustifyH("RIGHT")
    ApplyFont(priceSourceText, scanScheme, "normal")
    priceSourceText:SetText(HDG.Theme.C_Dim() .. "Loading...|r")

    scanButton:SetScript("OnClick", function()
        local canScan, reason = HDG_GoblinTab.CanTriggerScan()
        if canScan then
            HDG_GoblinTab.TriggerScan()
        else
            print("|cFFFFD100[HDG]|r Cannot scan: " .. (reason or "Not ready"))
        end
    end)
    scanButton:SetScript("OnEnter", function(self)
        self:SetBackdropColor(GetScheme().button_hover.r, GetScheme().button_hover.g, GetScheme().button_hover.b, 1)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local canScan, reason = HDG_GoblinTab.CanTriggerScan()
        if canScan then
            GameTooltip:AddLine("Scan Auction House")
            GameTooltip:AddLine("Scans all current auctions to update pricing data for profit calculations.", 1, 1, 1, true)
            GameTooltip:AddLine(" ")
            local timeSince = GetTimeSinceLastScan()
            if timeSince then
                GameTooltip:AddLine("Last scan: " .. FormatTimeSince(timeSince), 0.7, 0.7, 0.7)
            end
            GameTooltip:AddLine("Click to scan now", 0.5, 1, 0.5)
        elseif reason == "AH Closed" then
            GameTooltip:AddLine("Auction House Required")
            GameTooltip:AddLine("Visit an auctioneer and open the Auction House window to scan prices.", 1, 1, 1, true)
        else
            GameTooltip:AddLine("Scan Cooldown")
            GameTooltip:AddLine(reason or "Please wait before scanning again.", 1, 0.82, 0, true)
        end
        GameTooltip:Show()
    end)
    scanButton:SetScript("OnLeave", function()
        UpdateScanButton()
        GameTooltip:Hide()
    end)

    -- Filter row (search box + checkboxes)
    local filterRow = CreateFrame("Frame", nil, goblinFrame)
    filterRow:SetPoint("TOPLEFT", 8, -48)
    filterRow:SetPoint("TOPRIGHT", -8, -48)
    filterRow:SetHeight(20)

    -- Search box
    local searchBox = CreateFrame("EditBox", nil, filterRow, "BackdropTemplate")
    searchBox:SetPoint("LEFT", 0, 0)
    searchBox:SetSize(115, 20)
    searchBox:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false, edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    local searchScheme = GetScheme()
    if searchScheme then
        searchBox:SetBackdropColor(searchScheme.bg.r, searchScheme.bg.g, searchScheme.bg.b, 1)
        searchBox:SetBackdropBorderColor(searchScheme.border.r, searchScheme.border.g, searchScheme.border.b, 0.8)
    else
        searchBox:SetBackdropColor(0.05, 0.05, 0.07, 1)
        searchBox:SetBackdropBorderColor(0.3, 0.3, 0.35, 0.8)
    end
    -- Apply themed font to editbox
    if searchScheme and searchScheme.fonts and searchScheme.fonts.small then
        local fontFile = searchScheme.fonts.small.file or "Fonts\\FRIZQT__.TTF"
        local fontSize = searchScheme.fonts.small.size or 10
        searchBox:SetFont(fontFile, fontSize, "")
    else
        searchBox:SetFontObject("GameFontHighlightSmall")
    end
    searchBox:SetAutoFocus(false)
    searchBox:SetTextInsets(6, 20, 0, 0)
    searchBox:SetMaxLetters(50)

    -- Search placeholder text
    local searchPlaceholder = searchBox:CreateFontString(nil, "ARTWORK")
    searchPlaceholder:SetPoint("LEFT", 6, 0)
    ApplyFont(searchPlaceholder, searchScheme, "small")
    searchPlaceholder:SetText("Search...")
    if searchScheme then
        searchPlaceholder:SetTextColor(searchScheme.text_dim.r, searchScheme.text_dim.g, searchScheme.text_dim.b, 0.6)
    end
    searchBox.placeholder = searchPlaceholder

    -- Search clear button (X)
    local searchClear = CreateFrame("Button", nil, searchBox)
    searchClear:SetSize(14, 14)
    searchClear:SetPoint("RIGHT", -3, 0)
    searchClear:Hide()
    local searchClearTex = searchClear:CreateTexture(nil, "ARTWORK")
    searchClearTex:SetAllPoints()
    searchClearTex:SetTexture("Interface\\Buttons\\UI-StopButton")
    searchClear:SetScript("OnClick", function()
        searchBox:SetText("")
        searchBox:ClearFocus()
        searchFilter = ""
        HDG_GoblinTab.Refresh()
    end)

    -- Search text change handler
    searchBox:SetScript("OnTextChanged", function(self, userInput)
        local text = self:GetText()
        if text and text ~= "" then
            searchPlaceholder:Hide()
            searchClear:Show()
        else
            searchPlaceholder:Show()
            searchClear:Hide()
        end
        if userInput then
            searchFilter = text
            HDG_GoblinTab.Refresh()
        end
    end)
    searchBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    searchBox:SetScript("OnEnterPressed", function(self)
        self:ClearFocus()
    end)
    RegisterWidget(searchBox, "SearchBox")

    -- Export to CSV button (icon only) - right of search box
    exportButton = CreateFrame("Button", nil, filterRow)
    exportButton:SetSize(16, 16)
    exportButton:SetPoint("LEFT", searchBox, "RIGHT", 6, 0)
    local exportIcon = exportButton:CreateTexture(nil, "ARTWORK")
    exportIcon:SetAllPoints()
    exportIcon:SetAtlas("housing-neighborhood-invite-icon")
    exportIcon:SetVertexColor(0.85, 0.85, 0.85)
    exportButton.icon = exportIcon
    exportButton:SetScript("OnEnter", function(self)
        self.icon:SetVertexColor(1, 1, 1)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("Export to CSV")
        GameTooltip:AddLine("Click to export profit data to CSV format", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    exportButton:SetScript("OnLeave", function(self)
        self.icon:SetVertexColor(0.85, 0.85, 0.85)
        GameTooltip:Hide()
    end)
    exportButton:SetScript("OnClick", ShowCSVExportWindow)

    -- Hide unprofitable checkbox
    local hideUnprofitableCheck = CreateCheckbox(filterRow, L["CHECK_HIDE_UNPROFITABLE"] or "Hide unprofitable", false, function(checked)
        hideUnprofitable = checked
        HDG_GoblinTab.Refresh()
    end)
    hideUnprofitableCheck:SetPoint("LEFT", exportButton, "RIGHT", 8, 0)

    -- Only known recipes checkbox (for current character) -- mutually exclusive with knownByAltsCheck
    local onlyKnownCheck, knownByAltsCheck
    onlyKnownCheck = CreateCheckbox(filterRow, L["CHECK_ONLY_KNOWN"] or "My Recipes", false, function(checked)
        onlyKnownRecipes = checked
        if checked and knownByAltsCheck then knownByAltsCheck:SetChecked(false); knownByAlts = false end
        HDG_GoblinTab.Refresh()
    end)
    onlyKnownCheck:SetPoint("LEFT", hideUnprofitableCheck, "RIGHT", 16, 0)
    onlyKnownCheck:SetSize(90, 18)

    -- Known by alts checkbox (any character) -- mutually exclusive with onlyKnownCheck
    knownByAltsCheck = CreateCheckbox(filterRow, L["CHECK_KNOWN_BY_ALTS"] or "All Known Recipes", false, function(checked)
        knownByAlts = checked
        if checked and onlyKnownCheck then onlyKnownCheck:SetChecked(false); onlyKnownRecipes = false end
        HDG_GoblinTab.Refresh()
    end)
    knownByAltsCheck:SetPoint("LEFT", onlyKnownCheck, "RIGHT", 16, 0)
    knownByAltsCheck:SetSize(140, 18)

    -- Item count text (shows filtered/total) - positioned at right edge
    local itemCountScheme = GetScheme()
    itemCountText = filterRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    itemCountText:SetPoint("RIGHT", filterRow, "RIGHT", -8, 0)
    itemCountText:SetJustifyH("RIGHT")
    ApplyFont(itemCountText, itemCountScheme, "small")
    itemCountText:SetText("")

    -- Column headers
    local columnHeaderRow = CreateFrame("Frame", nil, goblinFrame, "BackdropTemplate")
    columnHeaderRow:SetPoint("TOPLEFT", 8, -72)
    columnHeaderRow:SetPoint("TOPRIGHT", -8, -72)
    columnHeaderRow:SetHeight(22)
    columnHeaderRow:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
    })
    local colScheme = GetScheme()
    if colScheme then
        columnHeaderRow:SetBackdropColor(colScheme.panel.r, colScheme.panel.g, colScheme.panel.b, 1)
    else
        columnHeaderRow:SetBackdropColor(0.15, 0.15, 0.17, 1)
    end
    RegisterWidget(columnHeaderRow, "Panel")
    goblinFrame.columnHeaderRow = columnHeaderRow  -- Store reference for theme refresh

    -- Panel width ~826px usable (870 - 16 padding - 28 scrollbar)
    CreateColumnHeader(columnHeaderRow, "Item Name", "name", 8, 180, true)       -- Left-aligned
    CreateColumnHeader(columnHeaderRow, "Lumber", "lumberType", 192, 75, true)   -- Left-aligned (lumber type)

    -- Divider before $/Lum
    local divider1 = columnHeaderRow:CreateTexture(nil, "ARTWORK")
    local divScheme = GetScheme()
    if divScheme then
        divider1:SetColorTexture(divScheme.border.r, divScheme.border.g, divScheme.border.b, 0.6)
    else
        divider1:SetColorTexture(0.4, 0.4, 0.4, 0.6)
    end
    divider1:SetSize(1, 18)
    divider1:SetPoint("LEFT", 267, 0)

    CreateColumnHeader(columnHeaderRow, "$/Lum", "lumber", 271, 65, false)       -- Per-lumber value
    CreateColumnHeader(columnHeaderRow, "Total", "lumberTotal", 340, 65, false)  -- Total lumber value

    -- Divider after Total
    local divider2 = columnHeaderRow:CreateTexture(nil, "ARTWORK")
    if divScheme then
        divider2:SetColorTexture(divScheme.border.r, divScheme.border.g, divScheme.border.b, 0.6)
    else
        divider2:SetColorTexture(0.4, 0.4, 0.4, 0.6)
    end
    divider2:SetSize(1, 18)
    divider2:SetPoint("LEFT", 407, 0)

    CreateColumnHeader(columnHeaderRow, "Cost", "cost", 409, 65, false)          -- Right-aligned
    CreateColumnHeader(columnHeaderRow, "Sell", "sell", 478, 65, false)          -- Right-aligned
    CreateColumnHeader(columnHeaderRow, "Profit", "profit", 547, 70, false)      -- Right-aligned
    CreateColumnHeader(columnHeaderRow, "%", "margin", 621, 55, false)           -- Right-aligned

    -- Divider after %
    local divider3 = columnHeaderRow:CreateTexture(nil, "ARTWORK")
    if divScheme then
        divider3:SetColorTexture(divScheme.border.r, divScheme.border.g, divScheme.border.b, 0.6)
    else
        divider3:SetColorTexture(0.4, 0.4, 0.4, 0.6)
    end
    divider3:SetSize(1, 18)
    divider3:SetPoint("LEFT", 678, 0)

    -- TSM-only columns (visibility controlled by UpdateTSMColumnVisibility)
    local regionAvgHeader = CreateColumnHeader(columnHeaderRow, "Region Avg", "regionSaleAvg", 680, 90, false)
    columnHeaders["regionSaleAvg"] = regionAvgHeader

    local tsmMarginHeader = CreateColumnHeader(columnHeaderRow, "TSM %", "tsmMargin", 774, 55, false)
    columnHeaders["tsmMargin"] = tsmMarginHeader

    -- Set initial sort indicator
    UpdateSortIndicators()

    -- Set initial TSM column visibility
    UpdateTSMColumnVisibility()

    -- Scroll frame for item list
    scrollFrame = CreateFrame("ScrollFrame", "HDG_GoblinScrollFrame", goblinFrame, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 8, -96)
    scrollFrame:SetPoint("BOTTOMRIGHT", -28, 8)

    scrollContent = CreateFrame("Frame", nil, scrollFrame)
    scrollContent:SetSize(scrollFrame:GetWidth(), 1)
    scrollFrame:SetScrollChild(scrollContent)

    -- Initialize row pool
    rowPool = CreateRowFactory(scrollContent)

    -- Apply themed scrollbar styling
    if HDG_UIHelpers and HDG_UIHelpers.StyleScrollBar then
        HDG_UIHelpers.StyleScrollBar(scrollFrame)
    end

    -- Register for Auctionator scan callbacks
    RegisterScanCallback()

    -- Update freshness text every 60 seconds
    freshnessTimer = C_Timer.NewTicker(60, function()
        if goblinFrame and goblinFrame:IsShown() and scanState == "idle" then
            UpdateScanButton()
        end
    end)
end

-- ============================================================================
-- REFRESH / RENDER
-- ============================================================================

function HDG_GoblinTab.Refresh()
    if not goblinFrame or not goblinFrame:IsShown() then return end

    -- Update scan button state
    UpdateScanButton()

    -- Calculate fresh data
    CalculateProfitData()
    ApplyFiltersAndSort()

    -- Update item count (accent color, text/accent when filtered)
    if itemCountText then
        local filteredCount = #filteredData
        local totalCount = #profitData
        local countScheme = GetScheme()
        ApplyFont(itemCountText, countScheme, "small")  -- Reapply font for theme changes
        local accentHex = "14b8a6"
        local textHex = "FFFFFF"
        if countScheme then
            accentHex = string.format("%02x%02x%02x", countScheme.accent.r * 255, countScheme.accent.g * 255, countScheme.accent.b * 255)
            textHex = string.format("%02x%02x%02x", countScheme.text.r * 255, countScheme.text.g * 255, countScheme.text.b * 255)
        end
        if filteredCount == totalCount then
            itemCountText:SetText(string.format("|cFF%s%d items|r", accentHex, totalCount))
        else
            itemCountText:SetText(string.format("|cFF%s%d|r |cFF%s/ %d items|r", textHex, filteredCount, accentHex, totalCount))
        end
    end

    -- Release old rows and hide expansion panel temporarily
    rowPool:ReleaseAll()
    if expansionPanel then
        expansionPanel:Hide()
    end

    -- Render rows
    local yOffset = 0
    for i, item in ipairs(filteredData) do
        local row = rowPool:Acquire(scrollContent)
        row:SetPoint("TOPLEFT", 0, yOffset)
        row:SetPoint("RIGHT", 0, 0)

        -- Alternate row colors
        local rowScheme = GetScheme()
        local bgColor
        if rowScheme then
            if (i % 2 == 0) then
                bgColor = {rowScheme.panel.r, rowScheme.panel.g, rowScheme.panel.b, 0.7}
            else
                bgColor = {rowScheme.bg.r, rowScheme.bg.g, rowScheme.bg.b, 0.5}
            end
        else
            bgColor = (i % 2 == 0) and {0.14, 0.14, 0.16, 0.7} or {0.12, 0.12, 0.14, 0.5}
        end
        row.bgColor = bgColor
        row:SetBackdropColor(unpack(bgColor))

        row.itemID = item.itemID
        row.item = item

        -- Color name by item quality (theme-aware: Olive Green for Uncommon on light themes)
        local nameColor = HDG.Theme.GetQualityHex(item.quality or 1)
        row.nameText:SetText(nameColor .. item.name .. "|r")

        -- Lumber type (expansion-colored localized name + inventory count)
        if item.lumberType then
            local lumberInfo = HDG_ExpansionData.GetLumberInfo(item.lumberType)
            if lumberInfo then
                local lumberExpColor = HDG_ExpansionData.GetColor(lumberInfo.expansion)
                local lumberColorHex = string.format("|cFF%02x%02x%02x",
                    lumberExpColor[1] * 255, lumberExpColor[2] * 255, lumberExpColor[3] * 255)
                -- Get localized material name (strips lumber/wood words)
                local lumberDisplayName = HDG_ExpansionData.GetLumberMaterialName(item.lumberType) or lumberInfo.shortName or lumberInfo.name
                -- Get inventory count
                local invCount = _G.HDG_Data and _G.HDG_Data.GetItemCount(item.lumberType) or 0
                local countText = invCount > 0 and (" " .. HDG.Theme.C_Dim() .. "(" .. invCount .. ")|r") or ""
                row.lumberTypeText:SetText(lumberColorHex .. lumberDisplayName .. "|r" .. countText)
            else
                row.lumberTypeText:SetText(HDG.Theme.C_Dim() .. "-|r")
            end
        else
            row.lumberTypeText:SetText(HDG.Theme.C_Dim() .. "-|r")
        end

        -- Sell price (Solarized Yellow for currency)
        if item.sellPrice then
            row.sellText:SetText(HDG.Theme.C_Currency() .. HDG_PriceIntegration.FormatGoldOnly(item.sellPrice) .. "|r")
        else
            row.sellText:SetText(HDG.Theme.C_Dim() .. "?|r")
        end

        -- Material cost (Solarized Yellow for currency)
        if item.materialCost and item.materialCost > 0 then
            row.costText:SetText(HDG.Theme.C_Currency() .. HDG_PriceIntegration.FormatGoldOnly(item.materialCost) .. "|r")
        else
            row.costText:SetText(HDG.Theme.C_Dim() .. "-|r")
        end

        -- Profit (colored)
        if item.profit then
            local profitColor = item.profit >= 0 and (HDG.Theme.C_Success() .. "+") or HDG.Theme.C_Error()
            row.profitText:SetText(profitColor .. HDG_PriceIntegration.FormatGoldOnly(math.abs(item.profit)) .. "|r")
        else
            row.profitText:SetText(HDG.Theme.C_Dim() .. "?|r")
        end

        -- Margin
        if item.margin then
            local marginColor = item.margin >= 0 and HDG.Theme.C_Success() or HDG.Theme.C_Error()
            row.marginText:SetText(marginColor .. string.format("%.0f%%", item.margin) .. "|r")
        else
            row.marginText:SetText(HDG.Theme.C_Dim() .. "-|r")
        end

        -- Region Avg (TSM only)
        local isTSMActive = GetActivePriceAddonName() == "TSM"
        if isTSMActive then
            if item.regionSaleAvg then
                row.regionAvgText:SetText(HDG.Theme.C_Accent() .. HDG_PriceIntegration.FormatGoldOnly(item.regionSaleAvg) .. "|r")
            else
                row.regionAvgText:SetText(HDG.Theme.C_Dim() .. "-|r")
            end
            row.regionAvgText:Show()

            -- TSM Margin (based on region avg) - same color as Region Avg column
            if item.tsmMargin then
                row.tsmMarginText:SetText(HDG.Theme.C_Accent() .. string.format("%.0f%%", item.tsmMargin) .. "|r")
            else
                row.tsmMarginText:SetText(HDG.Theme.C_Dim() .. "-|r")
            end
            row.tsmMarginText:Show()
        else
            row.regionAvgText:Hide()
            row.tsmMarginText:Hide()
        end

        -- Per-lumber value ($/Lum)
        if item.lumberValue and item.lumberQty > 0 then
            local lumberColor = item.lumberValue >= 0 and HDG.Theme.C_Warning() or HDG.Theme.C_Error()
            row.lumberText:SetText(lumberColor .. HDG_PriceIntegration.FormatGoldOnly(item.lumberValue) .. "|r")
        else
            row.lumberText:SetText(HDG.Theme.C_Dim() .. "-|r")
        end

        -- Total lumber value (LumTot = per-lumber × qty)
        if item.lumberValue and item.lumberQty > 0 then
            local totalLumberValue = item.lumberValue * item.lumberQty
            local lumberColor = totalLumberValue >= 0 and HDG.Theme.C_Warning() or HDG.Theme.C_Error()
            row.lumberTotalText:SetText(lumberColor .. HDG_PriceIntegration.FormatGoldOnly(totalLumberValue) .. "|r")
        else
            row.lumberTotalText:SetText(HDG.Theme.C_Dim() .. "-|r")
        end

        yOffset = yOffset - 26

        -- Show expansion panel if this row is expanded
        if expandedRowKey == item.recipeKey then
            local panelHeight = ShowExpansionPanel(row, item)
            yOffset = yOffset - panelHeight
        end
    end

    -- Hide expansion panel if expanded row is no longer visible
    if expansionPanel and expandedRowKey then
        local found = false
        for _, item in ipairs(filteredData) do
            if item.recipeKey == expandedRowKey then
                found = true
                break
            end
        end
        if not found then
            HideExpansionPanel()
        end
    end

    -- Update scroll content height
    scrollContent:SetHeight(math.abs(yOffset) + 10)
end

-- Show the tab
function HDG_GoblinTab.Show()
    if not goblinFrame then
        HDG_GoblinTab.Create()
    end
    goblinFrame:Show()
    HDG_GoblinTab.Refresh()
end

-- Hide the tab
function HDG_GoblinTab.Hide()
    -- Close any open dropdown menus
    CloseActiveDropdown()
    -- Hide expansion panel
    HideExpansionPanel()
    if goblinFrame then
        goblinFrame:Hide()
    end
end

-- ============================================================================
-- EVENT BUS HANDLERS
-- ============================================================================

local function RegisterEventBusHandlers()
    if not HDG or not HDG.EventBus then return end
    -- Refresh on theme/font changes to recalculate row heights
    HDG.EventBus:Register("HDG_THEME_UPDATE", function()
        if goblinFrame and goblinFrame:IsShown() then
            HDG_GoblinTab.Refresh()
        end
    end)
end

C_Timer.After(0.1, RegisterEventBusHandlers)

-- Export to privateDB for HDG_UI access
privateDB.HDG_GoblinTab = HDG_GoblinTab
