--

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 GetCraftingQueue()
    local state = GetState()
    return state and state.craftingQueue or {}
end

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

-- Local references
local mainFrame
local pinnedFrame  -- Independent frame parented to UIParent (persists when main window closes)

-- 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 cachedQueueLumber = nil -- Cached result of CalculateCraftingQueueLumber
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
local farmingTicker = nil        -- 1-second ticker that refreshes pinned widget during active farming

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

-- O(1) lookup for ACHIEVEMENT_EARNED filtering
local LUMBER_ACHIEVE_IDS = {}
for _, info in pairs(LUMBER_TYPES) do
    if info.achieveID then LUMBER_ACHIEVE_IDS[info.achieveID] = true end
end

local GetLumberAchievementProgress = HDG_ExpansionData.GetLumberAchievementProgress

-- Public: check if an achievement ID is a lumber mounted-harvesting achievement
function HDG_LumberTracker.IsLumberAchievement(achieveID)
    return LUMBER_ACHIEVE_IDS[achieveID] == true
end

-- Public: invalidate pinned display state to force refresh
function HDG_LumberTracker.InvalidateDisplayState()
    pinnedDisplayState = nil
end

-- ============================================================================
-- 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
    if farmingTicker then
        farmingTicker:Cancel()
        farmingTicker = nil
    end
end

-- Start a 1-second ticker to refresh visible lumber widget during active farming (keeps elapsed/perHour live)
local function StartFarmingTicker()
    if farmingTicker then return end -- already running
    farmingTicker = C_Timer.NewTicker(1.0, function()
        if not activeFarmingLumber then
            farmingTicker:Cancel()
            farmingTicker = nil
            return
        end
        if HDG_LumberTracker.IsPinnedVisible() then
            HDG_LumberTracker.RefreshPinned()
        end
    end)
end

