--

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

HDG_LumberTracker = {}

-- ============================================================================
-- THEME HELPERS (LumberTracker always uses HDGDefault theme)
-- ============================================================================

local HDG = _G.HousingDecorGuide
-- LumberTracker widget always uses HDGDefault theme regardless of main addon theme
local function GetScheme() return HDG.Colors:GetScheme("HDGDefault") end
local function RegisterWidget(_, _) end  -- No-op: don't register with theme engine (fixed theme)
local ApplyFont = function(fs, scheme, fontType)
    -- Use HDGDefault fonts directly
    local s = scheme or GetScheme()
    if s and s.fonts and s.fonts[fontType] then
        local fontDef = s.fonts[fontType]
        fs:SetFont(fontDef.file, fontDef.size, fontDef.flags or "")
    end
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 GetConfig(key)
    local state = GetState()
    return state and state.config and state.config[key]
end

local function GetUIState(key)
    local state = GetState()
    return state and state.ui and state.ui[key]
end

local function GetShoppingList()
    local state = GetState()
    return state and state.shoppingList or {}
end

local function GetTrackedReagents()
    local state = GetState()
    return state and state.trackedReagents or {}
end

-- Local references
local mainFrame
local lumberFrame
local lumberContent
local pinnedFrame  -- Independent frame parented to UIParent (persists when main window closes)
local focusFrame   -- Minimal focus widget shown during active farming (replaces pinned when focus mode enabled)

-- Session-only farming stats (not saved, resets on logout/reload)
local sessionFarmingStats = {}  -- [lumberId] = { startCount, startTime, collected, zone, sessionId, lastCollectTime }
local previousLumberCounts = {}  -- Track previous counts to detect increases
local activeFarmingLumber = nil  -- Currently highlighted lumber ID
local cachedShoppingLumber = nil -- Cached result of CalculateShoppingListLumber
local cachedAllLumber = nil      -- Cached result of CalculateLumberNeeds
local lumberNeedsDirty = true    -- Invalidation flag for lumber needs cache
local cachedNonLumberMaterials = nil   -- Cached result of CalculateNonLumberMaterials
local cachedRawNonLumberMaterials = nil -- Cached result of CalculateRawNonLumberMaterials
local materialsCacheDirty = true       -- Invalidation flag for non-lumber materials cache
local pinnedDisplayState = nil   -- Snapshot of last-displayed state to skip redundant UI rebuilds
local pinnedLumberRows = {}      -- [lumberId] = pooled row frame (reused across refreshes)
local justCollectedLumber = nil  -- Animation: lumber that was just collected (triggers pop animation)
local animationPhase = 0         -- Animation phase: 0=none, 1=grow, 2=shrink
local animationTimer = nil       -- Timer handle for clearing animation state

-- Track when item-moving UIs are open to prevent false farming sessions
local uiSuspended = false  -- True when mail, bank, merchant, or trade is open
local isSessionInitialized = false  -- Prevents false collection detection during login
local initTimer = nil  -- Guard against stacking init timers
local initStartTime = 0  -- GetTime() when init was requested (for lazy fallback)

-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================

-- Use centralized Data() accessor from UIHelpers
local function Data()
    return HDG_UIHelpers.Data()
end

-- Format elapsed time as H:MM:SS or M:SS
local function FormatElapsedTime(seconds)
    if not seconds or seconds < 0 then seconds = 0 end
    local hours = math.floor(seconds / 3600)
    local mins = math.floor((seconds % 3600) / 60)
    local secs = math.floor(seconds % 60)
    if hours > 0 then
        return string.format("%d:%02d:%02d", hours, mins, secs)
    else
        return string.format("%d:%02d", mins, secs)
    end
end

-- Reference centralized lumber data
local LUMBER_TYPES = HDG_ExpansionData.LUMBER_TYPES

-- ============================================================================
-- SESSION FARMING TRACKING
-- ============================================================================

-- Initialize previous lumber counts (call on addon load)
local function InitializeLumberCounts()
    for lumberId, _ in pairs(LUMBER_TYPES) do
        previousLumberCounts[lumberId] = Data().GetItemCount(lumberId) or 0
    end
end

-- Clear all session state (called on init)
local function ClearAllSessionState()
    activeFarmingLumber = nil
    justCollectedLumber = nil
    animationPhase = 0
    if animationTimer then
        animationTimer:Cancel()
        animationTimer = nil
    end
end

-- Save lumber gathering coordinates for later analysis (heatmaps, route optimization, etc.)
local function SaveLumberCoordinates(lumberId, quantity)
    -- Initialize storage if needed
    if not HDG_Settings.lumberCoordinates then
        HDG_Settings.lumberCoordinates = {}
    end

    -- Get current position
    local mapID = C_Map.GetBestMapForUnit("player")
    if not mapID then return end

    local position = C_Map.GetPlayerMapPosition(mapID, "player")
    if not position then return end

    local x, y = position:GetXY()
    if not x or not y then return end

    -- Get zone info
    local zone = GetZoneText() or "Unknown"
    local subzone = GetSubZoneText() or ""

    -- Create coordinate entry
    local entry = {
        lumberId = lumberId,
        quantity = quantity or 1,
        mapID = mapID,
        x = math.floor(x * 10000) / 10000,  -- 4 decimal places (0.01% precision)
        y = math.floor(y * 10000) / 10000,
        zone = zone,
        subzone = subzone,
        timestamp = time(),
        character = UnitName("player"),
        realm = GetRealmName()
    }

    table.insert(HDG_Settings.lumberCoordinates, entry)

    -- Batch prune when 20+ over limit (efficient: O(MAX) table reconstruction vs O(n) per remove)
    local maxCoords = CONFIG.MAX_LUMBER_COORDINATES or 500
    local coords = HDG_Settings.lumberCoordinates
    if #coords > maxCoords + 20 then
        local newCoords = {}
        for i = #coords - maxCoords + 1, #coords do
            newCoords[#newCoords + 1] = coords[i]
        end
        HDG_Settings.lumberCoordinates = newCoords
    end

    -- Notify LumberHUD of new gathering location (auto-show compass)
    if privateDB.HDG_LumberHUD and privateDB.HDG_LumberHUD.OnLocationAdded then
        privateDB.HDG_LumberHUD.OnLocationAdded(x, y, mapID)
    end
end

-- Save or update a farming session to history (upsert)
local function SaveFarmingSession(lumberId)
    local stats = sessionFarmingStats[lumberId]
    if not stats or stats.collected == 0 then return end

    local lumberInfo = LUMBER_TYPES[lumberId]
    if not lumberInfo then return end

    local now = GetTime()
    local duration = now - stats.startTime
    local perHour = duration > 0 and (stats.collected / duration) * 3600 or 0

    -- Get HDG_DataTab to save/update the session
    local HDG_DataTab = privateDB and privateDB.HDG_DataTab
    if HDG_DataTab and HDG_DataTab.SaveFarmingSession then
        HDG_DataTab.SaveFarmingSession({
            sessionId = stats.sessionId,
            lumberId = lumberId,
            lumberName = HDG_ExpansionData.GetLumberMaterialName(lumberId) or lumberInfo.shortName or lumberInfo.name,
            collected = stats.collected,
            duration = duration,
            perHour = perHour,
            zone = stats.zone or "Unknown",
            timestamp = time(),
            character = UnitName("player"),
            realm = GetRealmName()
        })
    end
end

-- Check for lumber collection increases and update farming stats
-- Sessions only start fresh on reload/login - zone changes don't split sessions
local function CheckLumberChanges()
    -- Skip if session tracking hasn't been initialized yet (prevents false detection on login)
    if not isSessionInitialized then
        -- Lazy init fallback: if 4+ seconds have passed since init was requested,
        -- the timer may have been lost (rare edge case) — force init now
        if initStartTime > 0 and (GetTime() - initStartTime) > 4 then
            InitializeLumberCounts()
            isSessionInitialized = true
        else
            return nil
        end
    end

    -- Skip farming tracking if lumber came from mail, bank, merchant, or trade
    if uiSuspended then
        -- Still update previous counts so we don't trigger on close
        for lumberId, _ in pairs(LUMBER_TYPES) do
            previousLumberCounts[lumberId] = Data().GetItemCount(lumberId) or 0
        end
        return nil
    end

    local now = GetTime()
    local changedLumber = nil
    local currentZone = GetZoneText() or "Unknown"

    for lumberId, _ in pairs(LUMBER_TYPES) do
        local currentCount = Data().GetItemCount(lumberId) or 0
        local previousCount = previousLumberCounts[lumberId] or 0

        if currentCount > previousCount then
            local increase = currentCount - previousCount
            changedLumber = lumberId

            -- Initialize session if this is first collection of this lumber type this session
            if not sessionFarmingStats[lumberId] then
                sessionFarmingStats[lumberId] = {
                    startCount = previousCount,
                    startTime = now,
                    collected = 0,
                    zone = currentZone,  -- Record zone where farming started
                    sessionId = lumberId .. "_" .. time(),
                    lastCollectTime = now
                }
            end

            -- Update stats (including zone - always track current location)
            sessionFarmingStats[lumberId].collected = sessionFarmingStats[lumberId].collected + increase
            sessionFarmingStats[lumberId].lastCollectTime = now
            sessionFarmingStats[lumberId].zone = currentZone  -- Update zone to current location

            -- Save/update the session to history immediately
            SaveFarmingSession(lumberId)

            -- Save coordinates for later analysis (heatmaps, route optimization)
            SaveLumberCoordinates(lumberId, increase)

            -- Always set active farming lumber (allows title click to show focus tracker)
            activeFarmingLumber = lumberId

            -- Focus mode / auto-show only activates if the setting is enabled
            -- When disabled, lumber tracking still happens but no focus mode visual changes
            local autoShowEnabled = (GetConfig("lumberAutoShow") == true)
            if autoShowEnabled then
                -- Check if focus mode is enabled - use focus widget instead of pinned
                local focusModeEnabled = GetConfig("lumberFocusMode")
                if focusModeEnabled then
                    -- Show focus widget (hides pinned widget)
                    if not HDG_LumberTracker.IsFocusVisible() then
                        HDG_LumberTracker.ShowFocus()
                    else
                        HDG_LumberTracker.RefreshFocus()
                    end
                else
                    -- Show pinned widget (no focus mode)
                    if not HDG_LumberTracker.IsPinnedVisible() then
                        HDG_LumberTracker.ShowPinned()
                    end
                end
            end

            -- Vamoose quotes - random quotes while farming (0 = off)
            -- Check both master toggle and lumber quotes toggle
            local quotesEnabled = GetConfig("quotesEnabled")
            if quotesEnabled == nil then quotesEnabled = true end
            local lumberQuotesEnabled = GetConfig("lumberQuotesEnabled")
            if lumberQuotesEnabled == nil then lumberQuotesEnabled = true end
            local chattiness = GetConfig("vamooseChattiness") or 30
            if quotesEnabled and lumberQuotesEnabled and chattiness > 0 and privateDB.HDG_Vamoose then
                local quote, speaker = privateDB.HDG_Vamoose.OnLumberCollected(lumberId, increase, chattiness)
                if quote then
                    local onlyChat = GetConfig("quotesOnlyChat") or false
                    -- Show talking head unless "only chat" is enabled
                    if not onlyChat then
                        privateDB.HDG_Vamoose.DisplayQuote(quote, speaker)
                    end
                    -- Print to chat if "only chat" is enabled (always print when chat-only mode)
                    if onlyChat then
                        local speakerName, speakerColor
                        if speaker == "GOBLIN" then
                            speakerName = "Gazlowe"
                            speakerColor = "FFD100"  -- Gold for goblin
                        elseif speaker == "SQUIRREL" then
                            speakerName = "Squirrel"
                            speakerColor = "A0522D"  -- Brown for squirrel
                        else
                            speakerName = "Vamoose"
                            speakerColor = "0070DE"  -- Shaman blue
                        end
                        print(string.format("|cFF%s[%s]|r %s", speakerColor, speakerName, quote))
                    end
                end
            end

            -- Trigger collection animation (3-phase: grow -> hold -> shrink)
            justCollectedLumber = lumberId
            animationPhase = 1  -- Start with grow phase
            if animationTimer then
                animationTimer:Cancel()
            end
            -- Helper to refresh whichever widget is visible
            local function RefreshVisibleWidget()
                if HDG_LumberTracker.IsFocusVisible() then
                    HDG_LumberTracker.RefreshFocus()
                elseif HDG_LumberTracker.IsPinnedVisible() then
                    HDG_LumberTracker.RefreshPinned()
                end
            end
            -- Refresh immediately to show phase 1 (large)
            RefreshVisibleWidget()
            -- Phase 2: Hold at large size briefly
            animationTimer = C_Timer.NewTimer(0.2, function()
                animationPhase = 2  -- Shrink phase
                RefreshVisibleWidget()
                -- Phase 3: Return to normal
                animationTimer = C_Timer.NewTimer(0.15, function()
                    justCollectedLumber = nil
                    animationPhase = 0
                    RefreshVisibleWidget()
                end)
            end)
        end

        -- Update previous count
        previousLumberCounts[lumberId] = currentCount
    end

    return changedLumber
end

-- Get farming stats for a lumber type
local function GetFarmingStats(lumberId)
    local stats = sessionFarmingStats[lumberId]
    if not stats or stats.collected == 0 then
        return nil
    end

    local now = GetTime()
    local elapsed = now - stats.startTime
    local perHour = 0

    if elapsed > 0 then
        perHour = (stats.collected / elapsed) * 3600  -- Convert to per hour
    end

    return {
        collected = stats.collected,
        elapsed = elapsed,
        perHour = perHour
    }
end

-- ============================================================================
-- LUMBER CALCULATION FUNCTIONS
-- These are the primary implementations; HDG_Data delegates to these
-- ============================================================================

-- Calculate lumber needs from all uncollected recipes
-- Uses account-wide lumber counts (summed across all characters)
function HDG_LumberTracker.CalculateLumberNeeds(professions)
    local lumberNeeds = {}

    for _, profData in ipairs(professions) do
        for _, recipe in ipairs(profData.items) do
            if not Data().IsItemCollected(recipe.id) and recipe.reagents then
                for reagentKey, reagent in pairs(recipe.reagents) do
                    local reagentID = reagent.id or reagentKey
                    if LUMBER_TYPES[reagentID] then
                        if not lumberNeeds[reagentID] then
                            -- Use account-wide lumber count instead of just current character
                            local accountTotal, breakdown = Data().GetAccountWideLumberCount(reagentID)
                            lumberNeeds[reagentID] = {
                                id = reagentID,
                                name = LUMBER_TYPES[reagentID].name,
                                expansion = LUMBER_TYPES[reagentID].expansion,
                                needed = 0,
                                have = accountTotal,
                                breakdown = breakdown  -- Per-character breakdown for tooltip
                            }
                        end
                        lumberNeeds[reagentID].needed = lumberNeeds[reagentID].needed +
                            (reagent.amount or reagent.qty or 1)
                    end
                end
            end
        end
    end

    return lumberNeeds
end

-- Calculate lumber needs from shopping list only
-- Uses account-wide lumber counts (summed across all characters)
function HDG_LumberTracker.CalculateShoppingListLumber(shoppingList)
    local lumberNeeds = {}

    for i, item in ipairs(shoppingList) do
        if item.reagents then
            for reagentKey, reagent in pairs(item.reagents) do
                local reagentID = reagent.id or reagentKey
                if LUMBER_TYPES[reagentID] then
                    if not lumberNeeds[reagentID] then
                        -- Use account-wide lumber count instead of just current character
                        local accountTotal, breakdown = Data().GetAccountWideLumberCount(reagentID)
                        lumberNeeds[reagentID] = {
                            id = reagentID,
                            name = LUMBER_TYPES[reagentID].name,
                            needed = 0,
                            have = accountTotal,
                            breakdown = breakdown,  -- Per-character breakdown for tooltip
                            sources = {}
                        }
                    end

                    local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                    lumberNeeds[reagentID].needed = lumberNeeds[reagentID].needed + (reagentAmount * item.quantity)
                    lumberNeeds[reagentID].sources[i] = true
                end
            end
        end
    end

    return lumberNeeds
end

function HDG_LumberTracker.InvalidateLumberNeeds()
    lumberNeedsDirty = true
    materialsCacheDirty = true
end

function HDG_LumberTracker.GetCachedLumberData()
    return cachedShoppingLumber, cachedAllLumber
end

function HDG_LumberTracker.GetCachedLumberData()
    return cachedShoppingLumber, cachedAllLumber
end

local function CreateProgressBar(parent, have, needed, xPos, yPos, showText)
    if not CONFIG.PROGRESS_BAR.SHOW_BARS then return nil end

    local bar = CreateFrame("StatusBar", nil, parent, "BackdropTemplate")
    bar:SetSize(CONFIG.PROGRESS_BAR.WIDTH, CONFIG.PROGRESS_BAR.HEIGHT)
    bar:SetPoint("RIGHT", xPos or -60, yPos or 0)
    bar:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    bar:SetMinMaxValues(0, needed)
    bar:SetValue(math.min(have, needed))

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

    local scheme = GetScheme()
    local bg = bar:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(bar)
    bg:SetTexture(CONFIG.TEXTURES.STATUSBAR)
    bg:SetVertexColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.7)

    local overlay = bar:CreateTexture(nil, "OVERLAY")
    overlay:SetAllPoints(bar)
    overlay:SetTexture(CONFIG.TEXTURES.STATUSBAR)
    local overlayScheme = GetScheme()
    if overlayScheme then
        overlay:SetVertexColor(overlayScheme.accent.r, overlayScheme.accent.g, overlayScheme.accent.b, 0.15)
    else
        overlay:SetVertexColor(0.06, 0.73, 0.51, 0.15)
    end
    overlay:SetDrawLayer("OVERLAY", 1)

    if showText then
        local text = bar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        text:SetPoint("CENTER", 0, 0)
        text:SetJustifyH("CENTER")
        text:SetDrawLayer("OVERLAY", 2)
        ApplyFont(text, scheme, "small")

        if have >= needed then
            local extra = have - needed
            text:SetText(extra > 0 and string.format("|cFFFFFFFF%d/%d+%d|r", needed, needed, extra) or
                string.format("|cFFFFFFFF%d/%d|r", needed, needed))
        else
            text:SetText(string.format("|cFFFFFFFF%d/%d|r", have, needed))
        end
        bar.text = text
    end

    return bar
