local addonName, privateDB = ...

-- ============================================================================
-- DATA TAB MODULE
-- Crafting history and Farming history/statistics
-- ============================================================================

local CONFIG = privateDB.CONFIG
local L = HDG_L or {}  -- Localization
local HDG_DataTab = {}
privateDB.HDG_DataTab = HDG_DataTab

local mainFrame = nil
local dataFrame = nil

-- Farming history column widths (fills ~760px usable width)
local FARM_COL_TIME = 110
local FARM_COL_LUMBER = 95
local FARM_COL_COLLECTED = 65
local FARM_COL_RATE = 60
local FARM_COL_DURATION = 65
local FARM_COL_ZONE = 175
local FARM_COL_CHAR = 190

-- Decor collection cache. Event-invalidated (HDG_COLLECTION_READY listener
-- below wipes it); no TTL. Populated synchronously by BuildCollectionSnapshot
-- from HDG_Data.GetAllCatalog() — no per-tab searcher; HDG_Data owns the
-- only live searcher in the addon.
local decorCollectionCache = nil

-- ============================================================================
-- 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 function AddSectionStyle(fs, _) RegisterWidget(fs, "SectionHeader") end


-- ============================================================================
-- STORE HELPERS
-- ============================================================================

local function GetStore()
    local HDG = _G.HousingDecorGuide
    return HDG and HDG.Store
end

local function GetState()
    local store = GetStore()
    return store and store:GetState()
end

local function Dispatch(action, payload)
    local store = GetStore()
    if store then
        store:Dispatch(action, payload)
    end
end

local function GetFarmingHistoryFromStore()
    local state = GetState()
    return state and state.farmingHistory or {}
end

-- Reference to lumber types
local LUMBER_TYPES = nil

-- Spell ID to recipe lookup cache
local spellToRecipe = nil
-- Item ID to recipe lookup cache
local itemToRecipe = nil
-- Decor ID to recipe lookup cache
local decorToRecipe = nil

-- Build all recipe lookup tables in a single pass
local function BuildAllRecipeLookups()
    if spellToRecipe then return end
    spellToRecipe, itemToRecipe, decorToRecipe = {}, {}, {}
    if HDG_DecorDB then
        for _, recipe in pairs(HDG_DecorDB) do
            if recipe.spellID then spellToRecipe[recipe.spellID] = recipe end
            if recipe.itemID then itemToRecipe[recipe.itemID] = recipe end
            if recipe.decorID then decorToRecipe[recipe.decorID] = recipe end
        end
    end
end

local function BuildSpellToRecipeLookup()
    BuildAllRecipeLookups()
    return spellToRecipe
end

local function BuildItemToRecipeLookup()
    BuildAllRecipeLookups()
    return itemToRecipe
end

local function BuildDecorToRecipeLookup()
    BuildAllRecipeLookups()
    return decorToRecipe
end

-- ============================================================================
-- CRAFTING HISTORY FUNCTIONS
-- ============================================================================

-- Format timestamp for display
function HDG_DataTab.FormatTimestamp(timestamp)
    if not timestamp then return L["MISC_UNKNOWN"] or "Unknown" end
    return date("%Y-%m-%d %H:%M", timestamp)
end

-- Get crafting history from settings
function HDG_DataTab.GetCraftingHistory()
    local history = HDG.Store and HDG.Store.state and HDG.Store.state.craftingHistory
    if history then return history end
    return {}
end

-- Clear crafting history
function HDG_DataTab.ClearCraftingHistory()
    if HDG.Store then
        HDG.Store:Dispatch("CLEAR_CRAFTING_HISTORY", {})
        print(HDG.Theme.C_Success() .. "[Housing Decor Guide]|r " .. (L["DATA_CRAFTING_CLEARED"] or "Crafting history cleared."))
    end
end

-- Export crafting history to CSV format (copies to clipboard via edit box)
function HDG_DataTab.ExportCraftingToCSV()
    local history = HDG_DataTab.GetCraftingHistory()
    if #history == 0 then
        print(HDG.Theme.C_Error() .. "[Housing Decor Guide]|r " .. (L["DATA_NO_CRAFTING_EXPORT"] or "No crafting history to export."))
        return nil
    end

    -- Build CSV string
    local lines = {}
    table.insert(lines, "Timestamp,Item,ItemID,Quantity,Profession,Source,Character,Realm,ItemPrice,MaterialCost,PriceSource")

    for _, entry in ipairs(history) do
        local timestamp = entry.timestamp and date("%Y-%m-%d %H:%M:%S", entry.timestamp) or ""
        -- Use localized item name for export
        local fallbackName = entry.name or entry.itemName or "Unknown"
        local localizedName = entry.itemID and HDG_Data.GetLocalizedName(entry.itemID, fallbackName) or fallbackName
        local itemName = localizedName:gsub(",", ";")  -- Escape commas
        local itemID = entry.itemID or ""
        local quantity = entry.quantity or 1
        local profession = (entry.profession or ""):gsub(",", ";")
        local source = entry.source or "Crafted"
        local character = (entry.character or ""):gsub(",", ";")
        local realm = (entry.realm or ""):gsub(",", ";")
        local itemPrice = entry.itemPrice or ""
        local materialCost = entry.materialCost or ""
        local priceSource = entry.priceSource or ""

        table.insert(lines, string.format("%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s",
            timestamp, itemName, itemID, quantity, profession, source, character, realm, itemPrice, materialCost, priceSource))
    end

    return table.concat(lines, "\n")
end

-- Log a craft from item link (new method using TRADE_SKILL_ITEM_CRAFTED_RESULT)
function HDG_DataTab.LogCraftFromItemLink(itemLink, quantity)
    if not itemLink then return end

    -- Extract itemID from item link: |cff...|Hitem:ITEMID:...|h[Name]|h|r
    local itemID = tonumber(itemLink:match("item:(%d+)"))
    if not itemID then return end

    local lookup = BuildItemToRecipeLookup()
    local recipe = lookup[itemID]

    if recipe then
        -- This is a decor recipe craft!
        -- Use Store directly for proper persistence
        local HDG = _G.HousingDecorGuide
        local store = HDG and HDG.Store
        if not store then return end

        -- Get prices at time of crafting (if price addon available)
        local itemPrice, materialCost, priceSource
        if HDG_PriceIntegration then
            if HDG_PriceIntegration.GetItemPrice then
                local price, source = HDG_PriceIntegration.GetItemPrice(recipe.itemID)
                if price and price > 0 then itemPrice = price; priceSource = source end
            end
            if recipe.reagents and HDG_PriceIntegration.CalculateTotalPrice then
                local totalPrice = HDG_PriceIntegration.CalculateTotalPrice(recipe.reagents)
                if totalPrice and totalPrice > 0 then
                    materialCost = totalPrice
                    if not priceSource then
                        local _, source = HDG_PriceIntegration.HasPriceAddon()
                        priceSource = source
                    end
                end
            end
        end

        store:AddCraftingHistory({
            itemID = recipe.itemID,
            itemName = recipe.name,
            quantity = quantity or 1,
            profession = recipe.profession,
            character = UnitName("player"),
            realm = GetRealmName(),
            itemPrice = itemPrice,
            materialCost = materialCost,
            priceSource = priceSource,
            source = "Crafted",
        })

        -- Refresh history display if Data tab is visible
        if mainFrame and mainFrame.RefreshHistoryList then
            mainFrame.RefreshHistoryList()
        end
    end
end

-- Log acquisition when player learns a decor item from bags
function HDG_DataTab.LogAcquisition(decorID)
    if not decorID then return end

    -- Look up item info from central catalog cache
    local info = HDG_Data and HDG_Data.GetCatalog and HDG_Data.GetCatalog(decorID)
    if not info then return end

    local hdg = _G.HousingDecorGuide
    local store = hdg and hdg.Store
    if not store then return end

    store:AddCraftingHistory({
        decorID = decorID,
        itemID = info.itemID,
        itemName = info.name,
        character = UnitName("player"),
        realm = GetRealmName(),
        source = "Learned",
    })

    -- Trigger craft quote for acquired decor items (only if master toggle and chattiness > 0)
    local config = (store:GetState() or {}).config or {}
    local quotesEnabled = config.quotesEnabled
    if quotesEnabled == nil then quotesEnabled = true end
    local chattiness = config.vamooseChattiness or 30
    if quotesEnabled and config.craftQuotesEnabled ~= false and chattiness > 0 then
        local lookup = BuildDecorToRecipeLookup()
        local recipe = lookup[decorID]
        if recipe and recipe.profession and recipe.expansion then
            -- Get expansion abbreviation (e.g., "Classic Alchemy" -> "Classic")
            local expansionAbbr = HDG_ExpansionData.GetAbbreviation(recipe.expansion)

            -- Get quote for this profession/expansion combo
            local HDG_CraftQuotes = privateDB.HDG_CraftQuotes
            local HDG_Vamoose = privateDB.HDG_Vamoose
            if HDG_CraftQuotes and HDG_Vamoose then
                local quote = HDG_CraftQuotes.GetRandomQuote(recipe.profession, expansionAbbr)
                if quote then
                    local speaker = HDG_CraftQuotes.GetSpeaker(recipe.profession)
                    if speaker then
                        local onlyChat = config.quotesOnlyChat or false
                        -- Show talking head unless "only chat" is enabled
                        if not onlyChat then
                            HDG_Vamoose.PlayTalkingHead(quote, recipe.profession, speaker.displayID)
                        end
                        -- Print to chat if "only chat" is enabled
                        if onlyChat then
                            print(HDG.Theme.C_Accent() .. "[" .. speaker.name .. "]|r " .. quote)
                        end
                    end
                end
            end
        end
    end

    -- Refresh history display if Data tab is visible
    if mainFrame and mainFrame.RefreshHistoryList then
        mainFrame.RefreshHistoryList()
    end