-- Save lumber gathering coordinates for later analysis (heatmaps, route optimization, etc.)
local function SaveLumberCoordinates(lumberId, quantity)
    -- 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 and dispatch to Store (persists to HDG_DB)
    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()
    }

    Dispatch("ADD_LUMBER_COORDINATE", { node = entry })

    -- Batch prune when 20+ over limit
    local state = GetState()
    local coords = state and state.lumberCoordinates
    if coords then
        local maxCoords = CONFIG.MAX_LUMBER_COORDINATES or 500
        if #coords > maxCoords + 20 then
            local newCoords = {}
            for i = #coords - maxCoords + 1, #coords do
                newCoords[#newCoords + 1] = coords[i]
            end
            Dispatch("SET_LUMBER_COORDINATES", { coordinates = newCoords })
        end
    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

-- Speaker color lookup for lumber quotes (replaces inline if/elseif chain)
local SPEAKER_COLORS = {
    GOBLIN   = { name = "Gazlowe", color = "FFD100" },
    SQUIRREL = { name = "Squirrel", color = "A0522D" },
}
local SPEAKER_DEFAULT = { name = "Vamoose", color = "0070DE" }

-- Refresh whichever lumber widget is currently visible
local function RefreshVisibleWidget()
    if HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.RefreshPinned()
    end
end

-- Auto-show pinned widget on collection (if setting enabled)
-- Pinned widget handles collapse mode natively (only shows active row when farming)
local function ApplyAutoShowBehavior()
    local autoShowEnabled = (GetConfig("lumberAutoShow") == true)
    if not autoShowEnabled then return end
    if not HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.ShowPinned()
    end
end

-- Fire a Vamoose quote on lumber collection (if quotes enabled)
local function TriggerLumberQuote(lumberId, increase)
    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 not (quotesEnabled and lumberQuotesEnabled and chattiness > 0 and privateDB.HDG_Vamoose) then return end

    local quote, speaker = privateDB.HDG_Vamoose.OnLumberCollected(lumberId, increase, chattiness)
    if not quote then return end
    local onlyChat = GetConfig("quotesOnlyChat") or false
    if not onlyChat then
        privateDB.HDG_Vamoose.DisplayQuote(quote, speaker)
    end
    if onlyChat then
        local sp = SPEAKER_COLORS[speaker] or SPEAKER_DEFAULT
        print(string.format("|cFF%s[%s]|r %s", sp.color, sp.name, quote))
    end
end

-- Run the 3-phase collection animation (grow -> hold -> shrink)
local function TriggerCollectionAnimation(lumberId)
    justCollectedLumber = lumberId
    animationPhase = 1
    if animationTimer then animationTimer:Cancel() end
    RefreshVisibleWidget()
    animationTimer = C_Timer.NewTimer(0.2, function()
        animationPhase = 2
        RefreshVisibleWidget()
        animationTimer = C_Timer.NewTimer(0.15, function()
            justCollectedLumber = nil
            animationPhase = 0
            RefreshVisibleWidget()
        end)
    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,
                    sessionId = lumberId .. "_" .. time(),
                    lastCollectTime = now
                }
            end

            -- Update stats
            sessionFarmingStats[lumberId].collected = sessionFarmingStats[lumberId].collected + increase
            sessionFarmingStats[lumberId].lastCollectTime = now
            sessionFarmingStats[lumberId].zone = currentZone

            SaveFarmingSession(lumberId)
            SaveLumberCoordinates(lumberId, increase)
            activeFarmingLumber = lumberId
            StartFarmingTicker()
            ApplyAutoShowBehavior()
            TriggerLumberQuote(lumberId, increase)
            TriggerCollectionAnimation(lumberId)
        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
-- ============================================================================

-- Ensure a lumber entry exists in the table, using account-wide counts
local function EnsureLumberEntry(tbl, reagentID)
    if not tbl[reagentID] then
        local accountTotal, breakdown = Data().GetAccountWideLumberCount(reagentID)
        tbl[reagentID] = {
            id = reagentID,
            name = LUMBER_TYPES[reagentID].name,
            expansion = LUMBER_TYPES[reagentID].expansion,
            needed = 0,
            have = accountTotal,
            breakdown = breakdown,
        }
    end
    return tbl[reagentID]
end

-- Ensure a non-lumber reagent entry exists in the table
local function EnsureReagentEntry(tbl, reagentID, expansion)
    if not tbl[reagentID] then
        local reagentName = Data().GetCachedItemName(reagentID) or "Unknown"
        local reagentSource = (HDG_ReagentsDB and HDG_ReagentsDB[reagentID] and HDG_ReagentsDB[reagentID][1]) or "Unknown"
        tbl[reagentID] = {
            id = reagentID,
            name = reagentName,
            source = reagentSource,
            expansion = expansion,
            needed = 0,
            have = Data().GetItemCount(reagentID)
        }
    end
    return tbl[reagentID]
end

-- Accumulate direct (non-lumber) materials from a queue item's reagents
local function AccumulateDirectMaterials(materials, item)
    for reagentKey, reagent in pairs(item.reagents) do
        local reagentID = reagent.id or reagentKey
        if not LUMBER_TYPES[reagentID] then
            EnsureReagentEntry(materials, reagentID, item.expansion)
            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

-- 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
                        EnsureLumberEntry(lumberNeeds, reagentID)
                        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 crafting queue only
-- Uses account-wide lumber counts (summed across all characters)
function HDG_LumberTracker.CalculateCraftingQueueLumber(queue)
    local lumberNeeds = {}

    for i, item in ipairs(queue) do
        if item.reagents then
            for reagentKey, reagent in pairs(item.reagents) do
                local reagentID = reagent.id or reagentKey
                if LUMBER_TYPES[reagentID] then
                    local entry = EnsureLumberEntry(lumberNeeds, reagentID)
                    if not entry.sources then entry.sources = {} end
                    local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                    entry.needed = entry.needed + (reagentAmount * item.quantity)
                    entry.sources[i] = true
                end
            end
        end
    end

    return lumberNeeds
end

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

function HDG_LumberTracker.GetCachedLumberData()
    return cachedQueueLumber, cachedAllLumber
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
    -- Refresh pinned widget to reflect session ended
    if 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, queueData, 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, queueData, 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

    -- Refresh pinned widget to show active farming state
    if HDG_LumberTracker.IsPinnedVisible() 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 (CreateQueuePanel)
-- 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 — pass drag events through to parent for moving
    titleBtn:RegisterForDrag("LeftButton")
    titleBtn:SetScript("OnDragStart", function() pinnedFrame:StartMoving() end)
    titleBtn:SetScript("OnDragStop", function()
        pinnedFrame:StopMovingOrSizing()
        SavePinnedPosition()
    end)

    pinnedFrame.titleBtn = titleBtn
    pinnedFrame.titleTexture = titleTexture

    -- (Achievement badge removed -- per-row progress shown during farming instead)

    -- 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)
    local unpinScheme = GetScheme()
    if unpinScheme then
        unpinIcon:SetVertexColor(unpinScheme.text.r, unpinScheme.text.g, unpinScheme.text.b, 1)
    else
        unpinIcon:SetVertexColor(0.9, 0.9, 0.9, 1)
    end
    unpinBtn.icon = unpinIcon
    unpinBtn:SetScript("OnClick", function()
        Dispatch("SET_LUMBER_PINNED", { pinned = false })
        HDG_LumberTracker.HidePinned()
        -- Restore embedded lumber frame in queue 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(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
        else
            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
        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

    -- Collapse button - toggles between showing all lumber rows vs only active farming row
    local collapseBtn = CreateFrame("Button", nil, pinnedFrame)
    collapseBtn:SetSize(16, 16)
    collapseBtn:SetPoint("TOPRIGHT", materialsBtn, "TOPLEFT", -4, -4)
    collapseBtn:SetNormalAtlas("common-button-collapseExpand-up")
    collapseBtn:SetPushedAtlas("common-button-collapseExpand-up")

    local function UpdateCollapseIcon()
        if not activeFarmingLumber then
            collapseBtn:Hide()
            return
        end
        local atlas = GetConfig("lumberCollapsed") and "common-button-collapseExpand-up" or "common-button-collapseExpand-down"
        collapseBtn:SetNormalAtlas(atlas)
        collapseBtn:SetPushedAtlas(atlas)
        collapseBtn:Show()
    end
    collapseBtn.UpdateIcon = UpdateCollapseIcon

    collapseBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        if GetConfig("lumberCollapsed") then
            GameTooltip:SetText("Show All Lumber", 1, 1, 1)
        else
            GameTooltip:SetText("Show Only Active", 1, 1, 1)
        end
        GameTooltip:Show()
    end)

    collapseBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    collapseBtn:SetScript("OnClick", function()
        Dispatch("SET_CONFIG", { key = "lumberCollapsed", value = not GetConfig("lumberCollapsed") })
        UpdateCollapseIcon()
        HDG_LumberTracker.RefreshPinned()
    end)

    collapseBtn:Hide() -- hidden until farming starts
    pinnedFrame.collapseBtn = collapseBtn

    -- 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")
    materialsHeaderBg:EnableMouse(true)
    materialsHeaderBg:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("Materials Breakdown", 1, 1, 1)
        GameTooltip:AddLine("Full materials list available in\nShopping List > Mats tab", 0.7, 0.8, 0.9, true)
        GameTooltip:Show()
    end)
    materialsHeaderBg:SetScript("OnLeave", function() GameTooltip:Hide() end)
    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.materialsToggleUpdate then
                HDG_RecipesTab.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.materialsToggleUpdate then
                HDG_RecipesTab.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

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