end

-- Handle end session callback for farming tracking
local function HandleEndSession(lumberId)
    local stats = sessionFarmingStats[lumberId]
    if stats and stats.collected > 0 then
        local duration = (stats.lastCollectTime or GetTime()) - stats.startTime
        local perHour = duration > 0 and (stats.collected / duration) * 3600 or 0
        local lumberInfo = LUMBER_TYPES[lumberId]
        local lumberName = HDG_ExpansionData.GetLumberMaterialName(lumberId) or (lumberInfo and lumberInfo.shortName) or (lumberInfo and lumberInfo.name) or "Lumber"
        local expColor = HDG_ExpansionData.GetColor(lumberInfo and lumberInfo.expansion)
        local colorHex = string.format("%02x%02x%02x",
            math.min(255, expColor[1] * 255 * 1.3),
            math.min(255, expColor[2] * 255 * 1.3),
            math.min(255, expColor[3] * 255 * 1.3))
        print(string.format("|cFFfcd34d[Housing Decor Guide]|r |cFF%s%s|r farming session ended: |cFF88FF88%d|r collected in %s |cFF888888(first to last)|r (|cFFfcd34d%.1f/hr|r)",
            colorHex, lumberName, stats.collected, FormatElapsedTime(duration), perHour))
    end
    -- Clear session stats and all session state (including timeout timer)
    sessionFarmingStats[lumberId] = nil
    ClearAllSessionState()
    -- Hide the lumber radar when session ends
    local HDG_LumberHUD = privateDB and privateDB.HDG_LumberHUD
    if HDG_LumberHUD and HDG_LumberHUD.Hide then
        HDG_LumberHUD.Hide()
    end
    -- Hide focus widget and restore pinned widget
    if HDG_LumberTracker.IsFocusVisible() then
        HDG_LumberTracker.HideFocus()  -- This also restores pinned widget
    elseif HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.RefreshPinned()
    end
    local HDG_DataTab = privateDB and privateDB.HDG_DataTab
    if HDG_DataTab and HDG_DataTab.RefreshFarmingDisplay then
        HDG_DataTab.RefreshFarmingDisplay()
    end
end

-- Helper: Create a lumber row using shared UIHelpers function
-- Returns: nextYOffset (accounts for expanded row height if farming active)
local function CreateLumberRow(parent, lumberId, lumberInfo, shoppingData, allData, yOffset)
    local isActiveFarming = (activeFarmingLumber == lumberId)
    local isJustCollected = (justCollectedLumber == lumberId)
    local farmingStats = GetFarmingStats(lumberId)

    -- Build farming options for the shared helper
    -- Always pass scheme override so LumberTracker widget uses HDGDefault theme
    local farmingOptions = {
        scheme = GetScheme(),  -- Uses HDGDefault (fixed in this module)
        isActive = isActiveFarming,
        isJustCollected = isJustCollected,
        animationPhase = animationPhase,
        stats = (isActiveFarming or isJustCollected) and farmingStats and {
            collected = farmingStats.collected,
            perHour = farmingStats.perHour,
            elapsed = farmingStats.elapsed
        } or nil
    }

    return HDG_UIHelpers.CreateLumberRow(parent, lumberId, lumberInfo, shoppingData, allData, yOffset, farmingOptions)
end

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

function HDG_LumberTracker.Init(frame)
    mainFrame = frame
end

-- Initialize lumber tracking on addon load (called from main entry point)
function HDG_LumberTracker.InitializeSessionTracking()
    -- Clear any stale session state from previous session
    -- This ensures fresh start on login/reload
    ClearAllSessionState()
    sessionFarmingStats = {}
    uiSuspended = false  -- Reset suspend state on fresh init
    isSessionInitialized = false  -- Block CheckLumberChanges until counts are initialized
    initStartTime = GetTime()

    -- Cancel any previous init timer to prevent stacking
    if initTimer then
        initTimer:Cancel()
        initTimer = nil
    end

    -- Initialize lumber counts for session tracking
    -- Delay to ensure bags/bank data is fully loaded (matches SaveCharacterLumberInventory timing)
    -- This prevents false "collection" detection on first login when bags load
    initTimer = C_Timer.NewTimer(3.5, function()
        initTimer = nil
        InitializeLumberCounts()
        isSessionInitialized = true  -- Now safe to detect lumber changes
    end)
end

-- Save all active farming sessions (called on logout/reload)
function HDG_LumberTracker.SaveAllSessions()
    for lumberId, stats in pairs(sessionFarmingStats) do
        if stats and stats.collected > 0 then
            SaveFarmingSession(lumberId)
        end
    end
end

-- Check for lumber changes and return true if any lumber was collected
-- Called from QueueInventoryUpdate in HousingDecorGuide.lua
function HDG_LumberTracker.CheckForLumberChanges()
    return CheckLumberChanges()
end

-- Set UI suspended state (called from main event handler when mail/bank/merchant/trade opens/closes)
function HDG_LumberTracker.SetUISuspended(suspended)
    uiSuspended = suspended
    -- When un-suspending, re-snapshot counts so we don't false-trigger
    if not suspended then
        for lumberId, _ in pairs(LUMBER_TYPES) do
            previousLumberCounts[lumberId] = Data().GetItemCount(lumberId) or 0
        end
    end
end

-- Get active farming lumber (for debug)
function HDG_LumberTracker.GetActiveFarmingLumber()
    return activeFarmingLumber
end

-- Get session farming stats (for debug)
function HDG_LumberTracker.GetSessionStats()
    -- Calculate perHour for each active session
    local result = {}
    local now = GetTime()
    for lumberId, stats in pairs(sessionFarmingStats) do
        if stats then
            local elapsed = now - (stats.startTime or now)
            local perHour = 0
            if elapsed > 0 and stats.collected > 0 then
                perHour = (stats.collected / elapsed) * 3600
            end
            result[lumberId] = {
                collected = stats.collected or 0,
                perHour = perHour,
                zone = stats.zone,
                startTime = stats.startTime,
            }
        end
    end
    return result
end

-- Get previous lumber counts (for debug)
function HDG_LumberTracker.GetPreviousCounts()
    return previousLumberCounts
end

-- Manually start a farming session for a specific lumber type
-- This allows users to pre-start a session before they begin gathering
function HDG_LumberTracker.StartManualFarmingSession(lumberId)
    if not lumberId or not LUMBER_TYPES[lumberId] then return end

    local now = GetTime()
    local currentZone = GetZoneText() or "Unknown"
    local currentCount = Data().GetItemCount(lumberId) or 0

    -- Initialize session if not already active
    if not sessionFarmingStats[lumberId] then
        sessionFarmingStats[lumberId] = {
            startCount = currentCount,
            startTime = now,
            collected = 0,
            zone = currentZone,
            sessionId = lumberId .. "_" .. time(),
            lastCollectTime = now
        }
    end

    -- Set as active farming lumber
    activeFarmingLumber = lumberId

    -- Update previous count baseline
    previousLumberCounts[lumberId] = currentCount

    -- Enable focus mode display
    local focusModeEnabled = GetConfig("lumberFocusMode")
    if focusModeEnabled then
        HDG_LumberTracker.ShowFocus()
    elseif pinnedFrame then
        HDG_LumberTracker.RefreshPinned()
    end

    -- Notify user
    local lumberInfo = LUMBER_TYPES[lumberId]
    local lumberName = HDG_ExpansionData.GetLumberMaterialName(lumberId) or (lumberInfo and lumberInfo.shortName) or (lumberInfo and lumberInfo.name) or "Lumber"
    print(string.format("|cFFfcd34d[Housing Decor Guide]|r Started farming session for |cFF88FF88%s|r", lumberName))
end
-- ============================================================================
-- PINNED LUMBER WIDGET (Independent frame parented to UIParent)
-- Note: Embedded lumber frame is created in HDG_RecipesTab.lua (CreateShoppingPanel)
-- Uses FLOATING_WIDGET constants from HDG_Config.lua
-- ============================================================================

-- Convenience accessor for FLOATING_WIDGET config
local function FW()
    return CONFIG.FLOATING_WIDGET or {}
end

-- Calculate lumber panel height dynamically based on number of lumber types
local function GetLumberCount()
    return #HDG_ExpansionData.LUMBER_ORDER
end

local function GetLumberContentHeight()
    return GetLumberCount() * (FW().ROW_HEIGHT or 18)
end

local function GetLumberPanelHeight()
    return (FW().LUMBER_HEADER_HEIGHT or 18) + GetLumberContentHeight() + 4