end

-- ============================================================================
-- FARMING HISTORY FUNCTIONS
-- ============================================================================

-- Initialize farming history (no longer needed - Store handles defaults)
function HDG_DataTab.InitFarmingHistory()
    -- No-op: Store initializes farmingHistory in DEFAULT_STATE
end

-- Get farming history from Store
function HDG_DataTab.GetFarmingHistory()
    return GetFarmingHistoryFromStore()
end

-- Save or update a farming session in history (upsert behavior)
-- sessionData: { sessionId, lumberId, lumberName, collected, duration, perHour, zone, timestamp }
-- sessionId is used to update an existing session rather than creating a new one
function HDG_DataTab.SaveFarmingSession(sessionData)
    -- Build session object with defaults
    local session = {
        sessionId = sessionData.sessionId or (sessionData.lumberId .. "_" .. time()),
        timestamp = sessionData.timestamp or time(),
        lumberId = sessionData.lumberId,
        lumberName = sessionData.lumberName,
        collected = sessionData.collected or 0,
        duration = sessionData.duration or 0,  -- in seconds
        perHour = sessionData.perHour or 0,
        zone = sessionData.zone or "Unknown",
        character = sessionData.character,
        realm = sessionData.realm
    }

    -- Use Store reducer to upsert
    Dispatch("UPSERT_FARMING_SESSION", { session = session })

    return session
end

-- Clear farming history
function HDG_DataTab.ClearFarmingHistory()
    Dispatch("CLEAR_FARMING_HISTORY", {})
    print(HDG.Theme.C_Success() .. "[Housing Decor Guide]|r " .. (L["DATA_FARMING_CLEARED"] or "Farming history cleared."))
    -- Refresh display if visible
    if dataFrame and dataFrame:IsVisible() then
        HDG_DataTab.RefreshFarmingDisplay()
    end
end

-- Delete most recent farming session
function HDG_DataTab.DeleteMostRecentFarmingSession()
    local history = HDG_DataTab.GetFarmingHistory()
    if #history == 0 then
        print(HDG.Theme.C_Error() .. "[Housing Decor Guide]|r " .. (L["DATA_NO_FARMING_DELETE"] or "No farming sessions to delete."))
        return false
    end
    local deleted = history[1]
    Dispatch("DELETE_MOST_RECENT_FARMING_SESSION", {})
    print(HDG.Theme.C_Success() .. "[Housing Decor Guide]|r " .. (L["DATA_DELETED_SESSION"] or "Deleted most recent session: ") .. (deleted.lumberName or L["MISC_UNKNOWN"] or "Unknown"))
    -- Refresh display if visible
    if dataFrame and dataFrame:IsVisible() then
        HDG_DataTab.RefreshFarmingDisplay()
    end
    return true
end

-- Export farming history to CSV format
function HDG_DataTab.ExportFarmingToCSV()
    local history = HDG_DataTab.GetFarmingHistory()
    if #history == 0 then
        print(HDG.Theme.C_Error() .. "[Housing Decor Guide]|r " .. (L["DATA_NO_FARMING_EXPORT"] or "No farming history to export."))
        return nil
    end

    -- Build CSV string
    local lines = {}
    table.insert(lines, "Timestamp,Lumber,LumberID,Collected,Rate/Hour,Duration(sec),Zone,Character,Realm")

    for _, entry in ipairs(history) do
        local timestamp = entry.timestamp and date("%Y-%m-%d %H:%M:%S", entry.timestamp) or ""
        -- Use localized lumber name for export
        local localizedName = HDG_ExpansionData.GetLumberMaterialName(entry.lumberId) or entry.lumberName or "Unknown"
        local lumberName = localizedName:gsub(",", ";")  -- Escape commas
        local lumberId = entry.lumberId or ""
        local collected = entry.collected or 0
        local perHour = string.format("%.1f", entry.perHour or 0)
        local duration = math.floor(entry.duration or 0)
        local zone = (entry.zone or "Unknown"):gsub(",", ";")
        local character = (entry.character or ""):gsub(",", ";")
        local realm = (entry.realm or ""):gsub(",", ";")

        table.insert(lines, string.format("%s,%s,%s,%d,%s,%d,%s,%s,%s",
            timestamp, lumberName, lumberId, collected, perHour, duration, zone, character, realm))
    end

    return table.concat(lines, "\n")
end

-- Export decor collection to CSV format (sorted by category then name)
function HDG_DataTab.ExportDecorCollectionToCSV()
    if not decorCollectionCache or #decorCollectionCache == 0 then
        print(HDG.Theme.C_Error() .. "[Housing Decor Guide]|r " .. (L["DATA_NO_COLLECTION_EXPORT"] or "No collection data to export. Click Refresh first."))
        return nil
    end

    -- Sort by category then name
    local sorted = {}
    for _, item in ipairs(decorCollectionCache) do
        table.insert(sorted, item)
    end
    table.sort(sorted, function(a, b)
        local catA = a.categoryName or "ZZZ"
        local catB = b.categoryName or "ZZZ"
        if catA ~= catB then return catA < catB end
        return (a.name or "") < (b.name or "")
    end)

    local lines = {}
    table.insert(lines, "Name,RecordID,Owned,NumStored,NumPlaced,Category,Subcategory,Tags")

    for _, entry in ipairs(sorted) do
        local name = (entry.name or "Unknown"):gsub(",", ";"):gsub("|", "")
        local recordID = entry.recordID or ""
        local numStored = entry.numStored or 0
        local numPlaced = entry.numPlaced or 0
        local owned = (numStored > 0 or numPlaced > 0) and "Yes" or "No"
        local category = (entry.categoryName or ""):gsub(",", ";"):gsub("|", "")
        local subcategory = (entry.subcategoryName or ""):gsub(",", ";"):gsub("|", "")
        local tags = ""
        if entry.tags then
            local cleanTags = {}
            for _, t in ipairs(entry.tags) do
                table.insert(cleanTags, (t:gsub("|", ""):gsub(",", ";")))
            end
            tags = table.concat(cleanTags, "/")
        end

        table.insert(lines, string.format("%s,%s,%s,%d,%d,%s,%s,%s",
            name, recordID, owned, numStored, numPlaced, category, subcategory, tags))
    end

    return table.concat(lines, "\n")
end

-- Build the decor collection snapshot synchronously from HDG_Data's central
-- catalog. The legacy implementation spun up its own CreateCatalogSearcher
-- with SetOwnedOnly(false), but immediately filtered the result through
-- HDG_Data.GetCatalog (which only returns owned rows), so it produced the
-- same set HDG_Data already exposes — at the cost of a redundant 5k+ entry
-- searcher round-trip. Returns true on success, false if the central catalog
-- isn't ready yet (caller should kick HDG_Data.RefreshCatalog and rely on
-- HDG_COLLECTION_READY to re-render).
-- Per-session caches for category/subcategory name lookups. Categories are
-- static within a session; without these, BuildCollectionSnapshot would call
-- the API once per owned item per HDG_COLLECTION_READY event (4k+ items
-- × multiple events per session = avoidable).
local _catNameCache, _subcatNameCache = {}, {}
local function getCategoryName(catID)
    if not catID or not C_HousingCatalog or not C_HousingCatalog.GetCatalogCategoryInfo then return nil end
    local cached = _catNameCache[catID]
    if cached ~= nil then return cached or nil end  -- false sentinel for "looked up, not found"
    local ok, info = pcall(C_HousingCatalog.GetCatalogCategoryInfo, catID)
    local name = (ok and info and info.name) or nil
    _catNameCache[catID] = name or false
    return name
end
local function getSubcategoryName(subID)
    if not subID or not C_HousingCatalog or not C_HousingCatalog.GetCatalogSubcategoryInfo then return nil end
    local cached = _subcatNameCache[subID]
    if cached ~= nil then return cached or nil end
    local ok, info = pcall(C_HousingCatalog.GetCatalogSubcategoryInfo, subID)
    local name = (ok and info and info.name) or nil
    _subcatNameCache[subID] = name or false
    return name
end