-- Calculate non-lumber materials from crafting queue
local function CalculateNonLumberMaterials(queue)
    local materials = {}
    for _, item in ipairs(queue or {}) do
        if item.reagents then
            AccumulateDirectMaterials(materials, item)
        end
    end
    return materials
end

-- Calculate RAW non-lumber materials from crafting queue (uses HDG_PowerCrafting)
local function CalculateRawNonLumberMaterials(queue)
    local materials = {}

    for _, item in ipairs(queue or {}) do
        if item.reagents and HDG_PowerCrafting then
            -- Convert crafting queue reagents format to HDG_DecorDB format for Power Crafting
            local reagentsForGraph = {}
            for reagentKey, reagent in pairs(item.reagents) do
                local reagentID = reagent.id or reagentKey
                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
            AccumulateDirectMaterials(materials, item)
        end
    end

    return materials
end

function HDG_LumberTracker.GetNonLumberMaterials()
    local queue = GetCraftingQueue()
    if materialsCacheDirty then
        cachedNonLumberMaterials = CalculateNonLumberMaterials(queue)
        cachedRawNonLumberMaterials = CalculateRawNonLumberMaterials(queue)
        materialsCacheDirty = false
    end
    local direct = cachedNonLumberMaterials or {}
    local raw = cachedRawNonLumberMaterials or {}
    for id, mat in pairs(direct) do mat.have = Data().GetItemCount(id) end
    for id, mat in pairs(raw) do mat.have = Data().GetItemCount(id) end
    return direct, raw
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(HDG_UIHelpers.GetProgressBarColor(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.GetCraftingQueueItems then
                    local success, items = pcall(Auctionator.API.v1.GetCraftingQueueItems, callerID, listName)
                    if success and items then existingItems = items end
                end
                local exists = false
                for _, existingItem in ipairs(existingItems) do
                    if existingItem == matName then exists = true; break end
                end
                if not exists then table.insert(existingItems, matName) end
                if Auctionator.API.v1.CreateShoppingList then
                    Auctionator.API.v1.CreateShoppingList(callerID, listName, existingItems)
                    print("|cFFfcd34d[Housing Decor Guide]|r Added '" .. matName .. "' to Auctionator list")
                end
            else
                print("|cFFfcd34d[Housing Decor Guide]|r Install Auctionator for search list integration")
            end
        end
    end)

    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.queueData then
            GameTooltip:AddLine("|cFFfcd34dCrafting Queue:|r", 1, 1, 1)
            GameTooltip:AddDoubleLine("  Account Stock:",
                BreakUpLargeNumbers(td.queueData.have), 0.8, 0.8, 0.8, 1, 1, 1)
            if td.queueData.breakdown and #td.queueData.breakdown > 0 then
                for _, ci in ipairs(td.queueData.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.queueData.needed), 0.8, 0.8, 0.8, 1, 1, 1)
            if td.queueData.have >= td.queueData.needed then
                GameTooltip:AddLine("  |cFF88FF88Have enough for crafting queue|r", 0.5, 1, 0.5)
            else
                local shortfall = td.queueData.needed - td.queueData.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
        local achQty, achReq, achDone = GetLumberAchievementProgress(td.lumberId)
        if achQty then
            GameTooltip:AddLine(" ")
            if achDone then
                GameTooltip:AddLine("Mounted Harvesting: Unlocked!", 0.53, 1, 0.53)
            else
                GameTooltip:AddDoubleLine("Mounted Harvesting:",
                    string.format("%d / %d", achQty, achReq), 0.8, 0.8, 0.8, 1, 0.82, 0.2)
                GameTooltip:AddLine("Gather 250 to unlock mounted lumber farming", 0.6, 0.6, 0.6)
            end
        else
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Lumber must be farmed", 1, 0.67, 0)
        end
        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: Queue bar + empty text (one shown at a time)
    row._queueBar = CreatePooledProgressBar(row, scheme)
    row._queueBar:SetSize(CONFIG.LUMBER_COL_2 - 4, 12)
    row._queueEmpty = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row._queueEmpty:SetWidth(CONFIG.LUMBER_COL_2)
    row._queueEmpty:SetJustifyH("CENTER")
    row._queueEmpty:SetText("-")
    row._queueEmpty: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._queueEmpty, 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()

    row._achieveProgress = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row._achieveProgress:SetJustifyH("RIGHT")
    if HDG and HDG.Theme then HDG.Theme.ApplyFont(row._achieveProgress, scheme, "small", 9) end
    row._achieveProgress: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, queueData, 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 = { lumberId = lumberId, queueData = queueData, 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

    -- Achievement suffix: green * = unlocked, red * = not yet unlocked
    local achSuffix = ""
    local achQty, achReq, achDone = GetLumberAchievementProgress(lumberId)
    if achDone then
        achSuffix = " |cFF88FF88*|r"
    else
        achSuffix = " |cFFe05252*|r"
    end

    local nameFont = row._nameText:GetFont()
    if isJustCollected and animPhase == 1 then
        row._nameText:SetFont(nameFont, 14, "")
        row._nameText:SetText(string.format("|cFFFFFFFF%s|r%s", displayName, achSuffix))
    elseif isJustCollected and animPhase == 2 then
        row._nameText:SetFont(nameFont, 12, "")
        row._nameText:SetText(string.format("|cFFFFD700%s|r%s", displayName, achSuffix))
    elseif isActiveFarming then
        row._nameText:SetFont(nameFont, 11, "")
        row._nameText:SetText(string.format("|cFFfcd34d%s|r%s", displayName, achSuffix))
    else
        row._nameText:SetFont(nameFont, 11, "")
        row._nameText:SetText(string.format("|cFF%02x%02x%02x%s|r%s",
            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, achSuffix))
    end
    row._nameText:ClearAllPoints()
    row._nameText:SetPoint("LEFT", 0, contentYOffset)

    -- Column 2: Queue data
    if queueData then
        UpdateProgressBar(row._queueBar, queueData.have, queueData.needed,
            CONFIG.LUMBER_COL_1 + 2, contentYOffset)
        row._queueEmpty:Hide()
    else
        row._queueBar:Hide()
        row._queueEmpty:ClearAllPoints()
        row._queueEmpty:SetPoint("LEFT", CONFIG.LUMBER_COL_1 + 2, contentYOffset)
        row._queueEmpty: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:SetText(string.format("|cFFe2e8f0Zone:|r |cFF87CEEB%s|r", zoneName))
        row._zoneText:Show()

        if not achDone and achQty and achReq then
            row._achieveProgress:ClearAllPoints()
            row._achieveProgress:SetPoint("BOTTOMRIGHT", -4, 6)
            row._achieveProgress:SetText(string.format("|cFFe05252%d/%d|r", achQty, achReq))
            row._achieveProgress:Show()
        else
            row._achieveProgress:Hide()
        end
    else
        row._statsText:Hide()
        row._zoneText:Hide()
        row._achieveProgress:Hide()
        row._flipbook:Hide()
    end

    row:Show()
    return yOffset - rowHeight
end

-- Render lumber rows into the pinned widget; returns lumberPanelHeight
local function RefreshPinnedLumberRows(queueLumber, allLumber, collapseMode)
    local expansionOrder = HDG_ExpansionData.LUMBER_ORDER
    local yOffset = 0

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

    for _, lumberId in ipairs(expansionOrder) do
        if collapseMode 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 queueData = queueLumber[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, queueData, allData, yOffset, farmingOptions)
            end
        end
    end

    local lumberContentHeight = math.abs(yOffset)
    local fw = FW()
    local lumberPanelHeight = (fw.LUMBER_HEADER_HEIGHT or 18) + lumberContentHeight + 4
    if pinnedFrame.lumberContent then pinnedFrame.lumberContent:SetHeight(lumberContentHeight) end
    if pinnedFrame.lumberPanel then pinnedFrame.lumberPanel:SetHeight(lumberPanelHeight) end
    return lumberPanelHeight
end

-- Render the materials section of the pinned widget; returns totalCost, missingPrices, showMaterials
local function RefreshPinnedMaterials(lumberPanelHeight, isFarming)
    local showMaterials = GetConfig("showLumberMaterials") and not isFarming
    local totalCost = 0
    local missingPrices = 0

    if not pinnedFrame.materialsPanel then
        return totalCost, missingPrices, showMaterials
    end

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

        if pinnedFrame.materialsContent then
            HDG_UIHelpers.ClearScrollContent(pinnedFrame.materialsContent)

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

            -- Get all non-lumber materials (Direct or Raw based on setting)
            local queue = GetCraftingQueue()
            local isRaw = GetConfig("showRawMaterials")
            if materialsCacheDirty then
                cachedNonLumberMaterials = CalculateNonLumberMaterials(queue)
                cachedRawNonLumberMaterials = CalculateRawNonLumberMaterials(queue)
                materialsCacheDirty = false
            end
            local materials = isRaw and cachedRawNonLumberMaterials or cachedNonLumberMaterials
            for id, mat in pairs(materials) do
                mat.have = Data().GetItemCount(id)
            end

            if next(materials) then
                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 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)
                    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
                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

                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

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

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

            local contentHeight = math.abs(matYOffset) + 4
            pinnedFrame.materialsContent:SetHeight(contentHeight)
        end

        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()
        local fw = FW()
        pinnedFrame:SetHeight((fw.HEADER_HEIGHT or 34) + lumberPanelHeight + (fw.FOOTER_PADDING or 8))
    end

    return totalCost, missingPrices, showMaterials