end

local function GetCompactHeight()
    return (FW().HEADER_HEIGHT or 34) + GetLumberPanelHeight() + (FW().FOOTER_PADDING or 8)
end

local function GetExpandedHeight()
    return GetCompactHeight() + (FW().LUMBER_MATERIALS_HEIGHT or 372)
end

-- Helper to create standard button
local function CreateHeaderButton(parent, size, tooltip)
    local scheme = GetScheme()

    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(size, size)
    btn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })

    if scheme then
        btn:SetBackdropColor(scheme.button_normal.r, scheme.button_normal.g, scheme.button_normal.b, scheme.button_normal.a)
        btn:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
        btn._scheme = scheme
    else
        btn:SetBackdropColor(unpack(HDG_UIHelpers.BTN_NORMAL))
        btn:SetBackdropBorderColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    end

    btn:SetScript("OnEnter", function(self)
        local c = self._scheme or GetScheme()
        if c then
            self:SetBackdropColor(c.button_hover.r, c.button_hover.g, c.button_hover.b, c.button_hover.a)
        else
            self:SetBackdropColor(unpack(HDG_UIHelpers.BTN_HOVER))
        end
        if tooltip then
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:SetText(tooltip, 1, 1, 1)
            GameTooltip:Show()
        end
    end)
    btn:SetScript("OnLeave", function(self)
        local c = self._scheme or GetScheme()
        if c then
            self:SetBackdropColor(c.button_normal.r, c.button_normal.g, c.button_normal.b, c.button_normal.a)
        else
            self:SetBackdropColor(unpack(HDG_UIHelpers.BTN_NORMAL))
        end
        GameTooltip:Hide()
    end)

    -- Register with Theme Engine
    RegisterWidget(btn, "Button")

    return btn
end

-- Save pinned frame position
local function SavePinnedPosition()
    if not pinnedFrame then return end
    local point, _, relativePoint, xOfs, yOfs = pinnedFrame:GetPoint()
    Dispatch("SET_LUMBER_POSITION", { position = { point, relativePoint, xOfs, yOfs } })
end

-- Force recreate the pinned frame (destroys existing and creates new)
function HDG_LumberTracker.ResetPinnedFrame()
    if pinnedFrame then
        SavePinnedPosition()
        pinnedFrame:Hide()
        pinnedFrame:SetParent(nil)
        pinnedFrame = nil
    end
    return HDG_LumberTracker.CreatePinnedFrame()
end