local function BuildCollectionSnapshot()
    local catalog = HDG_Data and HDG_Data.GetAllCatalog and HDG_Data.GetAllCatalog()
    if not catalog then return false end

    local items = {}
    for decorID, row in pairs(catalog) do
        if row.name then
            local categoryName = getCategoryName(row.categoryID)
            local subcategoryName = getSubcategoryName(row.subcategoryID)

            -- row.tags is a {[tagID]=name} map (info.dataTagsByID); iterate
            -- with pairs() — ipairs misses non-sequential numeric keys.
            local itemTags = {}
            if row.tags then
                for _, tagName in pairs(row.tags) do
                    table.insert(itemTags, tagName)
                end
            end

            table.insert(items, {
                recordID = decorID,
                name = row.name,
                numStored = row.quantity or 0,
                numPlaced = row.numPlaced or 0,
                categoryName = categoryName,
                subcategoryName = subcategoryName,
                tags = itemTags,
            })
        end
    end

    local totalOwned, exemptOwned = 0, 0
    if C_HousingCatalog and C_HousingCatalog.GetDecorTotalOwnedCount then
        local ok, t, e = pcall(C_HousingCatalog.GetDecorTotalOwnedCount)
        if ok then totalOwned, exemptOwned = t or 0, e or 0 end
    end

    decorCollectionCache = items
    decorCollectionCache.totalOwned = totalOwned
    decorCollectionCache.exemptOwned = exemptOwned
    return true
end

-- Format duration for display (seconds -> "Xh Ym" or "Xm Ys")
local function FormatDuration(seconds)
    if not seconds or seconds <= 0 then return "0s" end

    if seconds < 60 then
        return string.format("%ds", math.floor(seconds))
    elseif seconds < 3600 then
        local mins = math.floor(seconds / 60)
        local secs = math.floor(seconds % 60)
        return string.format("%dm %ds", mins, secs)
    else
        local hours = math.floor(seconds / 3600)
        local mins = math.floor((seconds % 3600) / 60)
        return string.format("%dh %dm", hours, mins)
    end
end

-- FormatTimestamp is now defined above in CRAFTING HISTORY FUNCTIONS section

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

function HDG_DataTab.Init(frame)
    mainFrame = frame
    LUMBER_TYPES = HDG_ExpansionData and HDG_ExpansionData.LUMBER_TYPES or {}
end