end

-- Update the footer: materials button state, cost text, collapse button
local function RefreshPinnedFooter(showMaterials, totalCost, missingPrices, isFarming)
    -- Materials button icon: 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

    -- Cost text
    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:Hide()
        end
    end

    -- Collapse button
    if pinnedFrame.collapseBtn and pinnedFrame.collapseBtn.UpdateIcon then
        pinnedFrame.collapseBtn.UpdateIcon()
    end
end

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

    -- Build/refresh caches
    if lumberNeedsDirty then
        cachedQueueLumber = HDG_LumberTracker.CalculateCraftingQueueLumber(GetCraftingQueue())
        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(cachedQueueLumber) 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(cachedQueueLumber) 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 snapshot for change detection
    local currentState = {}
    local stateIdx = 0
    for lumberId, data in pairs(cachedQueueLumber) 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
    stateIdx = stateIdx + 1; currentState[stateIdx] = GetConfig("showLumberMaterials") and 1 or 0
    stateIdx = stateIdx + 1; currentState[stateIdx] = GetConfig("showRawMaterials") and 1 or 0
    stateIdx = stateIdx + 1; currentState[stateIdx] = GetConfig("lumberCollapsed") and 1 or 0
    stateIdx = stateIdx + 1; currentState[stateIdx] = activeFarmingLumber or 0
    stateIdx = stateIdx + 1; currentState[stateIdx] = justCollectedLumber or 0
    stateIdx = stateIdx + 1; currentState[stateIdx] = animationPhase
    stateIdx = stateIdx + 1; currentState[stateIdx] = GetZoneText() or "" -- zone change detection
    if activeFarmingLumber and sessionFarmingStats[activeFarmingLumber] then
        local st = sessionFarmingStats[activeFarmingLumber]
        stateIdx = stateIdx + 1; currentState[stateIdx] = math.floor(GetTime() - st.startTime)
        stateIdx = stateIdx + 1; currentState[stateIdx] = st.collected
    end

    -- Skip UI rebuild if nothing visually changed
    if pinnedDisplayState 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

    -- Render
    local isFarming = (activeFarmingLumber ~= nil)
    local collapseMode = GetConfig("lumberCollapsed") and isFarming
    local lumberPanelHeight = RefreshPinnedLumberRows(cachedQueueLumber, cachedAllLumber, collapseMode)
    local totalCost, missingPrices, showMaterials = RefreshPinnedMaterials(lumberPanelHeight, isFarming)
    RefreshPinnedFooter(showMaterials, totalCost, missingPrices, isFarming)
end

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

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