-- Create the independent pinned lumber frame
function HDG_LumberTracker.CreatePinnedFrame()
    if pinnedFrame then return pinnedFrame end

    local scheme = GetScheme()

    -- Create frame parented to UIParent (independent of main addon window)
    pinnedFrame = CreateFrame("Frame", "HDG_PinnedLumberFrame", UIParent, "BackdropTemplate")
    pinnedFrame:SetSize(FW().LUMBER_WIDTH or 280, GetCompactHeight())
    pinnedFrame:SetFrameStrata("DIALOG")  -- Show above main window and its close button
    pinnedFrame:SetToplevel(true)  -- Raise to top when clicked
    pinnedFrame:SetFrameLevel(100)
    pinnedFrame:SetClampedToScreen(true)
    pinnedFrame:SetMovable(true)
    pinnedFrame:EnableMouse(true)
    pinnedFrame:RegisterForDrag("LeftButton")

    -- Restore position or default to center-right
    local pos = GetUIState("pinnedLumberPosition")
    if pos and pos[1] and pos[3] and pos[4] then
        pinnedFrame:SetPoint(pos[1], UIParent, pos[2] or pos[1], pos[3], pos[4])
    else
        pinnedFrame:SetPoint("RIGHT", UIParent, "RIGHT", -50, 0)
    end

    pinnedFrame:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    pinnedFrame:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, scheme.bg.a)
    pinnedFrame:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)

    -- Register with Theme Engine
    RegisterWidget(pinnedFrame, "Frame")

    -- Dragging
    pinnedFrame:SetScript("OnDragStart", function(self) self:StartMoving() end)
    pinnedFrame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        SavePinnedPosition()
    end)

    -- Title button (clickable to start manual farming session)
    local titleBtn = CreateFrame("Button", nil, pinnedFrame)
    titleBtn:SetSize(160, 30)
    titleBtn:SetPoint("LEFT", 4, 0)
    titleBtn:SetPoint("TOP", 0, 0)

    local titleTexture = titleBtn:CreateTexture(nil, "ARTWORK")
    titleTexture:SetAllPoints()
    titleTexture:SetTexture(CONFIG.ADDON_TEXTURES.TRACKER_TITLE)
    titleBtn.texture = titleTexture

    -- Title is decorative only (no click handler)

    pinnedFrame.titleBtn = titleBtn
    pinnedFrame.titleTexture = titleTexture

    -- Unpin button (pin icon) - Unpins and restores embedded lumber frame
    local unpinBtn = CreateHeaderButton(pinnedFrame, 24, "Unpin (return to embedded)")
    unpinBtn:SetPoint("TOPRIGHT", -6, -6)
    local unpinIcon = unpinBtn:CreateTexture(nil, "ARTWORK")
    unpinIcon:SetSize(16, 16)
    unpinIcon:SetPoint("CENTER", 0, 0)
    unpinIcon:SetTexture(CONFIG.ADDON_TEXTURES.PIN)
    unpinIcon:SetDesaturated(true)
    local unpinScheme = GetScheme()
    if unpinScheme then
        unpinIcon:SetVertexColor(unpinScheme.text_dim.r, unpinScheme.text_dim.g, unpinScheme.text_dim.b, 0.8)
    else
        unpinIcon:SetVertexColor(0.7, 0.7, 0.7, 0.8)  -- Desaturated when pinned (click to unpin)
    end
    unpinBtn.icon = unpinIcon
    unpinBtn:SetScript("OnClick", function()
        Dispatch("SET_LUMBER_PINNED", { pinned = false })
        HDG_LumberTracker.HidePinned()
        -- Restore embedded lumber frame in shopping panel
        if HDG_UI and HDG_UI.UpdateLumberLayout then
            HDG_UI.UpdateLumberLayout()
        end
        -- Update config tab checkbox to reflect change
        if HDG_ConfigTab and HDG_ConfigTab.UpdateLumberPinCheckbox then
            HDG_ConfigTab.UpdateLumberPinCheckbox()
        end
        print("|cFFfcd34d[Housing Decor Guide]|r Lumber tracker unpinned")
    end)
    pinnedFrame.unpinBtn = unpinBtn

    -- Expand button (+/-) - toggles main window
    local expandBtn = CreateHeaderButton(pinnedFrame, 24)  -- No static tooltip
    expandBtn:SetPoint("TOPRIGHT", unpinBtn, "TOPLEFT", -4, 0)
    local expandText = expandBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    expandText:SetPoint("CENTER", 0, 0)
    ApplyFont(expandText, unpinScheme, "body")
    expandText:SetText("|cFFe2e8f0+|r")
    expandBtn.text = expandText

    -- Helper to check if main window is visible
    local function IsMainWindowVisible()
        return mainFrame and mainFrame:IsShown() or (HDG_UI and HDG_UI.IsVisible and HDG_UI.IsVisible())
    end

    -- Update button appearance based on main window state
    local function UpdateExpandButton()
        if IsMainWindowVisible() then
            expandText:SetText("|cFFe2e8f0-|r")
        else
            expandText:SetText("|cFFe2e8f0+|r")
        end
    end
    expandBtn.UpdateState = UpdateExpandButton

    -- Dynamic tooltip
    local function ShowExpandTooltip(btn)
        GameTooltip:SetOwner(btn, HDG_UIHelpers.TOOLTIP_ANCHOR)
        if IsMainWindowVisible() then
            GameTooltip:SetText(L["TIP_CLOSE_MAIN_WINDOW"] or "Close Main Window", 1, 1, 1)
        else
            GameTooltip:SetText(L["TIP_OPEN_MAIN_WINDOW"] or "Open Main Window", 1, 1, 1)
        end
        GameTooltip:Show()
    end

    expandBtn:SetScript("OnEnter", function(self)
        local hoverScheme = GetScheme()
        if hoverScheme then
            self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, hoverScheme.button_hover.a)
        else
            self:SetBackdropColor(unpack(HDG_UIHelpers.BTN_HOVER))
        end
        ShowExpandTooltip(self)
    end)
    expandBtn:SetScript("OnClick", function(self)
        -- Use HDG_UI.Toggle which handles window creation if needed
        if HDG_UI and HDG_UI.Toggle then
            HDG_UI.Toggle()
        end
        -- Update button state after toggle
        C_Timer.After(0.05, function()
            UpdateExpandButton()
            -- Update tooltip if still hovering
            if GameTooltip:IsOwned(self) then
                ShowExpandTooltip(self)
            end
        end)
    end)
    pinnedFrame.expandBtn = expandBtn

    -- Materials toggle button
    local materialsBtn = CreateHeaderButton(pinnedFrame, 24)
    materialsBtn:SetPoint("TOPRIGHT", expandBtn, "TOPLEFT", -4, 0)
    local materialsIcon = materialsBtn:CreateTexture(nil, "ARTWORK")
    materialsIcon:SetSize(14, 14)
    materialsIcon:SetPoint("CENTER", 0, 0)
    materialsIcon:SetTexture("Interface\\MINIMAP\\TRACKING\\Banker")  -- Bag-like icon
    materialsBtn.icon = materialsIcon

    local function UpdateMaterialsIcon()
        local iconScheme = GetScheme()
        if GetConfig("showLumberMaterials") then
            materialsIcon:SetDesaturated(false)
            if iconScheme then
                materialsIcon:SetVertexColor(iconScheme.text.r, iconScheme.text.g, iconScheme.text.b, 1)
            else
                materialsIcon:SetVertexColor(1, 1, 1, 1)
            end
        else
            materialsIcon:SetDesaturated(true)
            if iconScheme then
                materialsIcon:SetVertexColor(iconScheme.text_dim.r, iconScheme.text_dim.g, iconScheme.text_dim.b, 0.7)
            else
                materialsIcon:SetVertexColor(0.6, 0.6, 0.6, 0.7)
            end
        end
    end
    materialsBtn.UpdateIcon = UpdateMaterialsIcon

    local function ShowMaterialsTooltip(btn)
        GameTooltip:SetOwner(btn, HDG_UIHelpers.TOOLTIP_ANCHOR)
        -- Check if currently farming (materials hidden during farming)
        if activeFarmingLumber then
            GameTooltip:SetText(L["TIP_MATERIALS_UNAVAILABLE"] or "Materials Unavailable", 0.6, 0.6, 0.6)
            GameTooltip:AddLine("Materials panel is hidden while farming lumber", 1, 0.5, 0.5)
        elseif GetConfig("showLumberMaterials") then
            GameTooltip:SetText(L["TIP_HIDE_MATERIALS"] or "Hide Materials", 1, 1, 1)
            GameTooltip:AddLine("Click to hide required materials for crafting queue", 0.7, 0.7, 0.7)
        else
            GameTooltip:SetText(L["TIP_SHOW_MATERIALS"] or "Show Materials", 1, 1, 1)
            GameTooltip:AddLine("Click to show required materials for crafting queue", 0.7, 0.7, 0.7)
        end
        GameTooltip:Show()
    end

    materialsBtn:SetScript("OnEnter", function(self)
        local hoverScheme = GetScheme()
        if hoverScheme then
            self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, hoverScheme.button_hover.a)
        else
            self:SetBackdropColor(unpack(HDG_UIHelpers.BTN_HOVER))
        end
        ShowMaterialsTooltip(self)
    end)
    materialsBtn:SetScript("OnClick", function(self)
        -- Ignore clicks while farming (materials unavailable)
        if activeFarmingLumber then return end
        Dispatch("SET_CONFIG", { key = "showLumberMaterials", value = not GetConfig("showLumberMaterials") })
        UpdateMaterialsIcon()
        HDG_LumberTracker.RefreshPinned()
        -- Update tooltip to reflect new state
        ShowMaterialsTooltip(self)
    end)
    UpdateMaterialsIcon()
    pinnedFrame.materialsBtn = materialsBtn

    -- Static lumber section (not scrollable)
    local lumberPanel = CreateFrame("Frame", nil, pinnedFrame, "BackdropTemplate")
    lumberPanel:SetPoint("TOPLEFT", 6, -34)
    lumberPanel:SetPoint("TOPRIGHT", -6, -34)
    lumberPanel:SetHeight(GetLumberPanelHeight())
    lumberPanel:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    lumberPanel:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, scheme.panel.a)
    lumberPanel:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    pinnedFrame.lumberPanel = lumberPanel

    -- Register with Theme Engine
    RegisterWidget(lumberPanel, "Panel")

    -- Column header row (Type, Needed, In Stock)
    local headerRow = CreateFrame("Frame", nil, lumberPanel)
    headerRow:SetPoint("TOPLEFT", 4, -2)
    headerRow:SetPoint("TOPRIGHT", -4, -2)
    headerRow:SetHeight(14)

    local headerScheme = GetScheme()
    local headerColor = headerScheme and headerScheme.text_dim or {r=0.6, g=0.6, b=0.6, a=1}

    local typeHeader = headerRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    typeHeader:SetPoint("LEFT", 0, 0)
    typeHeader:SetWidth(CONFIG.LUMBER_COL_1)
    typeHeader:SetJustifyH("LEFT")
    ApplyFont(typeHeader, headerScheme, "small")
    typeHeader:SetText("Type")
    typeHeader:SetTextColor(headerColor.r, headerColor.g, headerColor.b, headerColor.a)

    local neededHeader = headerRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    neededHeader:SetPoint("LEFT", CONFIG.LUMBER_COL_1 + 2, 0)
    neededHeader:SetWidth(CONFIG.LUMBER_COL_2)
    neededHeader:SetJustifyH("CENTER")
    ApplyFont(neededHeader, headerScheme, "small")
    neededHeader:SetText("Queue")
    neededHeader:SetTextColor(headerColor.r, headerColor.g, headerColor.b, headerColor.a)

    local stockHeader = headerRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    stockHeader:SetPoint("LEFT", CONFIG.LUMBER_COL_1 + CONFIG.LUMBER_COL_2 + 4, 0)
    stockHeader:SetWidth(CONFIG.LUMBER_COL_3)
    stockHeader:SetJustifyH("CENTER")
    ApplyFont(stockHeader, headerScheme, "small")
    stockHeader:SetText("Uncollected")
    stockHeader:SetTextColor(headerColor.r, headerColor.g, headerColor.b, headerColor.a)

    pinnedFrame.lumberHeaderRow = headerRow

    -- Lumber content area (static, no scroll) - positioned below header
    local lumberContent = CreateFrame("Frame", nil, lumberPanel)
    local lumberHeaderH = -(FW().LUMBER_HEADER_HEIGHT or 18)
    lumberContent:SetPoint("TOPLEFT", 4, lumberHeaderH)
    lumberContent:SetPoint("TOPRIGHT", -4, lumberHeaderH)
    lumberContent:SetHeight(GetLumberContentHeight())
    pinnedFrame.lumberContent = lumberContent

    -- Materials section (scrollable, only visible when enabled)
    local materialsPanel = CreateFrame("Frame", nil, pinnedFrame, "BackdropTemplate")
    materialsPanel:SetPoint("TOPLEFT", lumberPanel, "BOTTOMLEFT", 0, -4)
    materialsPanel:SetPoint("TOPRIGHT", lumberPanel, "BOTTOMRIGHT", 0, -4)
    materialsPanel:SetHeight(FW().LUMBER_MATERIALS_HEIGHT or 372)  -- Fixed height for materials section
    materialsPanel:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    materialsPanel:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, scheme.panel.a)
    materialsPanel:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    pinnedFrame.materialsPanel = materialsPanel

    -- Register with Theme Engine
    RegisterWidget(materialsPanel, "Panel")

    -- Materials header (styled like recipes tab headers)
    local materialsHeaderBg = CreateFrame("Frame", nil, materialsPanel, "BackdropTemplate")
    materialsHeaderBg:SetPoint("TOPLEFT", 1, -1)
    materialsHeaderBg:SetPoint("TOPRIGHT", -1, -1)
    materialsHeaderBg:SetHeight(22)
    materialsHeaderBg:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    local mhbScheme = GetScheme()
    if mhbScheme then
        materialsHeaderBg:SetBackdropColor(mhbScheme.bg.r, mhbScheme.bg.g, mhbScheme.bg.b, 0.8)
    else
        materialsHeaderBg:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
    end

    local requiredMatsLabel = L["HEADER_REQUIRED_MATERIALS"] or "Required Materials"
    local rawMatsLabel = L["HEADER_RAW_MATERIALS"] or "Raw Materials"
    local directLabel = L["BTN_DIRECT"] or "Direct"
    local rawLabel = L["BTN_RAW"] or "Raw"

    local materialsTitle = materialsHeaderBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    materialsTitle:SetPoint("LEFT", 8, 0)
    ApplyFont(materialsTitle, mhbScheme, "body")
    materialsTitle:SetText("|cFFFFFFFF" .. requiredMatsLabel .. "|r")
    pinnedFrame.materialsHeader = materialsHeaderBg
    pinnedFrame.materialsTitle = materialsTitle

    -- Direct/Raw toggle buttons (like Recipes tab)
    local toggleContainer = CreateFrame("Frame", nil, materialsHeaderBg, "BackdropTemplate")
    toggleContainer:SetSize(80, 16)
    toggleContainer:SetPoint("RIGHT", -6, 0)
    toggleContainer:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    local tcScheme = GetScheme()
    if tcScheme then
        toggleContainer:SetBackdropColor(tcScheme.bg.r, tcScheme.bg.g, tcScheme.bg.b, 0.8)
    else
        toggleContainer:SetBackdropColor(0.05, 0.05, 0.05, 0.8)
    end

    local directBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    directBtn:SetSize(38, 14)
    directBtn:SetPoint("LEFT", 1, 0)
    directBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    local directText = directBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    directText:SetPoint("CENTER", 0, 0)
    ApplyFont(directText, tcScheme, "small")
    directText:SetText(directLabel)

    local rawBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    rawBtn:SetSize(38, 14)
    rawBtn:SetPoint("LEFT", directBtn, "RIGHT", 1, 0)
    rawBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    local rawText = rawBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    rawText:SetPoint("CENTER", 0, 0)
    ApplyFont(rawText, tcScheme, "small")
    rawText:SetText(rawLabel)

    -- Update toggle button appearance based on state
    local function UpdatePinnedToggleAppearance()
        local showRaw = GetConfig("showRawMaterials")
        local toggleScheme = GetScheme()
        if showRaw then
            if toggleScheme then
                directBtn:SetBackdropColor(toggleScheme.bg.r, toggleScheme.bg.g, toggleScheme.bg.b, 0.8)
                directText:SetTextColor(toggleScheme.text_dim.r, toggleScheme.text_dim.g, toggleScheme.text_dim.b, 1)
                rawBtn:SetBackdropColor(toggleScheme.accent.r, toggleScheme.accent.g, toggleScheme.accent.b, 1.0)
                rawText:SetTextColor(toggleScheme.text.r, toggleScheme.text.g, toggleScheme.text.b, 1)
            else
                directBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
                directText:SetTextColor(0.6, 0.6, 0.6, 1)
                rawBtn:SetBackdropColor(0.08, 0.45, 0.45, 1.0)
                rawText:SetTextColor(1, 1, 1, 1)
            end
            materialsTitle:SetText("|cFFFFFFFF" .. rawMatsLabel .. "|r")
        else
            if toggleScheme then
                directBtn:SetBackdropColor(toggleScheme.accent.r, toggleScheme.accent.g, toggleScheme.accent.b, 1.0)
                directText:SetTextColor(toggleScheme.text.r, toggleScheme.text.g, toggleScheme.text.b, 1)
                rawBtn:SetBackdropColor(toggleScheme.bg.r, toggleScheme.bg.g, toggleScheme.bg.b, 0.8)
                rawText:SetTextColor(toggleScheme.text_dim.r, toggleScheme.text_dim.g, toggleScheme.text_dim.b, 1)
            else
                directBtn:SetBackdropColor(0.08, 0.45, 0.45, 1.0)
                directText:SetTextColor(1, 1, 1, 1)
                rawBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
                rawText:SetTextColor(0.6, 0.6, 0.6, 1)
            end
            materialsTitle:SetText("|cFFFFFFFF" .. requiredMatsLabel .. "|r")
        end
    end

    directBtn:SetScript("OnClick", function()
        Dispatch("SET_CONFIG", { key = "showRawMaterials", value = false })
        UpdatePinnedToggleAppearance()
        HDG_LumberTracker.RefreshPinned()
        -- Also update main RecipesTab toggle appearance and materials
        if HDG_RecipesTab then
            if HDG_RecipesTab.mainFrame and HDG_RecipesTab.mainFrame.materialsToggleUpdate then
                HDG_RecipesTab.mainFrame.materialsToggleUpdate()
            end
            if HDG_RecipesTab.RefreshMaterials then
                HDG_RecipesTab:RefreshMaterials()
            end
        end
    end)

    rawBtn:SetScript("OnClick", function()
        Dispatch("SET_CONFIG", { key = "showRawMaterials", value = true })
        UpdatePinnedToggleAppearance()
        HDG_LumberTracker.RefreshPinned()
        -- Also update main RecipesTab toggle appearance and materials
        if HDG_RecipesTab then
            if HDG_RecipesTab.mainFrame and HDG_RecipesTab.mainFrame.materialsToggleUpdate then
                HDG_RecipesTab.mainFrame.materialsToggleUpdate()
            end
            if HDG_RecipesTab.RefreshMaterials then
                HDG_RecipesTab:RefreshMaterials()
            end
        end
    end)

    directBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine("|cFFe2e8f0Direct Materials|r", 1, 1, 1)
        GameTooltip:AddLine("Show materials as listed in recipes", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Crafted reagents shown as-is", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    directBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    rawBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine("|cFFe2e8f0Raw Materials (Power Craft)|r", 1, 1, 1)
        GameTooltip:AddLine("Expand crafted reagents to base materials", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Shows what you actually need to gather/buy", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    rawBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    pinnedFrame.materialsToggleUpdate = UpdatePinnedToggleAppearance
    UpdatePinnedToggleAppearance()

    -- ScrollFrame for materials only
    local scrollFrame = CreateFrame("ScrollFrame", "HDG_PinnedMaterialsScroll", materialsPanel, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 4, -26)
    scrollFrame:SetPoint("BOTTOMRIGHT", -22, 48)  -- Leave room for button and cost footer
    pinnedFrame.scrollFrame = scrollFrame

    -- Style scrollbar using helper
    HDG_UIHelpers.StyleScrollBar(scrollFrame)

    -- Scrollable materials content
    local materialsContent = CreateFrame("Frame", nil, scrollFrame)
    materialsContent:SetSize((FW().LUMBER_WIDTH or 280) - 36, 1)  -- Height set dynamically
    scrollFrame:SetScrollChild(materialsContent)
    pinnedFrame.materialsContent = materialsContent

    -- Cost footer (inside materials panel at bottom)
    local costScheme = GetScheme()
    local costText = materialsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    costText:SetPoint("BOTTOMLEFT", 10, 6)
    costText:SetPoint("BOTTOMRIGHT", -10, 6)
    costText:SetJustifyH("LEFT")
    ApplyFont(costText, costScheme, "small")
    pinnedFrame.costText = costText

    pinnedFrame:Hide()  -- Start hidden
    return pinnedFrame
end

-- Show the pinned lumber widget
function HDG_LumberTracker.ShowPinned()
    if not pinnedFrame then
        HDG_LumberTracker.CreatePinnedFrame()
    end

    Dispatch("SET_LUMBER_PINNED", { pinned = true })
    pinnedFrame:Show()
    HDG_LumberTracker.RefreshPinned()
    if HDG_LumberHUD and HDG_LumberHUD.UpdateAnchor then
        HDG_LumberHUD.UpdateAnchor()
    end
end

-- Hide the pinned lumber widget
function HDG_LumberTracker.HidePinned()
    if pinnedFrame then
        pinnedFrame:Hide()
    end
    if HDG_LumberHUD and HDG_LumberHUD.UpdateAnchor then
        HDG_LumberHUD.UpdateAnchor()
    end
end

-- Toggle pinned lumber tracker visibility
function HDG_LumberTracker.TogglePinned()
    if HDG_LumberTracker.IsPinnedVisible() then
        Dispatch("SET_LUMBER_PINNED", { pinned = false })
        HDG_LumberTracker.HidePinned()
    else
        HDG_LumberTracker.ShowPinned()
    end
end

-- ============================================================================
-- FOCUS FRAME (Minimal widget shown during active farming with focus mode)
-- ============================================================================

function HDG_LumberTracker.CreateFocusFrame()
    if focusFrame then return focusFrame end

    local scheme = GetScheme()

    -- Create minimal floating widget
    focusFrame = CreateFrame("Frame", "HDG_FocusLumberFrame", UIParent, "BackdropTemplate")
    focusFrame:SetSize(FW().LUMBER_WIDTH or 280, (FW().HEADER_HEIGHT or 34) + (FW().ROW_HEIGHT_EXPANDED or 62) + 8)
    focusFrame:SetFrameStrata("DIALOG")
    focusFrame:SetToplevel(true)
    focusFrame:SetFrameLevel(100)
    focusFrame:SetClampedToScreen(true)
    focusFrame:SetMovable(true)
    focusFrame:EnableMouse(true)
    focusFrame:RegisterForDrag("LeftButton")

    -- Use same position as pinned widget
    local pos = GetUIState("pinnedLumberPosition")
    if pos and pos[1] and pos[3] and pos[4] then
        focusFrame:SetPoint(pos[1], UIParent, pos[2] or pos[1], pos[3], pos[4])
    else
        focusFrame:SetPoint("RIGHT", UIParent, "RIGHT", -50, 0)
    end

    focusFrame:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    focusFrame:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, scheme.bg.a)
    focusFrame:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)

    RegisterWidget(focusFrame, "Frame")

    -- Dragging (syncs position with pinned widget)
    focusFrame:SetScript("OnDragStart", function(self) self:StartMoving() end)
    focusFrame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        -- Save position (shared with pinned widget) using Dispatch
        local point, _, relativePoint, xOfs, yOfs = self:GetPoint()
        Dispatch("SET_LUMBER_POSITION", { position = { point, relativePoint, xOfs, yOfs } })
        -- Sync pinned widget position
        if pinnedFrame then
            pinnedFrame:ClearAllPoints()
            pinnedFrame:SetPoint(point, UIParent, relativePoint, xOfs, yOfs)
        end
    end)

    -- Title button (20% smaller and 25% dimmer in focus mode)
    local titleBtn = CreateFrame("Button", nil, focusFrame)
    titleBtn:SetSize(128, 24)  -- 20% smaller than pinned (160x30)
    titleBtn:SetPoint("LEFT", 4, 0)
    titleBtn:SetPoint("TOP", 0, 0)

    local titleTexture = titleBtn:CreateTexture(nil, "ARTWORK")
    titleTexture:SetAllPoints()
    titleTexture:SetTexture(CONFIG.ADDON_TEXTURES.TRACKER_TITLE)
    titleTexture:SetAlpha(0.75)  -- 25% dimmer
    titleBtn.texture = titleTexture

    focusFrame.titleBtn = titleBtn
    focusFrame.titleTexture = titleTexture

    -- End Session button (replaces unpin - ends farming and returns to pinned view)
    local endBtn = CreateHeaderButton(focusFrame, 24, "End Farming Session")
    endBtn:SetPoint("TOPRIGHT", -6, -6)
    local endIcon = endBtn:CreateTexture(nil, "ARTWORK")
    endIcon:SetSize(14, 14)
    endIcon:SetPoint("CENTER", 0, 0)
    endIcon:SetTexture("Interface\\BUTTONS\\UI-StopButton")
    endBtn.icon = endIcon
    endBtn:SetScript("OnClick", function()
        if activeFarmingLumber then
            HandleEndSession(activeFarmingLumber)
        else
            HDG_LumberTracker.HideFocus() -- fallback: dismiss stuck focus frame
        end
    end)
    focusFrame.endBtn = endBtn

    -- Content area for the single lumber row (smaller offset for focus frame's smaller title)
    local content = CreateFrame("Frame", nil, focusFrame)
    content:SetPoint("TOPLEFT", 6, -27)  -- Reduced from HEADER_HEIGHT (34) to match smaller title
    content:SetPoint("TOPRIGHT", -6, -27)
    content:SetHeight(FW().ROW_HEIGHT_EXPANDED or 62)
    focusFrame.content = content

    focusFrame:Hide()
    return focusFrame
end

-- Show focus widget (hides pinned widget)
function HDG_LumberTracker.ShowFocus()
    if not focusFrame then
        HDG_LumberTracker.CreateFocusFrame()
    end
    -- Hide pinned widget
    if pinnedFrame and pinnedFrame:IsShown() then
        pinnedFrame:Hide()
    end
    focusFrame:Show()
    HDG_LumberTracker.RefreshFocus()
end

-- Hide focus widget (shows pinned widget if lumber is pinned)
function HDG_LumberTracker.HideFocus()
    if focusFrame then
        focusFrame:Hide()
    end
    -- Restore pinned widget if pinned setting is enabled
    if GetUIState("lumberPinned") then
        HDG_LumberTracker.ShowPinned()
    end
end

-- Refresh focus widget content (single lumber row)
function HDG_LumberTracker.RefreshFocus()
    if not focusFrame or not focusFrame.content then return end

    -- Clear content
    HDG_UIHelpers.ClearScrollContent(focusFrame.content)

    -- Show message if no active farming session
    if not activeFarmingLumber then
        local msg = focusFrame.content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        msg:SetPoint("CENTER", 0, 0)
        msg:SetText("|cFF888888Go farm some lumber to start tracking|r")
        local scheme = GetScheme()
        if scheme then
            ApplyFont(msg, scheme, "body")
        end
        return
    end

    local shoppingLumber = HDG_LumberTracker.CalculateShoppingListLumber(GetShoppingList())
    local allLumber = HDG_LumberTracker.CalculateLumberNeeds(privateDB.professions or {})
    local lumberInfo = LUMBER_TYPES[activeFarmingLumber]
    local shoppingData = shoppingLumber[activeFarmingLumber]
    local allData = allLumber[activeFarmingLumber]

    -- Create the single lumber row (always expanded with session info)
    CreateLumberRow(focusFrame.content, activeFarmingLumber, lumberInfo, shoppingData, allData, 0)
end

-- Check if focus widget is visible
function HDG_LumberTracker.IsFocusVisible()
    return focusFrame and focusFrame:IsShown()
end

-- Check if pinned widget is visible
function HDG_LumberTracker.IsPinnedVisible()
    return pinnedFrame and pinnedFrame:IsShown()
end

-- Restore pinned state on login (called from HDG_UI after settings load)
function HDG_LumberTracker.RestorePinnedState()
    if GetUIState("lumberPinned") then
        HDG_LumberTracker.ShowPinned()
    end
end

-- ============================================================================
-- REFRESH FUNCTIONS
-- ============================================================================

function HDG_LumberTracker.Refresh()
    if not lumberContent then return end
    HDG_UIHelpers.ClearScrollContent(lumberContent)

    -- Use local lumber calculation functions
    local shoppingLumber = HDG_LumberTracker.CalculateShoppingListLumber(GetShoppingList())
    local allLumber = HDG_LumberTracker.CalculateLumberNeeds(privateDB.professions or {})
    local expansionOrder = HDG_ExpansionData.LUMBER_ORDER

    local yOffset = 0
    local hasAnyLumber = false
    local isFarming = (activeFarmingLumber ~= nil)
    local focusMode = GetConfig("lumberFocusMode") and isFarming

    for _, lumberId in ipairs(expansionOrder) do
        -- Skip non-active lumber when in focus mode
        if focusMode and lumberId ~= activeFarmingLumber then
            -- Skip this lumber row
        else
            local shoppingData = shoppingLumber[lumberId]
            local allData = allLumber[lumberId]

            hasAnyLumber = true
            local lumberInfo = LUMBER_TYPES[lumberId]

            yOffset = CreateLumberRow(lumberContent, lumberId, lumberInfo, shoppingData, allData, yOffset)
        end
    end

    if not hasAnyLumber then
        local emptyScheme = GetScheme()
        local emptyText = lumberContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        emptyText:SetPoint("CENTER", 0, 0)
        emptyText:SetText("|cFF888888" .. (L["STATUS_ALL_LUMBER_COLLECTED"] or "All lumber collected!") .. "|r")
        ApplyFont(emptyText, emptyScheme, "small")
    end

    -- Also refresh pinned widget if visible
    if HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.RefreshPinned()
    end
end

-- Calculate non-lumber materials from shopping list
local function CalculateNonLumberMaterials(shoppingList)
    local materials = {}

    for _, item in ipairs(shoppingList or {}) do
        if item.reagents then
            for reagentKey, reagent in pairs(item.reagents) do
                local reagentID = reagent.id or reagentKey
                -- Skip lumber types
                if not LUMBER_TYPES[reagentID] then
                    if not materials[reagentID] then
                        local reagentName = "Unknown"
                        local reagentSource = "Unknown"

                        -- Try to get name from ReagentsDB
                        if HDG_ReagentsDB and HDG_ReagentsDB[reagentID] then
                            reagentSource = HDG_ReagentsDB[reagentID]
                        end

                        -- Get item info using shared cache (same as materials panel)
                        local itemName = Data().GetCachedItemName(reagentID)
                        if itemName then
                            reagentName = itemName
                        end

                        materials[reagentID] = {
                            id = reagentID,
                            name = reagentName,
                            source = reagentSource,
                            expansion = item.expansion,  -- Track expansion for coloring
                            needed = 0,
                            have = Data().GetItemCount(reagentID)
                        }
                    end
                    local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                    materials[reagentID].needed = materials[reagentID].needed + (reagentAmount * (item.quantity or 1))
                end
            end
        end
    end

    return materials
end

-- Calculate RAW non-lumber materials from shopping list (uses HDG_PowerCrafting)
local function CalculateRawNonLumberMaterials(shoppingList)
    local materials = {}

    for _, item in ipairs(shoppingList or {}) do
        if item.reagents and HDG_PowerCrafting then
            -- Convert shopping list reagents format to HDG_DecorDB format for Power Crafting
            local reagentsForGraph = {}
            for _, reagent in ipairs(item.reagents) do
                local reagentID = reagent.id
                local reagentQty = reagent.amount or reagent.qty or 1
                reagentsForGraph[reagentID] = { name = reagent.name, qty = reagentQty }
            end

            -- Build a pseudo-recipe for the graph builder
            local pseudoRecipe = { reagents = reagentsForGraph }

            -- Build dependency graph to get raw materials
            local graph = HDG_PowerCrafting.BuildDependencyGraph(pseudoRecipe)
            if graph and graph.rawMaterials then
                for rawID, rawData in pairs(graph.rawMaterials) do
                    -- Skip lumber types
                    if not LUMBER_TYPES[rawID] then
                        if not materials[rawID] then
                            materials[rawID] = {
                                id = rawID,
                                name = rawData.name or "Unknown",
                                source = rawData.source or "Unknown",
                                expansion = item.expansion,
                                needed = 0,
                                have = Data().GetItemCount(rawID)
                            }
                        end
                        materials[rawID].needed = materials[rawID].needed + (rawData.qty * (item.quantity or 1))
                    end
                end
            end
        elseif item.reagents then
            -- Fallback to direct materials if PowerCrafting not available
            for _, reagent in ipairs(item.reagents) do
                local reagentID = reagent.id
                if not LUMBER_TYPES[reagentID] then
                    if not materials[reagentID] then
                        local reagentName = "Unknown"
                        local reagentSource = "Unknown"

                        if HDG_ReagentsDB and HDG_ReagentsDB[reagentID] then
                            reagentSource = HDG_ReagentsDB[reagentID]
                        end

                        local itemName = Data().GetCachedItemName(reagentID)
                        if itemName then
                            reagentName = itemName
                        end

                        materials[reagentID] = {
                            id = reagentID,
                            name = reagentName,
                            source = reagentSource,
                            expansion = item.expansion,
                            needed = 0,
                            have = Data().GetItemCount(reagentID)
                        }
                    end
                    local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                    materials[reagentID].needed = materials[reagentID].needed + (reagentAmount * (item.quantity or 1))
                end
            end
        end
    end

    return materials
end

-- Get progress bar color based on percentage
local function GetMaterialProgressColor(percentage)
    local tealR, tealG, tealB = 0.18, 0.80, 0.44
    if percentage >= 100 then
        return 0.17, 0.48, 0.48, 1.0
    elseif percentage >= 1 then
        local opacity = 0.3 + (percentage / 100) * 0.4
        return tealR, tealG, tealB, opacity
    else
        return tealR, tealG, tealB, 0.15
    end
end

-- Create a material row styled like the recipe tab (with progress bar)
local function CreateMaterialRow(parent, reagentId, matInfo, yOffset, rowWidth)
    local ROW_HEIGHT = 18
    rowWidth = rowWidth or ((FW().LUMBER_WIDTH or 280) - 24)

    -- Create StatusBar row like recipe tab
    local row = CreateFrame("StatusBar", nil, parent, "BackdropTemplate")
    row:SetSize(rowWidth, ROW_HEIGHT)
    row:SetPoint("TOPLEFT", 0, yOffset)
    row:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    row:SetMinMaxValues(0, matInfo.needed)
    row:SetValue(math.min(matInfo.have, matInfo.needed))

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

    -- Background with expansion color tint (like recipe tab)
    local expColor = HDG_ExpansionData.GetColor(matInfo.expansion)
    if expColor then
        row:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        row:SetBackdropColor(expColor[1], expColor[2], expColor[3], 0.15)
    else
        row:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        row:SetBackdropColor(0.05, 0.05, 0.08, 0.8)
    end

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

    -- Count text
    local countStr
    if matInfo.have > matInfo.needed then
        local excess = matInfo.have - matInfo.needed
        countStr = string.format("%s/%s+|cFF88FF88%s|r",
            BreakUpLargeNumbers(matInfo.needed),
            BreakUpLargeNumbers(matInfo.needed),
            BreakUpLargeNumbers(excess))
    else
        countStr = string.format("%s/%s",
            BreakUpLargeNumbers(matInfo.have),
            BreakUpLargeNumbers(matInfo.needed))
    end

    local countText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    countText:SetPoint("RIGHT", -4, 0)
    ApplyFont(countText, matNameScheme, "small")
    countText:SetText(countStr)

    -- Click handler for AH integration
    local clickButton = CreateFrame("Button", nil, row)
    clickButton:SetAllPoints(row)
    clickButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    clickButton:SetScript("OnClick", function(_, button)
        if button == "LeftButton" and not IsShiftKeyDown() and not IsControlKeyDown() then
            if not AuctionHouseFrame or not AuctionHouseFrame:IsShown() then
                print("|cFFfcd34d[Housing Decor Guide]|r Please open the Auction House first")
            elseif AuctionHouseFrame.SearchBar and AuctionHouseFrame.SearchBar.SearchBox then
                AuctionHouseFrame.SearchBar.SearchBox:SetText(matName)
                print("|cFFfcd34d[Housing Decor Guide]|r Set AH search to: " .. matName)
            end
        elseif button == "LeftButton" and IsShiftKeyDown() then
            if C_AddOns.IsAddOnLoaded("Auctionator") and Auctionator and Auctionator.API and Auctionator.API.v1 and Auctionator.API.v1.MultiSearch then
                Auctionator.API.v1.MultiSearch("HousingDecorGuide", {matName})
                print("|cFFfcd34d[Housing Decor Guide]|r Searching Auctionator for: " .. matName)
            end
        elseif button == "RightButton" then
            if C_AddOns.IsAddOnLoaded("Auctionator") and Auctionator and Auctionator.API and Auctionator.API.v1 then
                local callerID = "HousingDecorGuide"
                local listName = "Housing Decor"
                local existingItems = {}
                if Auctionator.API.v1.GetShoppingListItems then
                    local success, items = pcall(Auctionator.API.v1.GetShoppingListItems, callerID, listName)
                    if success and items then existingItems = items end
                end
                local exists = false
                for _, existingItem in ipairs(existingItems) do
                    if existingItem == matName then exists = true; break end
                end
                if not exists then table.insert(existingItems, matName) end
                if Auctionator.API.v1.CreateShoppingList then
                    Auctionator.API.v1.CreateShoppingList(callerID, listName, existingItems)
                    print("|cFFfcd34d[Housing Decor Guide]|r Added '" .. matName .. "' to Auctionator list")
                end
            else
                print("|cFFfcd34d[Housing Decor Guide]|r Install Auctionator for search list integration")
            end
        end
    end)

    clickButton:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetItemByID(reagentId)
        GameTooltip:AddLine(" ")
        GameTooltip:AddDoubleLine("Source:", matInfo.source or "Unknown", 1, 1, 1, 0.7, 0.7, 0.7)
        GameTooltip:AddDoubleLine("Have:", BreakUpLargeNumbers(matInfo.have), 1, 1, 1, 1, 1, 1)
        GameTooltip:AddDoubleLine("Need:", BreakUpLargeNumbers(matInfo.needed), 1, 1, 1, 1, 1, 1)
        if matInfo.have >= matInfo.needed then
            GameTooltip:AddLine("|cFF88FF88Have enough!|r")
        else
            local shortfall = matInfo.needed - matInfo.have
            GameTooltip:AddLine(string.format("|cFFFF5555Need %s more|r", BreakUpLargeNumbers(shortfall)))
        end
        -- Show price if available
        local price = Data().GetItemPrice and Data().GetItemPrice(reagentId)
        if price and price > 0 then
            GameTooltip:AddLine(" ")
            local priceStr = Data().FormatGold and Data().FormatGold(price) or tostring(price)
            GameTooltip:AddDoubleLine("Unit Price:", priceStr, 1, 1, 1, 1, 0.82, 0.2)
            if matInfo.have < matInfo.needed then
                local stillNeeded = matInfo.needed - matInfo.have
                local totalCost = price * stillNeeded
                local costStr = Data().FormatGold and Data().FormatGold(totalCost) or tostring(totalCost)
                GameTooltip:AddDoubleLine("Est. Cost:", costStr, 1, 1, 1, 1, 0.82, 0.2)
            end
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("|cFF00FF00Click|r to set AH search text", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("|cFF00FF00Shift-Click|r to search Auctionator", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("|cFF00FF00Right-Click|r to add to Auctionator list", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    clickButton:SetScript("OnLeave", function() GameTooltip:Hide() end)

    return yOffset - (ROW_HEIGHT + 1)
end

-- Create a tracked reagent row with X button for removal
local function CreateTrackedReagentRow(parent, reagentId, matInfo, yOffset, rowWidth)
    local ROW_HEIGHT = 18
    rowWidth = rowWidth or ((FW().LUMBER_WIDTH or 280) - 24)

    -- Create row frame
    local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    row:SetSize(rowWidth, ROW_HEIGHT)
    row:SetPoint("TOPLEFT", 0, yOffset)
    row:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    local trackedScheme = GetScheme()
    if trackedScheme then
        row:SetBackdropColor(trackedScheme.accent.r, trackedScheme.accent.g, trackedScheme.accent.b, 0.3)
    else
        row:SetBackdropColor(0.08, 0.35, 0.33, 0.3)  -- Teal tint for tracked items
    end

    -- Item name
    local matName = matInfo.name or "Unknown"
    local nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    nameText:SetPoint("LEFT", 4, 0)
    nameText:SetWidth(rowWidth - 85)  -- Leave room for count and X button
    nameText:SetJustifyH("LEFT")
    nameText:SetWordWrap(false)
    ApplyFont(nameText, trackedScheme, "small")
    nameText:SetText(matName)
    if trackedScheme then
        nameText:SetTextColor(trackedScheme.accent.r, trackedScheme.accent.g, trackedScheme.accent.b)
    else
        nameText:SetTextColor(0.08, 0.72, 0.65)  -- Teal color for tracked items
    end

    -- Count text (just show current count, or count/target if target set)
    local countStr
    if matInfo.needed > 0 then
        if matInfo.have >= matInfo.needed then
            local excess = matInfo.have - matInfo.needed
            countStr = string.format("|cFF88FF88%s|r/%s", BreakUpLargeNumbers(matInfo.have), BreakUpLargeNumbers(matInfo.needed))
        else
            countStr = string.format("%s/%s", BreakUpLargeNumbers(matInfo.have), BreakUpLargeNumbers(matInfo.needed))
        end
    else
        countStr = string.format("|cFFe2e8f0%s|r", BreakUpLargeNumbers(matInfo.have))
    end

    local countText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    countText:SetPoint("RIGHT", -20, 0)
    ApplyFont(countText, trackedScheme, "small")
    countText:SetText(countStr)

    -- X button to remove from tracking
    local removeBtn = CreateFrame("Button", nil, row)
    removeBtn:SetSize(14, 14)
    removeBtn:SetPoint("RIGHT", -3, 0)
    local removeTex = removeBtn:CreateTexture(nil, "ARTWORK")
    removeTex:SetAllPoints()
    removeTex:SetTexture("Interface\\Buttons\\UI-StopButton")
    local removeScheme = GetScheme()
    if removeScheme then
        removeTex:SetVertexColor(removeScheme.text_dim.r, removeScheme.text_dim.g, removeScheme.text_dim.b, 0.8)
    else
        removeTex:SetVertexColor(0.6, 0.6, 0.6, 0.8)
    end
    removeBtn:SetScript("OnEnter", function(self)
        local hoverScheme = GetScheme()
        if hoverScheme then
            removeTex:SetVertexColor(hoverScheme.error.r, hoverScheme.error.g, hoverScheme.error.b, 1)
        else
            removeTex:SetVertexColor(1, 0.3, 0.3, 1)
        end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(L["BTN_STOP_TRACKING"] or "Stop Tracking", 1, 0.3, 0.3)
        GameTooltip:AddLine("Click to remove from tracked reagents", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    removeBtn:SetScript("OnLeave", function()
        local leaveScheme = GetScheme()
        if leaveScheme then
            removeTex:SetVertexColor(leaveScheme.text_dim.r, leaveScheme.text_dim.g, leaveScheme.text_dim.b, 0.8)
        else
            removeTex:SetVertexColor(0.6, 0.6, 0.6, 0.8)
        end
        GameTooltip:Hide()
    end)
    removeBtn:SetScript("OnClick", function()
        HDG_LumberTracker.UntrackReagent(reagentId)
    end)

    -- Click handler for row (AH search)
    local clickButton = CreateFrame("Button", nil, row)
    clickButton:SetPoint("TOPLEFT", 0, 0)
    clickButton:SetPoint("BOTTOMRIGHT", removeBtn, "BOTTOMLEFT", -2, 0)
    clickButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    clickButton:SetScript("OnClick", function(_, button)
        if button == "LeftButton" and not IsShiftKeyDown() then
            if not AuctionHouseFrame or not AuctionHouseFrame:IsShown() then
                print("|cFFfcd34d[Housing Decor Guide]|r Please open the Auction House first")
            elseif AuctionHouseFrame.SearchBar and AuctionHouseFrame.SearchBar.SearchBox then
                AuctionHouseFrame.SearchBar.SearchBox:SetText(matName)
                print("|cFFfcd34d[Housing Decor Guide]|r Set AH search to: " .. matName)
            end
        elseif button == "LeftButton" and IsShiftKeyDown() then
            if C_AddOns.IsAddOnLoaded("Auctionator") and Auctionator and Auctionator.API and Auctionator.API.v1 and Auctionator.API.v1.MultiSearch then
                Auctionator.API.v1.MultiSearch("HousingDecorGuide", {matName})
                print("|cFFfcd34d[Housing Decor Guide]|r Searching Auctionator for: " .. matName)
            end
        end
    end)

    clickButton:SetScript("OnEnter", function(self)
        local hoverScheme = GetScheme()
        if hoverScheme then
            row:SetBackdropColor(hoverScheme.accent.r, hoverScheme.accent.g, hoverScheme.accent.b, 0.5)
        else
            row:SetBackdropColor(0.1, 0.4, 0.38, 0.5)  -- Lighter on hover
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetItemByID(reagentId)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("|cFF14b8a6Tracked Reagent|r", 0.08, 0.72, 0.65)
        GameTooltip:AddDoubleLine("Have:", BreakUpLargeNumbers(matInfo.have), 1, 1, 1, 1, 1, 1)
        if matInfo.needed > 0 then
            GameTooltip:AddDoubleLine("Target:", BreakUpLargeNumbers(matInfo.needed), 1, 1, 1, 1, 1, 1)
            if matInfo.have >= matInfo.needed then
                GameTooltip:AddLine("|cFF88FF88Target reached!|r")
            else
                local shortfall = matInfo.needed - matInfo.have
                GameTooltip:AddLine(string.format("|cFFFF5555Need %s more|r", BreakUpLargeNumbers(shortfall)))
            end
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("|cFF00FF00Click|r to set AH search text", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("|cFF00FF00Shift-Click|r to search Auctionator", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("|cFFFF5555X button|r to stop tracking", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Stays tracked even if recipe removed", 0.78, 0.78, 0.78)
        GameTooltip:Show()
    end)
    clickButton:SetScript("OnLeave", function()
        local leaveScheme = GetScheme()
        if leaveScheme then
            row:SetBackdropColor(leaveScheme.accent.r, leaveScheme.accent.g, leaveScheme.accent.b, 0.3)
        else
            row:SetBackdropColor(0.08, 0.35, 0.33, 0.3)
        end
        GameTooltip:Hide()
    end)

    return yOffset - (ROW_HEIGHT + 1)
end

-- ============================================================================
-- POOLED PINNED LUMBER ROWS (avoid frame churn on every refresh)
-- Rows are created once per lumber type and updated in-place.
-- ============================================================================

local function CreatePooledProgressBar(parent, scheme)
    local bar = CreateFrame("StatusBar", nil, parent, "BackdropTemplate")
    bar:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    local bg = bar:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(bar)
    bg:SetTexture(CONFIG.TEXTURES.STATUSBAR)
    bg:SetVertexColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.7)
    bar._bg = bg
    local overlay = bar:CreateTexture(nil, "OVERLAY")
    overlay:SetAllPoints(bar)
    overlay:SetTexture(CONFIG.TEXTURES.STATUSBAR)
    overlay:SetVertexColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.15)
    overlay:SetDrawLayer("OVERLAY", 1)
    bar._overlay = overlay
    local text = bar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    text:SetPoint("CENTER", 0, 0)
    text:SetJustifyH("CENTER")
    text:SetDrawLayer("OVERLAY", 2)
    text:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    if HDG and HDG.Theme then HDG.Theme.ApplyFont(text, scheme, "small") end
    text:SetWidth(76)
    text:SetWordWrap(false)
    bar._text = text
    return bar
end

local function UpdateProgressBar(bar, have, needed, xPos, yOffset)
    local maxVal = math.max(1, needed)
    bar:SetMinMaxValues(0, maxVal)
    bar:SetValue(math.min(have, maxVal))
    local pct = needed > 0 and (have / needed * 100) or 0
    bar:SetStatusBarColor(HDG_UIHelpers.GetProgressBarColor(pct))
    bar:ClearAllPoints()
    bar:SetPoint("LEFT", xPos, yOffset)
    if have >= needed and needed > 0 then
        local extra = have - needed
        bar._text:SetText(extra > 0 and string.format("%d/%d+%d", needed, needed, extra) or
            string.format("%d/%d", needed, needed))
    else
        bar._text:SetText(string.format("%d/%d", have, needed))
    end
    bar:Show()
end

local function CreatePinnedLumberRow(parent, lumberId, lumberInfo)
    local scheme = GetScheme()
    local row = CreateFrame("Button", nil, parent, "BackdropTemplate")
    row:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    row:SetBackdropColor(unpack(HDG_UIHelpers.BG_TRANSPARENT))

    C_Item.RequestLoadItemDataByID(lumberId)

    -- Tooltip reads live data from row._td
    row:SetScript("OnEnter", function(self)
        local td = self._td or {}
        if not td.isActiveFarming then
            self:SetBackdropColor(scheme.button_hover.r, scheme.button_hover.g, scheme.button_hover.b, 0.6)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        local itemLink = select(2, C_Item.GetItemInfo(lumberId))
        if itemLink then GameTooltip:SetHyperlink(itemLink)
        else GameTooltip:SetText(lumberInfo.name or "Lumber", 1, 1, 1) end
        GameTooltip:AddLine(" ")
        local expColor = HDG_ExpansionData.GetColor(lumberInfo.expansion)
        GameTooltip:AddDoubleLine("Expansion:", lumberInfo.expansion,
            1, 1, 1, expColor[1], expColor[2], expColor[3])
        GameTooltip:AddLine(" ")
        if td.shoppingData then
            GameTooltip:AddLine("|cFFfcd34dCrafting Queue:|r", 1, 1, 1)
            GameTooltip:AddDoubleLine("  Account Stock:",
                BreakUpLargeNumbers(td.shoppingData.have), 0.8, 0.8, 0.8, 1, 1, 1)
            if td.shoppingData.breakdown and #td.shoppingData.breakdown > 0 then
                for _, ci in ipairs(td.shoppingData.breakdown) do
                    local pfx = ci.isCurrent and "    * " or "    "
                    GameTooltip:AddDoubleLine(pfx .. ci.name .. ":",
                        BreakUpLargeNumbers(ci.count), 0.6, 0.6, 0.6, 0.8, 0.8, 0.8)
                end
            end
            GameTooltip:AddDoubleLine("  Needed:",
                BreakUpLargeNumbers(td.shoppingData.needed), 0.8, 0.8, 0.8, 1, 1, 1)
            if td.shoppingData.have >= td.shoppingData.needed then
                GameTooltip:AddLine("  |cFF88FF88Have enough for crafting queue|r", 0.5, 1, 0.5)
            else
                local shortfall = td.shoppingData.needed - td.shoppingData.have
                GameTooltip:AddLine(string.format("  |cFFFF5555Need %s more|r",
                    BreakUpLargeNumbers(shortfall)), 1, 0.3, 0.3)
            end
        else
            GameTooltip:AddLine("|cFF888888Not needed for crafting queue|r", 0.6, 0.6, 0.6)
        end
        GameTooltip:AddLine(" ")
        if td.allData then
            if td.allData.needed > 0 then
                GameTooltip:AddLine("|cFFfcd34dUncollected crafted Decor needs:|r", 1, 1, 1)
                GameTooltip:AddDoubleLine("  Account Stock:",
                    BreakUpLargeNumbers(td.allData.have), 0.8, 0.8, 0.8, 1, 1, 1)
                if td.allData.breakdown and #td.allData.breakdown > 0 then
                    for _, ci in ipairs(td.allData.breakdown) do
                        local pfx = ci.isCurrent and "    * " or "    "
                        GameTooltip:AddDoubleLine(pfx .. ci.name .. ":",
                            BreakUpLargeNumbers(ci.count), 0.6, 0.6, 0.6, 0.8, 0.8, 0.8)
                    end
                end
                GameTooltip:AddDoubleLine("  Total Needed:",
                    BreakUpLargeNumbers(td.allData.needed), 0.8, 0.8, 0.8, 1, 1, 1)
                if td.allData.have >= td.allData.needed then
                    GameTooltip:AddLine("  |cFF88FF88Have enough for all recipes|r", 0.5, 1, 0.5)
                else
                    local shortfall = td.allData.needed - td.allData.have
                    GameTooltip:AddLine(string.format("  |cFFFF5555Need %s more|r",
                        BreakUpLargeNumbers(shortfall)), 1, 0.3, 0.3)
                end
            else
                GameTooltip:AddLine("|cFF888888All recipes using this collected|r", 0.6, 0.6, 0.6)
            end
        else
            GameTooltip:AddLine("|cFF888888All recipes using this collected|r", 0.6, 0.6, 0.6)
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("|cFFFFAA00Lumber must be farmed|r", 1, 0.8, 0)
        GameTooltip:Show()
    end)
    row:SetScript("OnLeave", function(self)
        if not (self._td and self._td.isActiveFarming) then
            self:SetBackdropColor(unpack(HDG_UIHelpers.BG_TRANSPARENT))
        end
        GameTooltip:Hide()
    end)

    -- Column 1: Name
    row._nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row._nameText:SetWidth(CONFIG.LUMBER_COL_1)
    row._nameText:SetJustifyH("LEFT")
    row._nameText:SetWordWrap(false)
    if HDG and HDG.Theme then HDG.Theme.ApplyFont(row._nameText, scheme, "small") end

    -- Column 2: Shopping bar + empty text (one shown at a time)
    row._shoppingBar = CreatePooledProgressBar(row, scheme)
    row._shoppingBar:SetSize(CONFIG.LUMBER_COL_2 - 4, 12)
    row._shoppingEmpty = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row._shoppingEmpty:SetWidth(CONFIG.LUMBER_COL_2)
    row._shoppingEmpty:SetJustifyH("CENTER")
    row._shoppingEmpty:SetText("-")
    row._shoppingEmpty:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
    if HDG and HDG.Theme then HDG.Theme.ApplyFont(row._shoppingEmpty, scheme, "small") end

    -- Column 3: All bar + stock text (one shown at a time)
    row._allBar = CreatePooledProgressBar(row, scheme)
    row._allBar:SetSize(CONFIG.LUMBER_COL_3 - 4, 12)
    row._stockText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row._stockText:SetWidth(CONFIG.LUMBER_COL_3)
    row._stockText:SetJustifyH("CENTER")
    if HDG and HDG.Theme then HDG.Theme.ApplyFont(row._stockText, scheme, "small") end

    -- Farming section (pre-created, hidden when not farming)
    row._statsText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row._statsText:SetPoint("LEFT", 4, 0)
    row._statsText:SetPoint("RIGHT", -4, 0)
    row._statsText:SetJustifyH("LEFT")
    if HDG and HDG.Theme then HDG.Theme.ApplyFont(row._statsText, scheme, "small") end
    row._statsText:Hide()

    row._zoneText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row._zoneText:SetJustifyH("LEFT")
    if HDG and HDG.Theme then HDG.Theme.ApplyFont(row._zoneText, scheme, "small", 9) end
    row._zoneText:Hide()

    -- Flipbook animation texture (reused)
    row._flipbook = row:CreateTexture(nil, "BACKGROUND")
    row._flipbook:SetSize(32, 28)
    row._flipbook:SetAtlas("housing-dashboard-fillbar-icon-fx-flipbook", false)
    row._flipbook:SetAlpha(0)
    row._flipbook:Hide()
    local ag = row._flipbook:CreateAnimationGroup()
    ag:SetLooping("NONE")
    local flip = ag:CreateAnimation("Flipbook")
    flip:SetDuration(1.0)
    flip:SetFlipBookColumns(5)
    flip:SetFlipBookRows(8)
    flip:SetFlipBookFrames(38)
    local fadeOut = ag:CreateAnimation("Alpha")
    fadeOut:SetStartDelay(0.7)
    fadeOut:SetDuration(0.3)
    fadeOut:SetFromAlpha(0.9)
    fadeOut:SetToAlpha(0)
    row._flipbookAnim = ag

    row._lumberId = lumberId
    return row
end

local function UpdatePinnedLumberRow(row, lumberId, lumberInfo, shoppingData, allData, yOffset, farmingOptions)
    local isActiveFarming = farmingOptions and farmingOptions.isActive
    local isJustCollected = farmingOptions and farmingOptions.isJustCollected
    local animPhase = farmingOptions and farmingOptions.animationPhase or 0
    local farmingStats = farmingOptions and farmingOptions.stats
    local rowHeight = isActiveFarming and 62 or 18
    local contentYOffset = isActiveFarming and 18 or 0
    local scheme = GetScheme()

    row:SetSize(CONFIG.LUMBER_COL_1 + CONFIG.LUMBER_COL_2 + CONFIG.LUMBER_COL_3 + 4, rowHeight)
    row:ClearAllPoints()
    row:SetPoint("TOPLEFT", 0, yOffset)

    -- Tooltip data (read by OnEnter)
    row._td = { shoppingData = shoppingData, allData = allData, isActiveFarming = isActiveFarming }

    -- Active farming backdrop highlight
    if isActiveFarming then
        row:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
    else
        row:SetBackdropColor(unpack(HDG_UIHelpers.BG_TRANSPARENT))
    end

    -- Column 1: Name
    local expColor = HDG_ExpansionData.GetColor(lumberInfo.expansion)
    local displayName = HDG_ExpansionData.GetLumberMaterialName(lumberId) or lumberInfo.shortName or lumberInfo.name
    local nameFont = row._nameText:GetFont()
    if isJustCollected and animPhase == 1 then
        row._nameText:SetFont(nameFont, 14, "")
        row._nameText:SetText(string.format("|cFFFFFFFF%s|r", displayName))
    elseif isJustCollected and animPhase == 2 then
        row._nameText:SetFont(nameFont, 12, "")
        row._nameText:SetText(string.format("|cFFFFD700%s|r", displayName))
    elseif isActiveFarming then
        row._nameText:SetFont(nameFont, 11, "")
        row._nameText:SetText(string.format("|cFFfcd34d%s|r", displayName))
    else
        row._nameText:SetFont(nameFont, 11, "")
        row._nameText:SetText(string.format("|cFF%02x%02x%02x%s|r",
            math.min(255, expColor[1] * 255 * 1.3),
            math.min(255, expColor[2] * 255 * 1.3),
            math.min(255, expColor[3] * 255 * 1.3),
            displayName))
    end
    row._nameText:ClearAllPoints()
    row._nameText:SetPoint("LEFT", 0, contentYOffset)

    -- Column 2: Shopping data
    if shoppingData then
        UpdateProgressBar(row._shoppingBar, shoppingData.have, shoppingData.needed,
            CONFIG.LUMBER_COL_1 + 2, contentYOffset)
        row._shoppingEmpty:Hide()
    else
        row._shoppingBar:Hide()
        row._shoppingEmpty:ClearAllPoints()
        row._shoppingEmpty:SetPoint("LEFT", CONFIG.LUMBER_COL_1 + 2, contentYOffset)
        row._shoppingEmpty:Show()
    end

    -- Column 3: All recipes data
    if allData and allData.needed > 0 then
        UpdateProgressBar(row._allBar, allData.have, allData.needed,
            CONFIG.LUMBER_COL_1 + CONFIG.LUMBER_COL_2 + 4, contentYOffset)
        row._stockText:Hide()
    else
        row._allBar:Hide()
        row._stockText:ClearAllPoints()
        row._stockText:SetPoint("LEFT", CONFIG.LUMBER_COL_1 + CONFIG.LUMBER_COL_2 + 4, contentYOffset)
        local stockCount = allData and allData.have or (Data().GetAccountWideLumberCount(lumberId) or 0)
        row._stockText:SetText(string.format("|cFF14b8a6%s|r", BreakUpLargeNumbers(stockCount)))
        row._stockText:Show()
    end

    -- Farming section
    if isActiveFarming and farmingStats then
        local rateStr = string.format("%d", math.floor(farmingStats.perHour or 0))
        local timeStr = FormatElapsedTime(farmingStats.elapsed or 0)
        row._statsText:SetFont(row._statsText:GetFont(), 11, "")
        row._statsText:SetText(string.format("|cFF88FF88+%d this session|r  |cFFe2e8f0Rate:|r |cFFfcd34d%s/hr|r  |cFF888888(%s)|r",
            farmingStats.collected or 0, rateStr, timeStr))
        row._statsText:Show()

        -- Flipbook animation on collection
        if isJustCollected and animPhase == 1 then
            row._flipbook:ClearAllPoints()
            row._flipbook:SetPoint("LEFT", row._statsText, "LEFT", -7, 0)
            row._flipbook:SetAlpha(0.9)
            row._flipbook:Show()
            row._flipbookAnim:Stop()
            row._flipbookAnim:Play()
        end

        local zoneName = GetZoneText() or "Unknown"
        row._zoneText:ClearAllPoints()
        row._zoneText:SetPoint("BOTTOMLEFT", 4, 6)
        row._zoneText:SetPoint("RIGHT", row, "RIGHT", -4, 0)
        row._zoneText:SetText(string.format("|cFFe2e8f0Zone:|r |cFF87CEEB%s|r", zoneName))
        row._zoneText:Show()
    else
        row._statsText:Hide()
        row._zoneText:Hide()
        row._flipbook:Hide()
    end

    row:Show()
    return yOffset - rowHeight
end

-- Refresh the pinned lumber widget
function HDG_LumberTracker.RefreshPinned()
    if not pinnedFrame or not pinnedFrame.lumberContent then return end

    local needsCacheBuild = lumberNeedsDirty
    if lumberNeedsDirty then
        cachedShoppingLumber = HDG_LumberTracker.CalculateShoppingListLumber(GetShoppingList())
        cachedAllLumber = HDG_LumberTracker.CalculateLumberNeeds(privateDB.professions or {})
        lumberNeedsDirty = false
    end
    -- Update only the "have" counts — single pass to deduplicate GetAccountWideLumberCount calls
    local lumberCounts = {}
    for lumberId in pairs(cachedShoppingLumber) do
        if not lumberCounts[lumberId] then
            local accountTotal, breakdown = Data().GetAccountWideLumberCount(lumberId)
            lumberCounts[lumberId] = { total = accountTotal, breakdown = breakdown }
        end
    end
    for lumberId in pairs(cachedAllLumber) do
        if not lumberCounts[lumberId] then
            local accountTotal, breakdown = Data().GetAccountWideLumberCount(lumberId)
            lumberCounts[lumberId] = { total = accountTotal, breakdown = breakdown }
        end
    end
    for lumberId, data in pairs(cachedShoppingLumber) do
        local cached = lumberCounts[lumberId]
        data.have = cached.total
        data.breakdown = cached.breakdown
    end
    for lumberId, data in pairs(cachedAllLumber) do
        local cached = lumberCounts[lumberId]
        data.have = cached.total
        data.breakdown = cached.breakdown
    end

    -- Build a snapshot of current visual state to detect changes
    local currentState = {}
    local stateIdx = 0
    for lumberId, data in pairs(cachedShoppingLumber) do
        stateIdx = stateIdx + 1
        currentState[stateIdx] = lumberId
        stateIdx = stateIdx + 1
        currentState[stateIdx] = data.have
        stateIdx = stateIdx + 1
        currentState[stateIdx] = data.needed
    end
    for lumberId, data in pairs(cachedAllLumber) do
        stateIdx = stateIdx + 1
        currentState[stateIdx] = lumberId
        stateIdx = stateIdx + 1
        currentState[stateIdx] = data.have
        stateIdx = stateIdx + 1
        currentState[stateIdx] = data.needed
    end
    -- Include farming/animation state so those refreshes always go through
    stateIdx = stateIdx + 1
    currentState[stateIdx] = activeFarmingLumber or 0
    stateIdx = stateIdx + 1
    currentState[stateIdx] = justCollectedLumber or 0
    stateIdx = stateIdx + 1
    currentState[stateIdx] = animationPhase

    -- Skip UI rebuild if nothing visually changed
    if pinnedDisplayState and not needsCacheBuild then
        local unchanged = (#currentState == #pinnedDisplayState)
        if unchanged then
            for i = 1, #currentState do
                if currentState[i] ~= pinnedDisplayState[i] then
                    unchanged = false
                    break
                end
            end
        end
        if unchanged then return end
    end
    pinnedDisplayState = currentState

    local shoppingLumber = cachedShoppingLumber
    local allLumber = cachedAllLumber
    local expansionOrder = HDG_ExpansionData.LUMBER_ORDER

    local yOffset = 0
    local isFarming = (activeFarmingLumber ~= nil)
    local focusMode = GetConfig("lumberFocusMode") and isFarming

    -- Hide all pooled rows, then show/update visible ones
    for _, row in pairs(pinnedLumberRows) do
        row:Hide()
    end

    -- Update lumber rows in-place (no frame creation after first pass)
    for _, lumberId in ipairs(expansionOrder) do
        if focusMode and lumberId ~= activeFarmingLumber then
            -- Skip this lumber row
        else
            local lumberInfo = LUMBER_TYPES[lumberId]
            if lumberInfo then
                local row = pinnedLumberRows[lumberId]
                if not row then
                    row = CreatePinnedLumberRow(pinnedFrame.lumberContent, lumberId, lumberInfo)
                    pinnedLumberRows[lumberId] = row
                end

                local shoppingData = shoppingLumber[lumberId]
                local allData = allLumber[lumberId]
                local farmingStats = GetFarmingStats(lumberId)
                local farmingOptions = {
                    scheme = GetScheme(),
                    isActive = (activeFarmingLumber == lumberId),
                    isJustCollected = (justCollectedLumber == lumberId),
                    animationPhase = animationPhase,
                    stats = ((activeFarmingLumber == lumberId) or (justCollectedLumber == lumberId)) and farmingStats and {
                        collected = farmingStats.collected,
                        perHour = farmingStats.perHour,
                        elapsed = farmingStats.elapsed
                    } or nil
                }
                yOffset = UpdatePinnedLumberRow(row, lumberId, lumberInfo, shoppingData, allData, yOffset, farmingOptions)
            end
        end
    end

    -- Calculate lumber panel height based on content (accounts for expanded farming row)
    local lumberContentHeight = math.abs(yOffset)
    local fw = FW()
    local lumberPanelHeight = (fw.LUMBER_HEADER_HEIGHT or 18) + lumberContentHeight + 4

    -- Update content and panel heights dynamically
    if pinnedFrame.lumberContent then
        pinnedFrame.lumberContent:SetHeight(lumberContentHeight)
    end
    if pinnedFrame.lumberPanel then
        pinnedFrame.lumberPanel:SetHeight(lumberPanelHeight)
    end

    -- Handle materials section visibility and content
    -- Hide materials when actively farming lumber
    local showMaterials = GetConfig("showLumberMaterials") and not isFarming
    local totalCost = 0
    local missingPrices = 0

    if pinnedFrame.materialsPanel then
        if showMaterials then
            pinnedFrame.materialsPanel:Show()

            -- Update toggle button appearance
            if pinnedFrame.materialsToggleUpdate then
                pinnedFrame.materialsToggleUpdate()
            end

            -- Clear and refresh materials content
            if pinnedFrame.materialsContent then
                HDG_UIHelpers.ClearScrollContent(pinnedFrame.materialsContent)

                local matYOffset = 0
                local rowWidth = (FW().LUMBER_WIDTH or 280) - 36  -- Account for scrollbar

                -- Get all non-lumber materials from shopping list (Direct or Raw based on setting)
                local materials
                local shoppingList = GetShoppingList()
                local isRaw = GetConfig("showRawMaterials")
                if materialsCacheDirty then
                    cachedNonLumberMaterials = CalculateNonLumberMaterials(shoppingList)
                    cachedRawNonLumberMaterials = CalculateRawNonLumberMaterials(shoppingList)
                    materialsCacheDirty = false
                end
                materials = isRaw and cachedRawNonLumberMaterials or cachedNonLumberMaterials
                -- Update "have" counts cheaply (bag contents change on every refresh)
                for id, mat in pairs(materials) do
                    mat.have = Data().GetItemCount(id)
                end

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

                    for _, mat in ipairs(sortedMaterials) do
                        matYOffset = CreateMaterialRow(pinnedFrame.materialsContent, mat.id, mat.info, matYOffset, rowWidth)

                        -- Calculate pricing
                        local price = Data().GetItemPrice and Data().GetItemPrice(mat.id)
                        if price and price > 0 then
                            local stillNeeded = math.max(0, mat.info.needed - mat.info.have)
                            totalCost = totalCost + (price * stillNeeded)
                        elseif mat.info.have < mat.info.needed then
                            missingPrices = missingPrices + 1
                        end
                    end
                else
                    local emptyMatsScheme = GetScheme()
                    local emptyText = pinnedFrame.materialsContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
                    emptyText:SetPoint("TOPLEFT", 4, matYOffset)
                    emptyText:SetText("|cFF888888" .. (L["STATUS_NO_MATERIALS_QUEUED"] or "No materials in crafting queue") .. "|r")
                    ApplyFont(emptyText, emptyMatsScheme, "small")
                    matYOffset = matYOffset - 14
                end

                -- Add tracked reagents section if any are being tracked
                local trackedReagents = GetTrackedReagents()
                if next(trackedReagents) then
                    -- Add separator line
                    matYOffset = matYOffset - 6
                    local separator = pinnedFrame.materialsContent:CreateTexture(nil, "ARTWORK")
                    local sepScheme = GetScheme()
                    if sepScheme then
                        separator:SetColorTexture(sepScheme.border.r, sepScheme.border.g, sepScheme.border.b, 0.6)
                    else
                        separator:SetColorTexture(0.3, 0.3, 0.3, 0.6)
                    end
                    separator:SetSize(rowWidth, 1)
                    separator:SetPoint("TOPLEFT", 4, matYOffset)
                    matYOffset = matYOffset - 6

                    -- Add header
                    local trackedHeader = pinnedFrame.materialsContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
                    trackedHeader:SetPoint("TOPLEFT", 4, matYOffset)
                    ApplyFont(trackedHeader, sepScheme, "small")
                    trackedHeader:SetText("|cFF14b8a6" .. (L["HEADER_TRACKED_REAGENTS"] or "Tracked Reagents") .. "|r")
                    matYOffset = matYOffset - 14

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

                    -- Render tracked reagent rows
                    for _, tracked in ipairs(sortedTracked) do
                        matYOffset = CreateTrackedReagentRow(pinnedFrame.materialsContent, tracked.id, tracked.info, matYOffset, rowWidth)
                    end
                end

                -- Set scroll content height based on actual content
                local contentHeight = math.abs(matYOffset) + 4
                pinnedFrame.materialsContent:SetHeight(contentHeight)
            end

            -- Set expanded height (header + lumber panel + materials panel + footer padding)
            local fw = FW()
            pinnedFrame:SetHeight((fw.HEADER_HEIGHT or 34) + lumberPanelHeight + (fw.LUMBER_MATERIALS_HEIGHT or 372) + (fw.FOOTER_PADDING or 8))
        else
            pinnedFrame.materialsPanel:Hide()
            -- Set compact height (header + lumber panel + footer padding)
            local fw = FW()
            pinnedFrame:SetHeight((fw.HEADER_HEIGHT or 34) + lumberPanelHeight + (fw.FOOTER_PADDING or 8))
        end
    end

    -- Update materials button icon to show disabled state when farming
    if pinnedFrame.materialsBtn and pinnedFrame.materialsBtn.icon then
        if isFarming then
            pinnedFrame.materialsBtn.icon:SetDesaturated(true)
            local disabledScheme = GetScheme()
            if disabledScheme then
                pinnedFrame.materialsBtn.icon:SetVertexColor(disabledScheme.text_dim.r, disabledScheme.text_dim.g, disabledScheme.text_dim.b, 0.5)
            else
                pinnedFrame.materialsBtn.icon:SetVertexColor(0.4, 0.4, 0.4, 0.5)
            end
        elseif pinnedFrame.materialsBtn.UpdateIcon then
            pinnedFrame.materialsBtn.UpdateIcon()
        end
    end

    -- Update cost footer (persistent element)
    if pinnedFrame.costText then
        if showMaterials then
            local hasPriceAddon, priceAddonName
            if Data().HasPriceAddon then
                hasPriceAddon, priceAddonName = Data().HasPriceAddon()
            end
            if hasPriceAddon then
                local costStr = Data().FormatGold and Data().FormatGold(totalCost) or tostring(totalCost)
                if missingPrices > 0 then
                    pinnedFrame.costText:SetText(string.format("|cFFe2e8f0Est:|r |cFFfcd34d~%s+|r |cFF888888(%d unknown)|r", costStr, missingPrices))
                elseif totalCost > 0 then
                    pinnedFrame.costText:SetText(string.format("|cFFe2e8f0Est:|r |cFFfcd34d~%s|r |cFF888888(%s)|r", costStr, priceAddonName or "AH"))
                else
                    pinnedFrame.costText:SetText("|cFFe2e8f0Est:|r |cFF88FF88Have all materials!|r")
                end
            else
                pinnedFrame.costText:SetText("|cFF888888Install TSM/Auctionator for prices|r")
            end
            pinnedFrame.costText:Show()
        else
            pinnedFrame.costText:SetText("")
            pinnedFrame.costText:Hide()
        end
    end
end

-- ============================================================================
-- FRAME ACCESSORS
-- ============================================================================

function HDG_LumberTracker.GetFrame()
    return lumberFrame
end

function HDG_LumberTracker.GetPinnedFrame()
    return pinnedFrame
end

-- ============================================================================
-- GATHERING TRACKER API
-- ============================================================================

--- Add a reagent to tracked list
-- @param itemID The item ID to track
-- @param name Optional item name (will be looked up if not provided)
-- @param targetQty Optional target quantity (defaults to shopping list need or 0)
function HDG_LumberTracker.TrackReagent(itemID, name, targetQty)
    if not itemID then return false end

    -- Don't track lumber (already tracked separately)
    if LUMBER_TYPES[itemID] then
        print("|cFFfcd34d[Housing Decor Guide]|r Lumber is already tracked in the Lumber section")
        return false
    end

    -- Check if already tracked
    local trackedReagents = GetTrackedReagents()
    if trackedReagents[itemID] then
        print("|cFFfcd34d[Housing Decor Guide]|r Already tracking this reagent")
        return false
    end

    -- Get item name if not provided
    local itemName = name
    if not itemName then
        itemName = Data().GetCachedItemName and Data().GetCachedItemName(itemID) or "Unknown"
    end

    -- Add to tracked list via Store
    Dispatch("ADD_TRACKED_REAGENT", {
        itemID = itemID,
        name = itemName,
        targetQty = targetQty or 0,
        addedTime = time()
    })

    print("|cFFfcd34d[Housing Decor Guide]|r Now tracking: " .. itemName)

    -- Refresh pinned widget if visible
    if HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.RefreshPinned()
    end

    return true
end

--- Remove a reagent from tracked list
-- @param itemID The item ID to stop tracking
function HDG_LumberTracker.UntrackReagent(itemID)
    if not itemID then return false end

    local trackedReagents = GetTrackedReagents()
    local tracked = trackedReagents[itemID]
    if not tracked then return false end

    local itemName = tracked.name or "Unknown"
    Dispatch("REMOVE_TRACKED_REAGENT", { itemID = itemID })

    print("|cFFfcd34d[Housing Decor Guide]|r Stopped tracking: " .. itemName)

    -- Refresh pinned widget if visible
    if HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.RefreshPinned()
    end

    return true
end

--- Check if a reagent is being tracked
-- @param itemID The item ID to check
-- @return boolean
function HDG_LumberTracker.IsReagentTracked(itemID)
    if not itemID then return false end
    return GetTrackedReagents()[itemID] ~= nil
end

--- Get all tracked reagents
-- @return table of tracked reagents
function HDG_LumberTracker.GetAllTrackedReagents()
    return GetTrackedReagents()
end

--- Update target quantity for a tracked reagent
-- @param itemID The item ID
-- @param targetQty The new target quantity
function HDG_LumberTracker.UpdateTrackedTarget(itemID, targetQty)
    if not itemID then return false end
    local trackedReagents = GetTrackedReagents()
    if not trackedReagents[itemID] then return false end

    Dispatch("UPDATE_TRACKED_REAGENT", { itemID = itemID, targetQty = targetQty or 0 })

    -- Refresh pinned widget if visible
    if HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.RefreshPinned()
    end

    return true
end