function HDG_DataTab.CreatePanel()
    if not mainFrame then return end

    -- Use factory for panel structure (no header bar - sections have their own titles)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = true,
    })

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

    local scrollChild = layout.scrollChild
    scrollChild:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 60)
    scrollChild:SetHeight(CONFIG.SCROLL_CHILD_HEIGHT)
    dataFrame.scrollChild = scrollChild

    local yOffset = 0

    -- ========================================================================
    -- HOUSING ACHIEVEMENTS SECTION (see HDG_AchievementSection.lua)
    -- ========================================================================
    local HDG_AchSection = privateDB.HDG_AchievementSection
    local contentParent = HDG_AchSection.Create(scrollChild, yOffset, dataFrame)
    scrollChild = contentParent -- redirect subsequent parenting
    yOffset = 0

    -- Separator after achievements
    local achSep = scrollChild:CreateTexture(nil, "ARTWORK")
    achSep:SetHeight(1)
    achSep:SetPoint("TOPLEFT", 4, yOffset)
    achSep:SetPoint("TOPRIGHT", -4, yOffset)
    local achScheme = GetScheme()
    if achScheme then
        achSep:SetColorTexture(achScheme.border.r, achScheme.border.g, achScheme.border.b, 0.5)
    else
        achSep:SetColorTexture(0.3, 0.3, 0.35, 0.5)
    end
    yOffset = yOffset - 15

    -- ========================================================================
    -- CRAFTING HISTORY SECTION
    -- ========================================================================
    local ctScheme = GetScheme()
    local craftingTitle = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    craftingTitle:SetPoint("TOPLEFT", 4, yOffset)
    ApplyFont(craftingTitle, ctScheme, "header")
    craftingTitle:SetText(L["HEADER_CRAFTING_HISTORY"] or "Decor Crafting / Acquisition History")
    if ctScheme then
        craftingTitle:SetTextColor(ctScheme.text_header.r, ctScheme.text_header.g, ctScheme.text_header.b, ctScheme.text_header.a)
    else
        craftingTitle:SetTextColor(0.99, 0.83, 0.22, 1)
    end
    craftingTitle.isHeader = true
    AddSectionStyle(craftingTitle, scrollChild)
    RegisterWidget(craftingTitle, "PanelTitle")

    -- Help text (next to title)
    local craftingDesc = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    craftingDesc:SetPoint("LEFT", craftingTitle, "RIGHT", 10, 0)
    craftingDesc:SetText(HDG.Theme.C_Dim() .. (L["DATA_CRAFTING_SORT_HELP"] or "(click item to preview, headers to sort)") .. "|r")
    ApplyFont(craftingDesc, ctScheme, "small")

    -- Export CSV Button for crafting
    local craftExportBtn = CreateFrame("Button", nil, scrollChild)
    craftExportBtn:SetSize(100, 22)
    craftExportBtn:SetPoint("TOPRIGHT", -4, yOffset)
    local craftExportTex = craftExportBtn:CreateTexture(nil, "ARTWORK")
    craftExportTex:SetAllPoints()
    craftExportTex:SetTexture(CONFIG.ADDON_TEXTURES.CSV_EXPORT)
    craftExportBtn:SetScript("OnEnter", function() craftExportTex:SetAlpha(0.8) end)
    craftExportBtn:SetScript("OnLeave", function() craftExportTex:SetAlpha(1.0) end)

    craftExportBtn:SetScript("OnClick", function()
        if HDG_DataTab.ExportCraftingToCSV then
            local csv = HDG_DataTab.ExportCraftingToCSV()
            if csv then
                if not HDG_CraftExportCtrl then
                    HDG_CraftExportCtrl = HDG_UIHelpers.CreateExportDialog()
                end
                HDG_CraftExportCtrl:Show(csv, HDG.Theme.C_Accent() .. (L["EXPORT_CRAFTING_TITLE"] or "Export Crafting History") .. "|r")
                local msg = L["DATA_EXPORTED_CRAFTING"] or "Exported %d crafting entries."
                print(HDG.Theme.C_Accent() .. "[Housing Decor Guide]|r " .. string.format(msg, #HDG_DataTab.GetCraftingHistory()))
            end
        end
    end)

    yOffset = yOffset - 25

    -- Sorting state for crafting history
    local craftSortColumn = "timestamp"
    local craftSortAscending = false

    -- Column widths for crafting (fills ~760px usable width)
    local COL_TIME = 115
    local COL_ITEM = 280
    local COL_PRICE = 90
    local COL_SOURCE = 80
    local COL_CHAR = 195

    -- Column header helper
    local function CreateColumnHeader(parent, text, width, xOffset, column, yPos)
        local btn = CreateFrame("Button", nil, parent)
        btn:SetSize(width, 18)
        btn:SetPoint("TOPLEFT", xOffset, yPos)

        local colScheme = GetScheme()
        local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("LEFT", 2, 0)
        ApplyFont(label, colScheme, "small")
        label:SetText(HDG.Theme.C_Dim() .. text .. "|r")
        btn.label = label

        local arrow = btn:CreateTexture(nil, "ARTWORK")
        arrow:SetSize(10, 10)
        arrow:SetPoint("LEFT", label, "RIGHT", 4, 0)
        arrow:SetTexture(CONFIG.ADDON_TEXTURES.SORT_ARROW)
        arrow:Hide()
        btn.arrow = arrow

        btn:SetScript("OnEnter", function(self)
            self.label:SetText(HDG.Theme.C_Text() .. text .. "|r")
        end)
        btn:SetScript("OnLeave", function(self)
            self.label:SetText(HDG.Theme.C_Dim() .. text .. "|r")
        end)

        btn.column = column
        return btn
    end

    -- Column headers for crafting
    local craftHeaderRow = CreateFrame("Frame", nil, scrollChild)
    craftHeaderRow:SetPoint("TOPLEFT", 4, yOffset)
    craftHeaderRow:SetSize(CONFIG.MAIN_FRAME_WIDTH - 80, 18)

    local craftHeaderBg = craftHeaderRow:CreateTexture(nil, "BACKGROUND")
    craftHeaderBg:SetAllPoints()
    local chbScheme = GetScheme()
    if chbScheme then
        craftHeaderBg:SetColorTexture(chbScheme.panel.r, chbScheme.panel.g, chbScheme.panel.b, 1)
    else
        craftHeaderBg:SetColorTexture(0.15, 0.15, 0.18, 1)
    end

    local craftTimeHeader = CreateColumnHeader(craftHeaderRow, "Time", COL_TIME, 5, "timestamp", 0)
    local craftItemHeader = CreateColumnHeader(craftHeaderRow, "Item", COL_ITEM, 5 + COL_TIME, "itemName", 0)
    local craftPriceHeader = CreateColumnHeader(craftHeaderRow, "Price", COL_PRICE, 5 + COL_TIME + COL_ITEM, "itemPrice", 0)
    local craftSourceHeader = CreateColumnHeader(craftHeaderRow, "Source", COL_SOURCE, 5 + COL_TIME + COL_ITEM + COL_PRICE, "source", 0)
    local craftCharHeader = CreateColumnHeader(craftHeaderRow, "Character", COL_CHAR, 5 + COL_TIME + COL_ITEM + COL_PRICE + COL_SOURCE, "character", 0)

    yOffset = yOffset - 20

    -- Crafting history container
    local craftContainer = CreateFrame("Frame", nil, scrollChild, "BackdropTemplate")
    craftContainer:SetPoint("TOPLEFT", 4, yOffset)
    craftContainer:SetSize(CONFIG.MAIN_FRAME_WIDTH - 80, 100)
    craftContainer: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 craftScheme = GetScheme()
    if craftScheme then
        craftContainer:SetBackdropColor(craftScheme.panel.r, craftScheme.panel.g, craftScheme.panel.b, 0.9)
        craftContainer:SetBackdropBorderColor(craftScheme.border.r, craftScheme.border.g, craftScheme.border.b, craftScheme.border.a)
        RegisterWidget(craftContainer, "Panel")
    else
        craftContainer:SetBackdropColor(0.08, 0.08, 0.10, 0.9)
        craftContainer:SetBackdropBorderColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    end

    -- Crafting history scroll frame
    local craftScrollFrame = CreateFrame("ScrollFrame", nil, craftContainer, "UIPanelScrollFrameTemplate")
    craftScrollFrame:SetPoint("TOPLEFT", 5, -5)
    craftScrollFrame:SetPoint("BOTTOMRIGHT", -25, 5)

    local craftScrollChild = CreateFrame("Frame", nil, craftScrollFrame)
    craftScrollFrame:SetScrollChild(craftScrollChild)
    craftScrollChild:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 120)
    craftScrollChild:SetHeight(1)
    dataFrame.craftScrollChild = craftScrollChild

    HDG_UIHelpers.StyleScrollBar(craftScrollFrame)

    -- Row pool for crafting history
    local craftRowPool = {}

    local function GetCraftRow(index)
        if craftRowPool[index] then
            return craftRowPool[index]
        end

        local rowScheme = GetScheme()
        local row = CreateFrame("Frame", nil, craftScrollChild)
        row:SetSize(CONFIG.MAIN_FRAME_WIDTH - 130, 18)

        row.timeText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.timeText:SetPoint("LEFT", 0, 0)
        row.timeText:SetWidth(COL_TIME)
        row.timeText:SetJustifyH("LEFT")
        row.timeText:SetWordWrap(false)
        ApplyFont(row.timeText, rowScheme, "small")

        row.itemBtn = CreateFrame("Button", nil, row)
        row.itemBtn:SetPoint("LEFT", COL_TIME, 0)
        row.itemBtn:SetSize(COL_ITEM, 18)

        row.itemText = row.itemBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.itemText:SetPoint("LEFT", 0, 0)
        row.itemText:SetWidth(COL_ITEM - 5)
        row.itemText:SetJustifyH("LEFT")
        row.itemText:SetWordWrap(false)
        ApplyFont(row.itemText, rowScheme, "small")

        row.itemBtn:SetScript("OnEnter", function(self)
            if self.itemID or self.itemName then
                row.itemText:SetText(HDG.Theme.C_Accent() .. (self.itemName or "Unknown") .. "|r")
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetText(L["TIP_CLICK_TO_PREVIEW"], 1, 1, 1)
                GameTooltip:Show()
            end
        end)
        row.itemBtn:SetScript("OnLeave", function(self)
            if self.itemID or self.itemName then
                row.itemText:SetText(HDG.Theme.C_Text() .. (self.itemName or "Unknown") .. "|r")
            end
            GameTooltip:Hide()
        end)
        row.itemBtn:SetScript("OnClick", function(self)
            if HDG_DecorPreview and HDG_DecorPreview.PreviewByName then
                -- Pass name, itemID, and decorID - PreviewByName will use what's available
                HDG_DecorPreview:PreviewByName(self.itemName, self.itemID, self.decorID)
            end
        end)

        row.priceBtn = CreateFrame("Button", nil, row)
        row.priceBtn:SetPoint("LEFT", COL_TIME + COL_ITEM, 0)
        row.priceBtn:SetSize(COL_PRICE, 18)

        row.priceText = row.priceBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.priceText:SetPoint("LEFT", 0, 0)
        row.priceText:SetWidth(COL_PRICE)
        row.priceText:SetJustifyH("LEFT")
        row.priceText:SetWordWrap(false)
        ApplyFont(row.priceText, rowScheme, "small")

        row.priceBtn:SetScript("OnEnter", function(self)
            local parent = self:GetParent()
            if parent.materialCost or parent.itemPrice then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:AddLine(L["DATA_TIP_AH_PRICE"], 1, 0.82, 0.3)
                if parent.itemPrice and parent.itemPrice > 0 then
                    local itemPriceStr = HDG_PriceIntegration and HDG_PriceIntegration.FormatGold(parent.itemPrice) or string.format("%dg", math.floor(parent.itemPrice / 10000))
                    GameTooltip:AddDoubleLine(L["DATA_TIP_ITEM_VALUE"], itemPriceStr, 0.7, 0.7, 0.7, 0.08, 0.72, 0.65)
                end
                if parent.materialCost and parent.materialCost > 0 then
                    local matCostStr = HDG_PriceIntegration and HDG_PriceIntegration.FormatGold(parent.materialCost) or string.format("%dg", math.floor(parent.materialCost / 10000))
                    GameTooltip:AddDoubleLine(L["DATA_TIP_MATERIAL_COST"], matCostStr, 0.7, 0.7, 0.7, 1, 0.82, 0.3)
                end
                if parent.itemPrice and parent.itemPrice > 0 and parent.materialCost and parent.materialCost > 0 then
                    local profit = parent.itemPrice - parent.materialCost
                    local profitStr = HDG_PriceIntegration and HDG_PriceIntegration.FormatGold(math.abs(profit)) or string.format("%dg", math.floor(math.abs(profit) / 10000))
                    if profit >= 0 then
                        GameTooltip:AddDoubleLine(L["DATA_TIP_PROFIT"], profitStr, 0.7, 0.7, 0.7, 0.53, 1, 0.53)
                    else
                        GameTooltip:AddDoubleLine(L["DATA_TIP_LOSS"], profitStr, 0.7, 0.7, 0.7, 1, 0.4, 0.4)
                    end
                end
                if parent.priceSource then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine(string.format(L["DATA_TIP_PRICE_SOURCE"], parent.priceSource), 0.78, 0.78, 0.78)
                end
                GameTooltip:Show()
            end
        end)
        row.priceBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

        row.sourceText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.sourceText:SetPoint("LEFT", COL_TIME + COL_ITEM + COL_PRICE, 0)
        row.sourceText:SetWidth(COL_SOURCE)
        row.sourceText:SetJustifyH("LEFT")
        row.sourceText:SetWordWrap(false)
        ApplyFont(row.sourceText, rowScheme, "small")

        row.charText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.charText:SetPoint("LEFT", COL_TIME + COL_ITEM + COL_PRICE + COL_SOURCE, 0)
        row.charText:SetWidth(COL_CHAR)
        row.charText:SetJustifyH("LEFT")
        row.charText:SetWordWrap(false)
        ApplyFont(row.charText, rowScheme, "small")

        craftRowPool[index] = row
        return row
    end

    -- Update sort arrows for crafting
    local function UpdateCraftSortArrows()
        local function SetArrowState(header, isActive)
            if isActive then
                header.arrow:Show()
                if craftSortAscending then
                    header.arrow:SetTexCoord(0, 1, 1, 0)
                else
                    header.arrow:SetTexCoord(0, 1, 0, 1)
                end
            else
                header.arrow:Hide()
            end
        end

        SetArrowState(craftTimeHeader, craftSortColumn == "timestamp")
        SetArrowState(craftItemHeader, craftSortColumn == "itemName")
        SetArrowState(craftPriceHeader, craftSortColumn == "itemPrice")
        SetArrowState(craftSourceHeader, craftSortColumn == "source")
        SetArrowState(craftCharHeader, craftSortColumn == "character")
    end

    -- Refresh crafting history list
    local function RefreshCraftingList()
        for _, row in pairs(craftRowPool) do
            row:Hide()
        end

        local HDG_ConfigTab = _G.HDG_ConfigTab
        if not HDG_ConfigTab then return end

        local history = HDG_DataTab.GetCraftingHistory()
        local historyYOffset = 0

        if #history == 0 then
            local row = GetCraftRow(1)
            row:SetPoint("TOPLEFT", 0, historyYOffset)
            row.timeText:SetText("")
            row.itemText:SetText(HDG.Theme.C_Dim() .. (L["STATUS_NO_HISTORY"] or "No crafting/acquisition history yet.") .. "|r")
            row.itemBtn.itemID = nil
            row.itemBtn.decorID = nil
            row.itemBtn.itemName = nil
            row.priceText:SetText("")
            row.sourceText:SetText("")
            row.charText:SetText("")
            row:Show()
            craftScrollChild:SetHeight(30)
            return
        end

        local sortedHistory = {}
        for i, entry in ipairs(history) do
            sortedHistory[i] = entry
        end

        table.sort(sortedHistory, function(a, b)
            local valA, valB
            if craftSortColumn == "timestamp" then
                valA = a.timestamp or 0
                valB = b.timestamp or 0
            elseif craftSortColumn == "itemName" then
                valA = (a.name or a.itemName or ""):lower()
                valB = (b.name or b.itemName or ""):lower()
            elseif craftSortColumn == "itemPrice" then
                valA = a.itemPrice or 0
                valB = b.itemPrice or 0
            elseif craftSortColumn == "source" then
                valA = (a.source or "Crafted"):lower()
                valB = (b.source or "Crafted"):lower()
            elseif craftSortColumn == "character" then
                valA = (a.character or ""):lower()
                valB = (b.character or ""):lower()
            end

            if craftSortAscending then
                return valA < valB
            else
                return valA > valB
            end
        end)

        for i, entry in ipairs(sortedHistory) do
            local row = GetCraftRow(i)
            row:SetPoint("TOPLEFT", 0, historyYOffset)

            local timeStr = HDG_DataTab.FormatTimestamp(entry.timestamp)
            row.timeText:SetText(HDG.Theme.C_Dim() .. timeStr .. "|r")

            local fallbackName = entry.name or entry.itemName or "Unknown"
            local itemName = entry.itemID and HDG_Data.GetLocalizedName(entry.itemID, fallbackName) or fallbackName
            local qty = entry.quantity or 1
            local qtyStr = qty > 1 and (" x" .. qty) or ""
            row.itemText:SetText(HDG.Theme.C_Text() .. itemName .. qtyStr .. "|r")
            row.itemBtn.itemID = entry.itemID
            row.itemBtn.decorID = entry.decorID
            row.itemBtn.itemName = itemName .. qtyStr

            -- Price display (item market value) with tooltip for both prices
            local priceDisplay = "-"
            if entry.itemPrice and entry.itemPrice > 0 then
                priceDisplay = HDG_PriceIntegration and HDG_PriceIntegration.FormatGold(entry.itemPrice) or string.format("%dg", math.floor(entry.itemPrice / 10000))
            end
            row.priceText:SetText(HDG.Theme.C_Accent() .. priceDisplay .. "|r")

            -- Store price data for tooltip
            row.materialCost = entry.materialCost
            row.itemPrice = entry.itemPrice
            row.priceSource = entry.priceSource

            -- Source display (Crafted = teal, Learned = gold)
            local sourceVal = entry.source or L["SOURCE_CRAFTED"] or "Crafted"
            if sourceVal == "Learned" or sourceVal == (L["SOURCE_LEARNED"] or "Learned") then
                row.sourceText:SetText(HDG.Theme.C_Warning() .. (L["SOURCE_LEARNED"] or "Learned") .. "|r")
            else
                row.sourceText:SetText(HDG.Theme.C_Accent() .. (L["SOURCE_CRAFTED"] or "Crafted") .. "|r")
            end

            local charName = entry.character or ""
            local realmName = entry.realm or ""
            local charDisplay = charName
            if realmName ~= "" then
                charDisplay = charName .. "-" .. realmName
            end
            row.charText:SetText(HDG.Theme.C_Accent() .. charDisplay .. "|r")

            row:Show()
            historyYOffset = historyYOffset - 18
        end

        craftScrollChild:SetHeight(math.abs(historyYOffset) + 10)
    end

    -- Column header click handlers for crafting
    local function OnCraftHeaderClick(header)
        if craftSortColumn == header.column then
            craftSortAscending = not craftSortAscending
        else
            craftSortColumn = header.column
            craftSortAscending = (header.column == "itemName" or header.column == "character")
        end
        UpdateCraftSortArrows()
        RefreshCraftingList()
    end

    craftTimeHeader:SetScript("OnClick", function() OnCraftHeaderClick(craftTimeHeader) end)
    craftItemHeader:SetScript("OnClick", function() OnCraftHeaderClick(craftItemHeader) end)
    craftPriceHeader:SetScript("OnClick", function() OnCraftHeaderClick(craftPriceHeader) end)
    craftSourceHeader:SetScript("OnClick", function() OnCraftHeaderClick(craftSourceHeader) end)
    craftCharHeader:SetScript("OnClick", function() OnCraftHeaderClick(craftCharHeader) end)

    UpdateCraftSortArrows()

    -- Store refresh function
    dataFrame.RefreshCraftingList = RefreshCraftingList
    mainFrame.RefreshHistoryList = RefreshCraftingList  -- Keep compatibility with ConfigTab logging

    yOffset = yOffset - 115

    -- Separator
    local separator = scrollChild:CreateTexture(nil, "ARTWORK")
    separator:SetHeight(1)
    separator:SetPoint("TOPLEFT", 4, yOffset)
    separator:SetPoint("TOPRIGHT", -4, yOffset)
    local sepScheme = GetScheme()
    if sepScheme then
        separator:SetColorTexture(sepScheme.border.r, sepScheme.border.g, sepScheme.border.b, 0.5)
    else
        separator:SetColorTexture(0.3, 0.3, 0.35, 0.5)
    end

    yOffset = yOffset - 15

    -- ========================================================================
    -- FARMING HISTORY SECTION
    -- ========================================================================
    local ftScheme = GetScheme()
    local farmingTitle = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    farmingTitle:SetPoint("TOPLEFT", 4, yOffset)
    ApplyFont(farmingTitle, ftScheme, "header")
    farmingTitle:SetText(L["HEADER_FARMING_HISTORY"] or "Farming History")
    if ftScheme then
        farmingTitle:SetTextColor(ftScheme.text_header.r, ftScheme.text_header.g, ftScheme.text_header.b, ftScheme.text_header.a)
    else
        farmingTitle:SetTextColor(0.99, 0.83, 0.22, 1)
    end
    farmingTitle.isHeader = true
    AddSectionStyle(farmingTitle, scrollChild)
    RegisterWidget(farmingTitle, "PanelTitle")

    -- Help text explaining how farming tracking works
    local farmingDesc = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    farmingDesc:SetPoint("TOPLEFT", 4, yOffset - 20)
    farmingDesc:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 120)
    farmingDesc:SetJustifyH("LEFT")
    ApplyFont(farmingDesc, ftScheme, "small")
    farmingDesc:SetText(HDG.Theme.C_Dim() .. (L["DATA_FARMING_HELP"] or "Sessions start when lumber enters your bags. Zone shows where the last lumber was collected. Reloading UI ends the session.") .. "|r")

    yOffset = yOffset - 18

    -- Delete Most Recent Button for farming
    local farmDeleteBtn = CreateFrame("Button", nil, scrollChild, "BackdropTemplate")
    farmDeleteBtn:SetSize(22, 22)
    farmDeleteBtn:SetPoint("TOPRIGHT", -4, yOffset)
    farmDeleteBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false, edgeSize = 1,
    })
    local delBtnScheme = GetScheme()
    if delBtnScheme then
        farmDeleteBtn:SetBackdropColor(delBtnScheme.error.r * 0.3, delBtnScheme.error.g * 0.3, delBtnScheme.error.b * 0.3, 0.8)
        farmDeleteBtn:SetBackdropBorderColor(delBtnScheme.error.r * 0.5, delBtnScheme.error.g * 0.5, delBtnScheme.error.b * 0.5, 0.8)
    end
    local farmDeleteText = farmDeleteBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    farmDeleteText:SetPoint("CENTER", 0, 0)
    ApplyFont(farmDeleteText, delBtnScheme, "body")
    farmDeleteText:SetText("|cFFFF6666X|r")
    farmDeleteBtn:SetScript("OnEnter", function(self)
        if delBtnScheme then
            self:SetBackdropColor(delBtnScheme.error.r * 0.5, delBtnScheme.error.g * 0.5, delBtnScheme.error.b * 0.5, 1)
        end
        GameTooltip:SetOwner(self, CONFIG.TOOLTIP.DEFAULT_ANCHOR)
        GameTooltip:SetText(L["DATA_TIP_DELETE_SESSION"], 1, 0.4, 0.4)
        GameTooltip:AddLine(L["DATA_TIP_DELETE_SESSION_DESC"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    farmDeleteBtn:SetScript("OnLeave", function(self)
        if delBtnScheme then
            self:SetBackdropColor(delBtnScheme.error.r * 0.3, delBtnScheme.error.g * 0.3, delBtnScheme.error.b * 0.3, 0.8)
        end
        GameTooltip:Hide()
    end)
    farmDeleteBtn:SetScript("OnClick", function()
        HDG_DataTab.DeleteMostRecentFarmingSession()
    end)

    -- Export CSV Button for farming
    local farmExportBtn = CreateFrame("Button", nil, scrollChild)
    farmExportBtn:SetSize(100, 22)
    farmExportBtn:SetPoint("TOPRIGHT", farmDeleteBtn, "TOPLEFT", -4, 0)
    local farmExportTex = farmExportBtn:CreateTexture(nil, "ARTWORK")
    farmExportTex:SetAllPoints()
    farmExportTex:SetTexture(CONFIG.ADDON_TEXTURES.CSV_EXPORT)
    farmExportBtn:SetScript("OnEnter", function() farmExportTex:SetAlpha(0.8) end)
    farmExportBtn:SetScript("OnLeave", function() farmExportTex:SetAlpha(1.0) end)

    farmExportBtn:SetScript("OnClick", function()
        local csv = HDG_DataTab.ExportFarmingToCSV()
        if csv then
            if not HDG_FarmingExportCtrl then
                HDG_FarmingExportCtrl = HDG_UIHelpers.CreateExportDialog()
            end
            HDG_FarmingExportCtrl:Show(csv, HDG.Theme.C_Accent() .. (L["EXPORT_FARMING_TITLE"] or "Export Farming History") .. "|r")
            local msg = L["DATA_EXPORTED_FARMING"] or "Exported %d farming entries."
            print(HDG.Theme.C_Accent() .. "[Housing Decor Guide]|r " .. string.format(msg, #HDG_DataTab.GetFarmingHistory()))
        end
    end)
    yOffset = yOffset - 25

    -- Farming column headers (uses module-level FARM_COL_* constants)
    local farmHeaders = {
        { text = "Date/Time", x = 4, width = FARM_COL_TIME },
        { text = "Lumber", x = 4 + FARM_COL_TIME, width = FARM_COL_LUMBER },
        { text = "Collected", x = 4 + FARM_COL_TIME + FARM_COL_LUMBER, width = FARM_COL_COLLECTED },
        { text = "Rate/hr", x = 4 + FARM_COL_TIME + FARM_COL_LUMBER + FARM_COL_COLLECTED, width = FARM_COL_RATE },
        { text = "Duration", x = 4 + FARM_COL_TIME + FARM_COL_LUMBER + FARM_COL_COLLECTED + FARM_COL_RATE, width = FARM_COL_DURATION },
        { text = "Zone", x = 4 + FARM_COL_TIME + FARM_COL_LUMBER + FARM_COL_COLLECTED + FARM_COL_RATE + FARM_COL_DURATION, width = FARM_COL_ZONE },
        { text = "Character", x = 4 + FARM_COL_TIME + FARM_COL_LUMBER + FARM_COL_COLLECTED + FARM_COL_RATE + FARM_COL_DURATION + FARM_COL_ZONE, width = FARM_COL_CHAR }
    }

    local farmHdrScheme = GetScheme()
    for _, header in ipairs(farmHeaders) do
        local headerText = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        headerText:SetPoint("TOPLEFT", header.x, yOffset)
        headerText:SetWidth(header.width)
        headerText:SetJustifyH("LEFT")
        ApplyFont(headerText, farmHdrScheme, "small")
        headerText:SetText(HDG.Theme.C_Warning() .. header.text .. "|r")
    end

    yOffset = yOffset - 20

    -- Farming history container
    local farmContainer = CreateFrame("Frame", nil, scrollChild, "BackdropTemplate")
    farmContainer:SetPoint("TOPLEFT", 4, yOffset)
    farmContainer:SetSize(CONFIG.MAIN_FRAME_WIDTH - 80, 110)
    farmContainer: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 farmScheme = GetScheme()
    if farmScheme then
        farmContainer:SetBackdropColor(farmScheme.panel.r, farmScheme.panel.g, farmScheme.panel.b, 0.9)
        farmContainer:SetBackdropBorderColor(farmScheme.border.r, farmScheme.border.g, farmScheme.border.b, farmScheme.border.a)
        RegisterWidget(farmContainer, "Panel")
    else
        farmContainer:SetBackdropColor(0.08, 0.08, 0.10, 0.9)
        farmContainer:SetBackdropBorderColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    end

    -- Farming scroll frame
    local farmScrollFrame = CreateFrame("ScrollFrame", nil, farmContainer, "UIPanelScrollFrameTemplate")
    farmScrollFrame:SetPoint("TOPLEFT", 5, -5)
    farmScrollFrame:SetPoint("BOTTOMRIGHT", -25, 5)

    local farmScrollChild = CreateFrame("Frame", nil, farmScrollFrame)
    farmScrollFrame:SetScrollChild(farmScrollChild)
    farmScrollChild:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 120)
    farmScrollChild:SetHeight(1)
    dataFrame.farmScrollChild = farmScrollChild

    HDG_UIHelpers.StyleScrollBar(farmScrollFrame)

    yOffset = yOffset - 125  -- Space for farming container

    -- ========================================================================
    -- DECOR COLLECTION SECTION
    -- ========================================================================
    local collSeparator = scrollChild:CreateTexture(nil, "ARTWORK")
    collSeparator:SetHeight(1)
    collSeparator:SetPoint("TOPLEFT", 4, yOffset)
    collSeparator:SetPoint("TOPRIGHT", -4, yOffset)
    local collSepScheme = GetScheme()
    if collSepScheme then
        collSeparator:SetColorTexture(collSepScheme.border.r, collSepScheme.border.g, collSepScheme.border.b, 0.5)
    else
        collSeparator:SetColorTexture(0.3, 0.3, 0.35, 0.5)
    end

    yOffset = yOffset - 15

    local dcScheme = GetScheme()
    local collectionTitle = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    collectionTitle:SetPoint("TOPLEFT", 4, yOffset)
    ApplyFont(collectionTitle, dcScheme, "header")
    collectionTitle:SetText(L["DATA_DECOR_COLLECTION"] or "Decor Collection")
    if dcScheme then
        collectionTitle:SetTextColor(dcScheme.text_header.r, dcScheme.text_header.g, dcScheme.text_header.b, dcScheme.text_header.a)
    else
        collectionTitle:SetTextColor(0.99, 0.83, 0.22, 1)
    end
    collectionTitle.isHeader = true
    AddSectionStyle(collectionTitle, scrollChild)
    RegisterWidget(collectionTitle, "PanelTitle")

    local collectionDesc = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    collectionDesc:SetPoint("TOPLEFT", 4, yOffset - 22)
    collectionDesc:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 180)
    collectionDesc:SetJustifyH("LEFT")
    ApplyFont(collectionDesc, dcScheme, "small")
    collectionDesc:SetText(HDG.Theme.C_Dim() .. (L["DATA_COLLECTION_DESC"] or "Overview of your owned decor items from the Housing Catalog.") .. "|r")

    -- Export CSV Button
    local collExportBtn = CreateFrame("Button", nil, scrollChild)
    collExportBtn:SetSize(100, 22)
    collExportBtn:SetPoint("TOPRIGHT", -4, yOffset)
    local collExportTex = collExportBtn:CreateTexture(nil, "ARTWORK")
    collExportTex:SetAllPoints()
    collExportTex:SetTexture(CONFIG.ADDON_TEXTURES.CSV_EXPORT)
    collExportBtn:SetScript("OnEnter", function(self)
        collExportTex:SetAlpha(0.8)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText(L["DATA_TIP_EXPORT_COLLECTION"], 1, 1, 1)
        GameTooltip:AddLine(L["DATA_TIP_EXPORT_COLLECTION_DESC"], 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    collExportBtn:SetScript("OnLeave", function()
        collExportTex:SetAlpha(1.0)
        GameTooltip:Hide()
    end)
    collExportBtn:SetScript("OnClick", function()
        local csv = HDG_DataTab.ExportDecorCollectionToCSV()
        if not csv then return end
        if not HDG_CollectionExportCtrl then
            HDG_CollectionExportCtrl = HDG_UIHelpers.CreateExportDialog()
        end
        HDG_CollectionExportCtrl:Show(csv, HDG.Theme.C_Accent() .. (L["DATA_DECOR_COLLECTION"] or "Decor Collection") .. "|r")
        local ownCount = 0
        for _, it in ipairs(decorCollectionCache) do
            if (it.numStored or 0) > 0 or (it.numPlaced or 0) > 0 then ownCount = ownCount + 1 end
        end
        local msg = L["DATA_EXPORTED_COLLECTION"] or "Exported %d items (%d owned)."
        print(HDG.Theme.C_Accent() .. "[Housing Decor Guide]|r " .. string.format(msg, #decorCollectionCache, ownCount))
    end)

    -- Refresh Button
    local collRefreshBtn = CreateFrame("Button", nil, scrollChild, "BackdropTemplate")
    collRefreshBtn:SetSize(80, 22)
    collRefreshBtn:SetPoint("RIGHT", collExportBtn, "LEFT", -8, 0)
    collRefreshBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
    local rScheme = GetScheme()
    if rScheme then
        collRefreshBtn:SetBackdropColor(rScheme.button_normal.r, rScheme.button_normal.g, rScheme.button_normal.b, 0.8)
        collRefreshBtn:SetBackdropBorderColor(rScheme.border.r, rScheme.border.g, rScheme.border.b, 1)
    else
        collRefreshBtn:SetBackdropColor(0.15, 0.15, 0.18, 0.8)
        collRefreshBtn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
    end
    local refreshText = collRefreshBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    refreshText:SetPoint("CENTER")
    refreshText:SetText(L["BTN_REFRESH"])
    ApplyFont(refreshText, rScheme, "small")
    -- Expose so the HDG_COLLECTION_READY listener can flip the label back
    -- after a Loading... state (event-driven, no UI timer).
    if dataFrame then dataFrame.refreshText = refreshText end
    collRefreshBtn:SetScript("OnEnter", function(self)
        if rScheme then
            self:SetBackdropColor(rScheme.button_hover.r, rScheme.button_hover.g, rScheme.button_hover.b, 1)
        end
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText(L["DATA_TIP_REFRESH_COLLECTION"], 1, 1, 1)
        GameTooltip:AddLine(L["DATA_TIP_REFRESH_COLLECTION_DESC"], 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    collRefreshBtn:SetScript("OnLeave", function(self)
        if rScheme then
            self:SetBackdropColor(rScheme.button_normal.r, rScheme.button_normal.g, rScheme.button_normal.b, 0.8)
        end
        GameTooltip:Hide()
    end)
    collRefreshBtn:SetScript("OnClick", function()
        -- Sync read from HDG_Data's central catalog. If the catalog isn't
        -- ready yet (cold start before owned-set has populated), kick the
        -- central searcher; the HDG_COLLECTION_READY listener below will
        -- both repaint the panel AND flip the button label back.
        if BuildCollectionSnapshot() then
            HDG_DataTab.RefreshCollectionDisplay()
        else
            refreshText:SetText(L["STATUS_LOADING"] or "Loading...")
            if HDG_Data and HDG_Data.RefreshCatalog then HDG_Data.RefreshCatalog() end
        end
    end)

    yOffset = yOffset - 45

    -- Stats Panel
    local statsPanel = CreateFrame("Frame", nil, scrollChild, "BackdropTemplate")
    statsPanel:SetPoint("TOPLEFT", 4, yOffset)
    statsPanel:SetSize(CONFIG.MAIN_FRAME_WIDTH - 80, 90)
    statsPanel:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
    local spScheme = GetScheme()
    if spScheme then
        statsPanel:SetBackdropColor(spScheme.panel.r, spScheme.panel.g, spScheme.panel.b, 0.6)
        statsPanel:SetBackdropBorderColor(spScheme.border.r, spScheme.border.g, spScheme.border.b, 0.5)
    else
        statsPanel:SetBackdropColor(0.1, 0.1, 0.12, 0.5)
        statsPanel:SetBackdropBorderColor(0.3, 0.3, 0.35, 0.5)
    end

    -- Row 1: Unique items, In Storage, Placed
    local uniqueLabel = statsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    uniqueLabel:SetPoint("TOPLEFT", 10, -8)
    ApplyFont(uniqueLabel, spScheme, "body")
    dataFrame.statsUniqueLabel = uniqueLabel

    local storedLabel = statsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    storedLabel:SetPoint("TOPLEFT", 180, -8)
    ApplyFont(storedLabel, spScheme, "body")
    dataFrame.statsStoredLabel = storedLabel

    local placedLabel = statsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    placedLabel:SetPoint("TOPLEFT", 330, -8)
    ApplyFont(placedLabel, spScheme, "body")
    dataFrame.statsPlacedLabel = placedLabel

    -- Row 2: Blizzard official counts
    local officialLabel = statsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    officialLabel:SetPoint("TOPLEFT", 10, -26)
    officialLabel:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 100)
    officialLabel:SetJustifyH("LEFT")
    ApplyFont(officialLabel, spScheme, "small")
    dataFrame.statsOfficialLabel = officialLabel

    -- Row 3: Category breakdown
    local categoryLabel = statsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    categoryLabel:SetPoint("TOPLEFT", 10, -42)
    categoryLabel:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 100)
    categoryLabel:SetJustifyH("LEFT")
    ApplyFont(categoryLabel, spScheme, "small")
    dataFrame.statsCategoryLabel = categoryLabel

    -- Update content parent height (scrollChild is now the contentParent wrapper)
    local cpHeight = math.abs(yOffset) + 150
    scrollChild:SetHeight(cpHeight)
    -- Initial achievement layout + real scrollChild height
    HDG_AchSection.Relayout()

    return dataFrame
end

function HDG_DataTab.RefreshFarmingDisplay()
    if not dataFrame or not dataFrame.farmScrollChild then return end

    local farmScrollChild = dataFrame.farmScrollChild

    -- Clear existing rows
    for _, child in pairs({farmScrollChild:GetChildren()}) do
        child:Hide()
        child:SetParent(nil)
    end

    local history = HDG_DataTab.GetFarmingHistory()
    local yOffset = 0
    local rowHeight = 20

    if #history == 0 then
        local emptyScheme = GetScheme()
        local emptyText = farmScrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        emptyText:SetPoint("TOPLEFT", 4, -20)
        emptyText:SetText(HDG.Theme.C_Dim() .. (L["DATA_EMPTY_FARMING"] or "No farming sessions recorded yet.\nStart farming lumber to see your history here!") .. "|r")
        ApplyFont(emptyText, emptyScheme, "body")
        farmScrollChild:SetHeight(60)
        return
    end

    for i, entry in ipairs(history) do
        local row = CreateFrame("Frame", nil, farmScrollChild, "BackdropTemplate")
        row:SetSize(farmScrollChild:GetWidth() - 10, rowHeight)
        row:SetPoint("TOPLEFT", 0, yOffset)
        row:SetBackdrop({
            bgFile = CONFIG.TEXTURES.WHITE_BG,
            tile = false,
            insets = { left = 0, right = 0, top = 0, bottom = 0 }
        })

        -- Alternate row colors
        local rowScheme = GetScheme()
        if i % 2 == 0 then
            if rowScheme then
                row:SetBackdropColor(rowScheme.panel.r, rowScheme.panel.g, rowScheme.panel.b, 0.3)
            else
                row:SetBackdropColor(0.15, 0.15, 0.18, 0.3)
            end
        else
            row:SetBackdropColor(0, 0, 0, 0)
        end

        -- Column x positions (calculated from module-level constants)
        local xTime = 0
        local xLumber = xTime + FARM_COL_TIME
        local xCollected = xLumber + FARM_COL_LUMBER
        local xRate = xCollected + FARM_COL_COLLECTED
        local xDuration = xRate + FARM_COL_RATE
        local xZone = xDuration + FARM_COL_DURATION
        local xChar = xZone + FARM_COL_ZONE

        -- Timestamp
        local timestamp = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        timestamp:SetPoint("LEFT", xTime, 0)
        timestamp:SetWidth(FARM_COL_TIME)
        timestamp:SetJustifyH("LEFT")
        timestamp:SetWordWrap(false)
        timestamp:SetText(HDG_DataTab.FormatTimestamp(entry.timestamp))
        ApplyFont(timestamp, rowScheme, "small")

        -- Lumber name (colored by expansion if we can find it)
        local lumberText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lumberText:SetPoint("LEFT", xLumber, 0)
        lumberText:SetWidth(FARM_COL_LUMBER)
        lumberText:SetJustifyH("LEFT")
        lumberText:SetWordWrap(false)
        ApplyFont(lumberText, rowScheme, "small")
        local lumberInfo = LUMBER_TYPES and LUMBER_TYPES[entry.lumberId]
        local localizedLumberName = HDG_ExpansionData.GetLumberMaterialName(entry.lumberId)
        if lumberInfo then
            local expColor = HDG_ExpansionData.GetColor(lumberInfo.expansion)
            lumberText:SetText(string.format("|cFF%02x%02x%02x%s|r",
                math.min(255, expColor[1] * 255),
                math.min(255, expColor[2] * 255),
                math.min(255, expColor[3] * 255),
                localizedLumberName or lumberInfo.shortName or lumberInfo.name or entry.lumberName))
        else
            lumberText:SetText(localizedLumberName or entry.lumberName or L["MISC_UNKNOWN"] or "Unknown")
        end

        -- Collected amount
        local collected = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        collected:SetPoint("LEFT", xCollected, 0)
        collected:SetWidth(FARM_COL_COLLECTED)
        collected:SetJustifyH("LEFT")
        collected:SetWordWrap(false)
        collected:SetText(HDG.Theme.C_Success() .. (entry.collected or 0) .. "|r")
        ApplyFont(collected, rowScheme, "small")

        -- Rate per hour
        local rate = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        rate:SetPoint("LEFT", xRate, 0)
        rate:SetWidth(FARM_COL_RATE)
        rate:SetJustifyH("LEFT")
        rate:SetWordWrap(false)
        rate:SetText(HDG.Theme.C_Warning() .. string.format("%.1f|r", entry.perHour or 0))
        ApplyFont(rate, rowScheme, "small")

        -- Duration
        local duration = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        duration:SetPoint("LEFT", xDuration, 0)
        duration:SetWidth(FARM_COL_DURATION)
        duration:SetJustifyH("LEFT")
        duration:SetWordWrap(false)
        duration:SetText(FormatDuration(entry.duration))
        ApplyFont(duration, rowScheme, "small")

        -- Zone
        local zone = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        zone:SetPoint("LEFT", xZone, 0)
        zone:SetWidth(FARM_COL_ZONE)
        zone:SetJustifyH("LEFT")
        zone:SetWordWrap(false)
        zone:SetText(HDG.Theme.C_Accent() .. (entry.zone or L["MISC_UNKNOWN"] or "Unknown") .. "|r")
        ApplyFont(zone, rowScheme, "small")

        -- Character name (last column)
        local charText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        charText:SetPoint("LEFT", xChar, 0)
        charText:SetWidth(FARM_COL_CHAR)
        charText:SetJustifyH("LEFT")
        charText:SetWordWrap(false)
        ApplyFont(charText, rowScheme, "small")
        local charDisplay = entry.character or ""
        if entry.realm and entry.realm ~= "" then
            charDisplay = charDisplay .. "-" .. entry.realm
        end
        -- Truncate if too long
        if #charDisplay > 20 then
            charDisplay = charDisplay:sub(1, 19) .. "..."
        end
        charText:SetText(HDG.Theme.C_Accent() .. charDisplay .. "|r")

        yOffset = yOffset - rowHeight
    end

    farmScrollChild:SetHeight(math.abs(yOffset) + 10)
end

-- Delegate to extracted achievement module
function HDG_DataTab.RefreshAchievementsDisplay()
    local achSection = privateDB.HDG_AchievementSection
    if achSection then achSection.Refresh() end
end

function HDG_DataTab.RelayoutAchievements()
    local achSection = privateDB.HDG_AchievementSection
    if achSection then achSection.Relayout() end
end

function HDG_DataTab.RefreshCollectionDisplay()
    if not dataFrame then return end

    -- Cache populated? Use it. Invalidated via HDG_COLLECTION_READY listener
    -- (event-driven, no TTL).
    local hasData = decorCollectionCache and #decorCollectionCache > 0

    if not hasData then
        if dataFrame.statsUniqueLabel then dataFrame.statsUniqueLabel:SetText(HDG.Theme.C_Dim() .. (L["DATA_STAT_TOTAL"] or "Total: ") .. "|r--") end
        if dataFrame.statsStoredLabel then dataFrame.statsStoredLabel:SetText(HDG.Theme.C_Dim() .. (L["DATA_STAT_OWNED"] or "Owned: ") .. "|r--") end
        if dataFrame.statsPlacedLabel then dataFrame.statsPlacedLabel:SetText(HDG.Theme.C_Dim() .. (L["DATA_STAT_PLACED"] or "Placed: ") .. "|r--") end
        if dataFrame.statsOfficialLabel then dataFrame.statsOfficialLabel:SetText("") end
        if dataFrame.statsCategoryLabel then dataFrame.statsCategoryLabel:SetText(HDG.Theme.C_Dim() .. (L["DATA_CLICK_REFRESH"] or "Click Refresh to load data") .. "|r") end
        return
    end

    -- Calculate statistics
    local totalItems = #decorCollectionCache
    local ownedUnique = 0
    local totalStored = 0
    local totalPlaced = 0
    local categoryBreakdown = {}

    for _, item in ipairs(decorCollectionCache) do
        local stored = item.numStored or 0
        local placed = item.numPlaced or 0
        totalStored = totalStored + stored
        totalPlaced = totalPlaced + placed
        if stored > 0 or placed > 0 then ownedUnique = ownedUnique + 1 end
        local cat = item.categoryName or "Uncategorized"
        categoryBreakdown[cat] = (categoryBreakdown[cat] or 0) + 1
    end

    -- Official Blizzard counts from cache
    local cache = decorCollectionCache
    local totalOwned = cache and cache.totalOwned or 0
    local exemptOwned = cache and cache.exemptOwned or 0

    -- Row 1: Core stats
    if dataFrame.statsUniqueLabel then
        dataFrame.statsUniqueLabel:SetText(HDG.Theme.C_Accent() .. (L["DATA_STAT_TOTAL"] or "Total: ") .. HDG.Theme.C_Text() .. totalItems .. "|r")
    end
    if dataFrame.statsStoredLabel then
        dataFrame.statsStoredLabel:SetText(HDG.Theme.C_Success() .. (L["DATA_STAT_OWNED"] or "Owned: ") .. HDG.Theme.C_Text() .. ownedUnique .. "|r")
    end
    if dataFrame.statsPlacedLabel then
        dataFrame.statsPlacedLabel:SetText(HDG.Theme.C_Warning() .. (L["DATA_STAT_PLACED"] or "Placed: ") .. HDG.Theme.C_Text() .. totalPlaced .. "|r")
    end

    -- Row 2: Blizzard official counts
    if dataFrame.statsOfficialLabel then
        local parts = {}
        table.insert(parts, (L["DATA_STAT_STORAGE"] or "Storage: ") .. totalStored)
        if totalOwned > 0 then table.insert(parts, "Blizzard Total: " .. totalOwned) end
        if exemptOwned > 0 then table.insert(parts, "Exempt: " .. exemptOwned) end
        dataFrame.statsOfficialLabel:SetText(HDG.Theme.C_Dim() .. table.concat(parts, "  |  ") .. "|r")
    end

    -- Build top 5 categories
    local sortedCats = {}
    for cat, count in pairs(categoryBreakdown) do
        table.insert(sortedCats, { name = cat, count = count })
    end
    table.sort(sortedCats, function(a, b) return a.count > b.count end)

    local catParts = {}
    for i = 1, math.min(5, #sortedCats) do
        table.insert(catParts, string.format("%s (%d)", sortedCats[i].name, sortedCats[i].count))
    end

    if dataFrame.statsCategoryLabel then
        if #catParts > 0 then
            dataFrame.statsCategoryLabel:SetText(HDG.Theme.C_Dim() .. L["DATA_TOP_CATEGORIES"] .. "|r" .. table.concat(catParts, ", "))
        else
            dataFrame.statsCategoryLabel:SetText(HDG.Theme.C_Dim() .. L["DATA_TOP_CATEGORIES"] .. "|r" .. L["DATA_NO_CATEGORIES"])
        end
    end
end

function HDG_DataTab.Show()
    if not dataFrame then
        HDG_DataTab.CreatePanel()
    end
    if dataFrame then
        if dataFrame.RefreshCraftingList then
            dataFrame.RefreshCraftingList()
        end
        HDG_DataTab.RefreshAchievementsDisplay()
        HDG_DataTab.RefreshFarmingDisplay()
        if not decorCollectionCache then BuildCollectionSnapshot() end
        HDG_DataTab.RefreshCollectionDisplay()
        dataFrame:Show()
    end
end

function HDG_DataTab.Hide()
    if dataFrame then
        dataFrame:Hide()
    end
end

-- ============================================================================
-- EVENTBUS SUBSCRIPTIONS
-- Decouple UI refresh from direct function calls
-- ============================================================================

local function RegisterEventBusHandlers()
    local HDG = _G.HousingDecorGuide
    if not HDG or not HDG.EventBus then return end

    HDG.EventBus:Register("HDG_STATE_CHANGED", function(payload)
        if not payload or not payload.action then return end

        local action = payload.action

        -- Crafting history changes
        if action == "ADD_CRAFTING_HISTORY" or action == "CLEAR_CRAFTING_HISTORY" then
            if dataFrame and dataFrame.RefreshCraftingList then
                dataFrame.RefreshCraftingList()
            end
        end

        -- Farming history changes
        if action == "UPSERT_FARMING_SESSION" or action == "CLEAR_FARMING_HISTORY" then
            HDG_DataTab.RefreshFarmingDisplay()
        end
    end)

    -- Rebuild the snapshot when the central owned set changes (background
    -- HOUSING_STORAGE_* updates picked up by HDG_Data). Cheap — pure
    -- iteration over GetAllCatalog, no API round-trip.
    HDG.EventBus:Register("HDG_COLLECTION_READY", function()
        BuildCollectionSnapshot()
        if dataFrame and dataFrame.refreshText then
            dataFrame.refreshText:SetText(L["BTN_REFRESH"])  -- clear any "Loading..." state
        end
        if dataFrame and dataFrame:IsShown() and HDG_DataTab.RefreshCollectionDisplay then
            HDG_DataTab.RefreshCollectionDisplay()
        end
    end)
end

-- Register handlers after a short delay to ensure EventBus is ready
RegisterEventBusHandlers()

-- ============================================================================
-- EXPORT TO GLOBAL
-- ============================================================================

_G.HDG_DataTab = HDG_DataTab

return HDG_DataTab
