local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide
local Log = privateDB.Log
local LOG_LEVEL = privateDB.LOG_LEVEL

-- ============================================================================
-- MAIN ADDON FILE
-- Event Handling and Entry Point
-- ============================================================================

-- Track pending craft operations for multi-craft scenarios
-- When crafting multiple items, only close window after all complete
privateDB.pendingCraftInfo = {
    itemID = nil,      -- The itemID being crafted
    remaining = 0,     -- How many crafts remaining
}

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

-- Track craft quote frequency (respects vamooseChattiness slider)
-- Similar to lumber quote throttling in HDG_Vamoose.lua
local craftQuoteTracking = {
    craftsSinceQuote = 0,      -- Counter since last quote
    nextQuoteThreshold = 1,    -- Random threshold for next quote (1 to chattiness)
}

-- Check if a craft quote should be displayed based on chattiness setting
-- Returns true if quote should show, false otherwise
local function ShouldShowCraftQuote(chattiness)
    if chattiness <= 0 then return false end

    -- Increment craft count
    craftQuoteTracking.craftsSinceQuote = craftQuoteTracking.craftsSinceQuote + 1

    -- Check if we've hit the threshold
    if craftQuoteTracking.craftsSinceQuote >= craftQuoteTracking.nextQuoteThreshold then
        -- Reset for next quote with new random threshold
        craftQuoteTracking.craftsSinceQuote = 0
        craftQuoteTracking.nextQuoteThreshold = math.random(1, math.max(1, chattiness))
        return true
    end

    return false
end

-- Initialize Database
local initialized = HDG_Data.InitializeDatabase()
if not initialized then
    print("|cFFFF0000[Housing Decor Guide]|r Failed to initialize database! Addon will not work.")
    return
end

-- Bridge functions to expose UI methods on the global object if needed by legacy code or macros
function HDG:RefreshUI()
    HDG_UI.RefreshUI()
end

function HDG:RefreshCraftingQueue()
    HDG_UI.RefreshCraftingQueue()
end

function HDG:RefreshMaterials()
    HDG_UI.RefreshMaterials()
end

function HDG:RefreshLumberTracking()
    HDG_UI.RefreshLumberTracking()
end

function HDG:Toggle()
    HDG_UI.Toggle()
end

-- Forward declarations for local utility functions defined below
local HDG_ExportReagents
local HDG_ShowExportWindow

-- Slash command
SLASH_HDG1 = "/hdg"
SlashCmdList["HDG"] = function(msg)
    local cmd = msg and msg:lower():trim() or ""

    -- /hdg dump — open a copy dialog with the effective LAYOUT_CONFIG so
    -- runtime picker tweaks can be pasted back into source. Aliases: layout
    -- dump / layoutdump / decordump.
    if cmd == "dump" or cmd == "layout dump" or cmd == "layoutdump" or cmd == "decordump" then
        local HDG_HT = privateDB and privateDB.HDG_HT
        if HDG_HT and HDG_HT.LayoutDump and HDG_HT.LayoutDump.Dump then
            HDG_HT.LayoutDump.Dump()
        else
            print("|cFFFF0000[HDG]|r Layout dump not loaded.")
        end
        return
    end

    -- /hdg order — print effective widget order to chat (cheap inspection)
    if cmd == "order" then
        if _G.HDG_PrintOrder then _G.HDG_PrintOrder() end
        return
    end

    -- /hdg projects — open HDG with the Projects tab active
    if cmd == "projects" or cmd == "proj" then
        if HDG_UI and HDG_UI.Show then HDG_UI.Show() end
        if HDG_UI and HDG_UI.SwitchTab then HDG_UI.SwitchTab("projects") end
        return
    end

    -- Debug commands (only available when debug mode is enabled)
    local isDebugCmd = cmd == "collection" or cmd == "tags" or cmd == "debuglumber" or cmd == "debugprof" or cmd == "tsm"
    if isDebugCmd and not GetConfig("debug") then
        HDG_UI.Toggle()
        return
    end

    if cmd == "collection" then
        -- Debug: Test catalog searcher for owned decor
        print("|cFF14b8a6[Housing Decor Guide]|r Testing collection searcher...")

        if not C_HousingCatalog then
            print("|cFFFF0000C_HousingCatalog not available|r")
            return
        end

        if not C_HousingCatalog.CreateCatalogSearcher then
            print("|cFFFF0000CreateCatalogSearcher not available|r")
            return
        end

        local catalogSearcher = C_HousingCatalog.CreateCatalogSearcher()
        if not catalogSearcher then
            print("|cFFFF0000Failed to create catalog searcher|r")
            return
        end

        print("|cFF00FF00Searcher created successfully|r")

        -- Check what methods are available
        local methods = {"SetOwnedOnly", "SetCollected", "SetUncollected", "GetAllSearchItems", "RunSearch", "SetResultsUpdatedCallback"}
        for _, method in ipairs(methods) do
            if catalogSearcher[method] then
                print(string.format("  |cFF00FF00%s|r: available", method))
            else
                print(string.format("  |cFFFF0000%s|r: NOT available", method))
            end
        end

        -- Try to configure and get results
        if catalogSearcher.SetOwnedOnly then
            catalogSearcher:SetOwnedOnly(true)
            print("|cFF00FF00SetOwnedOnly(true) called|r")
        end

        if catalogSearcher.SetCollected then
            catalogSearcher:SetCollected(true)
            print("|cFF00FF00SetCollected(true) called|r")
        end

        -- Try GetAllSearchItems
        if catalogSearcher.GetAllSearchItems then
            local items = catalogSearcher:GetAllSearchItems()
            if items then
                print(string.format("|cFFfcd34dGetAllSearchItems returned %d items|r", #items))
                -- Print first 5 items
                for i = 1, math.min(5, #items) do
                    local item = items[i]
                    print(string.format("  Item %d: type=%s, recordID=%s", i,
                        tostring(item.entryType), tostring(item.recordID)))
                end
            else
                print("|cFFFF0000GetAllSearchItems returned nil|r")
            end
        end

        -- Try async with callback
        if catalogSearcher.SetResultsUpdatedCallback and catalogSearcher.RunSearch then
            print("|cFF00FF00Trying async search with callback...|r")
            catalogSearcher:SetResultsUpdatedCallback(function()
                local items = catalogSearcher:GetAllSearchItems()
                if items then
                    print(string.format("|cFF00FF00Async callback: %d items found|r", #items))
                    for i = 1, math.min(3, #items) do
                        local item = items[i]
                        local info = item.recordID and HDG_Data and HDG_Data.GetCatalog
                            and HDG_Data.GetCatalog(item.recordID)
                        if info then
                            print(string.format("  %d: %s", i, tostring(info.name)))
                            -- Dump all fields in info
                            for k, v in pairs(info) do
                                print(string.format("    %s = %s", tostring(k), tostring(v)))
                            end
                            -- Also dump entryID fields
                            print("    entryID fields:")
                            for k, v in pairs(item) do
                                print(string.format("      %s = %s", tostring(k), tostring(v)))
                            end
                        end
                    end
                else
                    print("|cFFFF0000Async callback: nil items|r")
                end
                if catalogSearcher.Release then
                    catalogSearcher:Release()
                end
            end)
            catalogSearcher:RunSearch()
        end

        return

    elseif cmd == "tags" then
        -- Dump all unique tags with counts
        print("|cFF14b8a6[Housing Decor Guide]|r Scanning all decor items for tags...")
        local tagCounts = {}
        local totalItems = 0
        local itemsWithTags = 0

        if HDG_DecorDB and HDG_Data and HDG_Data.GetCatalogByItem then
            for spellID, recipe in pairs(HDG_DecorDB) do
                if recipe.itemID then
                    totalItems = totalItems + 1
                    local decorInfo = HDG_Data.GetCatalogByItem(recipe.itemID)
                    if decorInfo and decorInfo.tags then
                        local hasTag = false
                        for _, tagName in pairs(decorInfo.tags) do
                            tagCounts[tagName] = (tagCounts[tagName] or 0) + 1
                            hasTag = true
                        end
                        if hasTag then
                            itemsWithTags = itemsWithTags + 1
                        end
                    end
                end
            end
        end

        -- Convert to sorted array
        local tags = {}
        for tag, count in pairs(tagCounts) do
            table.insert(tags, { name = tag, count = count })
        end
        table.sort(tags, function(a, b) return a.count > b.count end)

        -- Print results
        print(string.format("|cFFfcd34dFound %d unique tags across %d/%d items:|r", #tags, itemsWithTags, totalItems))
        for i, tagData in ipairs(tags) do
            print(string.format("  |cFF14b8a6%s|r: %d items", tagData.name, tagData.count))
        end

    elseif cmd == "featuredtest" then
        -- Probe C_HousingCatalog.GetFeaturedSmallProducts() — 12.0.5+ replacement
        -- for GetFeaturedDecor. Each entry: { entryVariantID, price, originalPrice,
        -- canPreview, ... }. Dumps full struct shape for the first 5 entries +
        -- looks up GetCatalogEntryVariantInfo(entryVariantID) for cross-reference.
        if not (C_HousingCatalog and C_HousingCatalog.GetFeaturedSmallProducts) then
            print("|cFFFF0000[HDG]|r C_HousingCatalog.GetFeaturedSmallProducts not available.")
            return
        end

        local products = C_HousingCatalog.GetFeaturedSmallProducts()
        if not products then
            print("|cFFFFD34D[HDG featuredtest]|r GetFeaturedSmallProducts returned nil. Try opening the housing catalog first, then re-run.")
            return
        end

        print("|cFF14b8a6[HDG]|r GetFeaturedSmallProducts -> " .. #products .. " entries")

        local function dumpTable(tbl, prefix)
            prefix = prefix or "    "
            for k, v in pairs(tbl) do
                local tv = type(v)
                if tv == "table" then
                    print(string.format("%s|cFFFFD34D%s|r = (table)", prefix, tostring(k)))
                    dumpTable(v, prefix .. "  ")
                else
                    print(string.format("%s|cFFFFD34D%s|r = %s (%s)",
                        prefix, tostring(k), tostring(v), tv))
                end
            end
        end

        local n = math.min(#products, 5)
        for i = 1, n do
            print(string.format("|cFF14b8a6  Entry %d:|r", i))
            dumpTable(products[i])

            -- If entryVariantID is present, also look up full variant info
            local p = products[i]
            if p.entryVariantID and C_HousingCatalog.GetCatalogEntryVariantInfo then
                local variantInfo = C_HousingCatalog.GetCatalogEntryVariantInfo(p.entryVariantID)
                if variantInfo then
                    print("    |cFF14b8a6→ GetCatalogEntryVariantInfo:|r")
                    dumpTable(variantInfo, "      ")
                end
            end
        end

        if #products > n then
            print(string.format("|cFF888888  (%d more entries not dumped)|r", #products - n))
        end

        -- Tally any currency-shaped fields across ALL products
        local cidTally = {}
        for _, p in ipairs(products) do
            local function harvest(t)
                for k, v in pairs(t) do
                    if type(v) == "table" then
                        if v.currencyID and v.amount then
                            cidTally[v.currencyID] = (cidTally[v.currencyID] or 0) + v.amount
                        else
                            for _, sub in ipairs(v) do
                                if type(sub) == "table" and sub.currencyID and sub.amount then
                                    cidTally[sub.currencyID] = (cidTally[sub.currencyID] or 0) + sub.amount
                                end
                            end
                        end
                    end
                end
            end
            harvest(p)
        end
        if next(cidTally) then
            print("|cFF14b8a6  Currency tally across all featured products:|r")
            for cid, total in pairs(cidTally) do
                local info = C_CurrencyInfo.GetCurrencyInfo(cid)
                local name = (info and info.name) or ("Currency#" .. cid)
                print(string.format("    %s (%d): %s", name, cid, tostring(total)))
            end
        else
            print("|cFF888888  No {currencyID, amount} sub-tables found in any product.|r")
        end

    elseif cmd == "costtest" then
        -- HDG_VendorDB validation against C_HousingCatalog.GetCatalogEntryInfoByItem.
        -- Cost data lives in entryInfo.sourceText as parseable text:
        --   Vendor: NPC Name
        --   Zone: Zone Name
        --   Cost: 50g            (gold)
        --   Cost: 10 Voidlight Marl  (currency, name → currencyID via map)
        -- We parse both gold and currency lines and compare to our DB to
        -- find mismatches + missing-from-DB cases.
        if not HDG_VendorDB then
            print("|cFFFF0000[HDG]|r HDG_VendorDB not loaded.")
            return
        end
        if not (C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem) then
            print("|cFFFF0000[HDG]|r C_HousingCatalog.GetCatalogEntryInfoByItem not available.")
            return
        end

        -- Vendor name filter — skip these on BOTH sides (DB and API parsing)
        -- so totals stay apples-to-apples. Rae'ana is duplicated in our DB
        -- with name-suffix tags to distinguish her npcIDs; matching them by
        -- bare name confuses the audit.
        local IGNORED_VENDORS = {
            ["Rae'ana"] = true,
        }
        local function isIgnoredVendor(name)
            if not name then return false end
            if IGNORED_VENDORS[name] then return true end
            -- Also catch tagged variants like "Rae'ana <Some Tag>"
            local bare = name:match("^([^<]+)%s*<")
            if bare then bare = bare:gsub("%s+$", "") end
            return bare and IGNORED_VENDORS[bare] or false
        end

        -- Build name → currencyID map from every currency referenced in our DB.
        -- Walk VendorDB's currCost arrays, collect distinct currencyIDs, and
        -- resolve names via C_CurrencyInfo. Used to parse "Cost: 10 Foo".
        local nameToCID = {}
        for _, vendor in pairs(HDG_VendorDB) do
            if type(vendor) == "table" and vendor.items then
                for _, item in pairs(vendor.items) do
                    local currCost = item[5]
                    if type(currCost) == "table" then
                        for _, c in ipairs(currCost) do
                            if c[1] == "c" and c[2] and not nameToCID[c[2]] then
                                local info = C_CurrencyInfo.GetCurrencyInfo(c[2])
                                if info and info.name then
                                    nameToCID[info.name] = c[2]
                                end
                            end
                        end
                    end
                end
            end
        end

        -- Parse "Cost: 50g 25s" → copper. Bare number = gold. Returns nil on
        -- non-money cost (currency text).
        local function ParseGoldCost(costStr)
            if not costStr or costStr == "" then return nil end
            local g = tonumber(costStr:match("(%d+)g")) or 0
            local s = tonumber(costStr:match("(%d+)s")) or 0
            local c = tonumber(costStr:match("(%d+)c")) or 0
            if g > 0 or s > 0 or c > 0 then return g * 10000 + s * 100 + c end
            local bare = tonumber(costStr:match("^%s*(%d+)%s*$"))
            if bare then return bare * 10000 end
            return nil
        end

        -- Parse currency costs. Returns an ARRAY of {cid, amt} pairs since
        -- a single Cost: line can carry multiple currencies (e.g. 2000 OR +
        -- 1000 GR). Three formats supported:
        --   1. Amount-before-link:  "2000 |Hcurrency:1220|h[Order Resources]|h"
        --   2. Amount-inside-link:  "|Hcurrency:1220|h[2000]|h"
        --      (Blizzard renders this as "2000" with no name visible)
        --   3. Plain text fallback: "10 Voidlight Marl" → nameToCID map
        local function ParseCurrencyCosts(costStr)
            local out2 = {}
            if not costStr or costStr == "" then return out2 end
            local seen = {}
            local function push(cid, amt)
                cid = tonumber(cid); amt = tonumber(amt)
                if not cid or not amt then return end
                local key = cid .. "x" .. amt
                if seen[key] then return end
                seen[key] = true
                out2[#out2 + 1] = { cid = cid, amt = amt }
            end
            -- Form 1: amount before link
            for amt, cid in costStr:gmatch("(%d+)%s*|Hcurrency:(%d+)") do push(cid, amt) end
            -- Form 2: amount inside link bracket
            for cid, amt in costStr:gmatch("|Hcurrency:(%d+)|h%[?(%d+)%]?|h") do push(cid, amt) end
            -- Form 3: plain-text fallback only when no link matched
            if #out2 == 0 then
                local amt, name = costStr:match("^%s*(%d+)%s+(.+)$")
                if amt and name then
                    name = name:gsub("%s+$", ""):gsub("^%s+", "")
                    local cid = nameToCID[name]
                    if cid then push(cid, amt) end
                end
            end
            return out2
        end

        -- Parse the multi-line sourceText into vendor entries.
        -- Strip color codes, textures (|T..|t), atlas markup (|A:..|a), and
        -- expand |n to real newlines. Keep |H..|h..|h hyperlinks intact so
        -- ParseCurrencyCost can extract currency:ID from currency-link text.
        local function ParseSourceText(sourceText)
            if not sourceText or sourceText == "" then return {} end
            local normalized = sourceText
                :gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
                :gsub("|T.-|t", "")
                :gsub("|A:.-|a", "")
                :gsub("|n", "\n")
            local vendors, current = {}, nil
            for line in normalized:gmatch("[^\n]+") do
                line = line:match("^%s*(.-)%s*$") or line
                local vName = line:match("^Vendor:%s*(.+)")
                local cost  = line:match("^Cost:%s*(.+)")
                if vName then
                    if current then vendors[#vendors + 1] = current end
                    current = { name = vName, costs = {} }
                elseif cost and current then
                    current.costs[#current.costs + 1] = cost
                end
            end
            if current then vendors[#vendors + 1] = current end
            return vendors
        end

        -- Collect every itemID HDG_VendorDB knows about.
        local items = {}
        for _, vendor in pairs(HDG_VendorDB) do
            if type(vendor) == "table" and vendor.items then
                for itemID in pairs(vendor.items) do
                    items[itemID] = true
                end
            end
        end
        local itemList = {}
        for id in pairs(items) do itemList[#itemList + 1] = id end
        table.sort(itemList)

        local scanned, notInCatalog, noCost = 0, 0, 0
        local goldMatch, goldMismatch, goldMissing = 0, 0, 0
        local currMatch, currMismatch, currMissing = 0, 0, 0
        local mismatchLines, missingLines = {}, {}

        -- All output accumulated into `out` and dumped to a copy-paste edit
        -- window at the end. Plain text — no color codes — for clean copy.
        local out = {}
        local function emit(s) out[#out + 1] = s or "" end

        -- One-shot raw-sourceText dump for the first item with currency cost
        -- in our DB. Shows BOTH the rendered text (EditBox interprets |c |T
        -- |H markup) AND the escaped/literal version (each | replaced with
        -- /| so EditBox renders it as plain text and we can SEE the markup
        -- structure — currency hyperlinks, atlases, etc.).
        local sampleDumped = false
        local function maybeDumpSample(itemID, entry, dbHasCurrCost)
            if sampleDumped or not dbHasCurrCost then return end
            sampleDumped = true
            local raw = entry.sourceText or ""
            emit(string.format("-- RAW sourceText for itemID %d (%q) — RENDERED --",
                itemID, entry.name or "?"))
            for line in (raw .. "\n"):gmatch("([^\n]*)\n") do
                if line ~= "" then emit("    " .. line) end
            end
            emit("-- RAW sourceText — ESCAPED (markup visible) --")
            local escaped = raw:gsub("|", "/|")
            for line in (escaped .. "\n"):gmatch("([^\n]*)\n") do
                if line ~= "" then emit("    " .. line) end
            end
            emit("-- END --")
            emit("")
        end

        -- Per-currency aggregate totals: { dbAll, dbUncoll, apiAll, apiUncoll, items }
        local totals = {}
        local function bumpTotals(cid, side, amount, isUncoll)
            local t = totals[cid]
            if not t then t = { dbAll = 0, dbUncoll = 0, apiAll = 0, apiUncoll = 0, items = 0 }; totals[cid] = t end
            if side == "db"  then t.dbAll  = t.dbAll  + amount; if isUncoll then t.dbUncoll  = t.dbUncoll  + amount end end
            if side == "api" then t.apiAll = t.apiAll + amount; if isUncoll then t.apiUncoll = t.apiUncoll + amount end end
        end

        local function isCollected(itemID)
            if HDG_Data and HDG_Data.GetCatalogByItem then
                local info = HDG_Data.GetCatalogByItem(itemID)
                if info and info.collected ~= nil then return info.collected end
            end
            return false
        end

        local function goldStr(copper) return string.format("%.0fg", (copper or 0) / 10000) end

        for _, itemID in ipairs(itemList) do
            local entry = C_HousingCatalog.GetCatalogEntryInfoByItem(itemID, false)
            if not entry then
                notInCatalog = notInCatalog + 1
            else
                scanned = scanned + 1
                local blizVendors = ParseSourceText(entry.sourceText)
                if #blizVendors == 0 then
                    noCost = noCost + 1
                else
                    -- Find the DB cost for this item — use first non-ignored
                    -- vendor that carries it (multi-vendor items: prices
                    -- match per-vendor in our DB so any will do).
                    local dbGold, dbCurr
                    for _, vendor in pairs(HDG_VendorDB) do
                        if type(vendor) == "table" and vendor.items and vendor.items[itemID]
                           and not isIgnoredVendor(vendor[1]) then
                            dbGold = vendor.items[itemID][1] or 0
                            dbCurr = vendor.items[itemID][5] or {}
                            break
                        end
                    end

                    -- Tally DB-side currency cost per item (once, regardless
                    -- of how many vendors sell it — same prices across).
                    local uncoll = not isCollected(itemID)
                    local dbHasCurrCost = false
                    if dbCurr then
                        for _, c in ipairs(dbCurr) do
                            if c[1] == "c" and c[2] and c[3] then
                                bumpTotals(c[2], "db", c[3], uncoll)
                                local t = totals[c[2]]
                                if t then t.items = t.items + 1 end
                                dbHasCurrCost = true
                            end
                        end
                    end
                    maybeDumpSample(itemID, entry, dbHasCurrCost)

                    -- Compare each Bliz Cost: line to our DB. Tally totals
                    -- ONCE per item (first non-ignored vendor) — multi-vendor
                    -- items would otherwise double-count on the API side
                    -- since the same prices appear under each vendor block.
                    local apiTallied = false
                    for _, bv in ipairs(blizVendors) do
                        if not isIgnoredVendor(bv.name) then
                        for _, costStr in ipairs(bv.costs) do
                            local blizGold = ParseGoldCost(costStr)
                            if blizGold then
                                if not dbGold or dbGold == 0 then
                                    goldMissing = goldMissing + 1
                                    missingLines[#missingLines + 1] = string.format(
                                        "  %d  %q  bliz=%s  vendor=%q",
                                        itemID, entry.name or "?", goldStr(blizGold), bv.name)
                                elseif blizGold == dbGold then
                                    goldMatch = goldMatch + 1
                                else
                                    goldMismatch = goldMismatch + 1
                                    mismatchLines[#mismatchLines + 1] = string.format(
                                        "  %d  %q  db=%s  bliz=%s  vendor=%q",
                                        itemID, entry.name or "?",
                                        goldStr(dbGold), goldStr(blizGold), bv.name)
                                end
                            else
                                local pairs2 = ParseCurrencyCosts(costStr)
                                for _, pair in ipairs(pairs2) do
                                    local cid, amt = pair.cid, pair.amt
                                    -- Tally only once per item (first vendor)
                                    if not apiTallied then
                                        bumpTotals(cid, "api", amt, uncoll)
                                    end
                                    local dbAmt
                                    for _, c in ipairs(dbCurr) do
                                        if c[1] == "c" and c[2] == cid then
                                            dbAmt = c[3]; break
                                        end
                                    end
                                    if not dbAmt then
                                        currMissing = currMissing + 1
                                        missingLines[#missingLines + 1] = string.format(
                                            "  %d  %q  bliz=%dx cid:%d  vendor=%q (no currCost in DB)",
                                            itemID, entry.name or "?", amt, cid, bv.name)
                                    elseif dbAmt == amt then
                                        currMatch = currMatch + 1
                                    else
                                        currMismatch = currMismatch + 1
                                        mismatchLines[#mismatchLines + 1] = string.format(
                                            "  %d  %q  cid:%d  db=%d  bliz=%d  vendor=%q",
                                            itemID, entry.name or "?", cid, dbAmt, amt, bv.name)
                                    end
                                end
                                -- else: unparseable (rep gates, etc.) — silent
                            end
                        end
                        apiTallied = true
                        end -- if not isIgnoredVendor(bv.name)
                    end
                end
            end
        end

        emit("HDG Vendor Cost Validation — VendorDB vs Blizzard sourceText")
        emit(string.format("Scanned: %d   Not in catalog: %d   No vendor sourceText: %d",
            scanned, notInCatalog, noCost))
        emit(string.format("Gold:     match=%d  mismatch=%d  missing-in-DB=%d",
            goldMatch, goldMismatch, goldMissing))
        emit(string.format("Currency: match=%d  mismatch=%d  missing-in-DB=%d",
            currMatch, currMismatch, currMissing))
        emit("")

        -- Per-currency totals — same numbers the Decor Currencies widget
        -- shows (uncollected) plus the catalog-wide total and the API-side
        -- equivalents for cross-validation.
        local cids = {}
        for cid in pairs(totals) do cids[#cids + 1] = cid end
        table.sort(cids, function(a, b)
            local ca = C_CurrencyInfo.GetCurrencyInfo(a)
            local cb = C_CurrencyInfo.GetCurrencyInfo(b)
            return ((ca and ca.name) or "") < ((cb and cb.name) or "")
        end)

        local function fmt(n) return BreakUpLargeNumbers and BreakUpLargeNumbers(n) or tostring(n) end
        emit("-- TOTALS PER CURRENCY (DB vs API; uncollected = widget number) --")
        emit(string.format("%-2s %-26s %-7s %12s  %12s  %12s  %12s  %12s",
            "", "Currency", "(cid)", "DB-uncoll", "API-uncoll", "DB-all", "API-all", "Balance"))
        for _, cid in ipairs(cids) do
            local info = C_CurrencyInfo.GetCurrencyInfo(cid)
            local name = (info and info.name) or ("Currency#" .. cid)
            local bal  = (info and info.quantity) or 0
            local t    = totals[cid]
            local mark = (t.dbUncoll == t.apiUncoll) and "OK" or "!!"
            emit(string.format("%-2s %-26s (%5d) %12s  %12s  %12s  %12s  %12s",
                mark, name, cid, fmt(t.dbUncoll), fmt(t.apiUncoll),
                fmt(t.dbAll), fmt(t.apiAll), fmt(bal)))
        end

        if #mismatchLines > 0 then
            emit("")
            emit(string.format("-- MISMATCHES (DB vs Blizzard) — %d --", #mismatchLines))
            for _, l in ipairs(mismatchLines) do emit(l) end
        end
        if #missingLines > 0 then
            emit("")
            emit(string.format("-- MISSING IN DB (Blizzard has cost, ours empty) — %d --", #missingLines))
            for _, l in ipairs(missingLines) do emit(l) end
        end

        local fullText = table.concat(out, "\n")
        if privateDB and privateDB.HDG_HT and privateDB.HDG_HT.LayoutDump
           and privateDB.HDG_HT.LayoutDump.ShowCopyDialog then
            privateDB.HDG_HT.LayoutDump.ShowCopyDialog(
                "HDG Vendor Cost Validation", fullText)
        else
            -- Fallback to chat if the copy dialog module isn't loaded.
            for _, line in ipairs(out) do print(line) end
            print("|cFFFF0000[HDG]|r LayoutDump.ShowCopyDialog not available; printed to chat instead.")
        end
        print(string.format("|cFF14b8a6[HDG]|r Cost validation: %d gold mismatches, %d gold missing, %d currency mismatches, %d currency missing — see copy window",
            goldMismatch, goldMissing, currMismatch, currMissing))

    elseif cmd == "decor" then
        -- Filter profession window to show only House Decor recipes using text search
        if not C_TradeSkillUI then
            print("|cFFFF0000[Housing Decor Guide]|r Trade skill API not available.")
            return
        end

        if not C_TradeSkillUI.IsTradeSkillReady() then
            print("|cFFfcd34d[Housing Decor Guide]|r Open a profession window first, then run |cFFFFFFFF/hdg decor|r")
            return
        end

        -- Use text filter to search for "House Decor"
        if C_TradeSkillUI.SetRecipeItemNameFilter then
            C_TradeSkillUI.SetRecipeItemNameFilter("House Decor")
            print("|cFF14b8a6[Housing Decor Guide]|r Filtered to 'House Decor' recipes. Clear the search box to reset.")
        else
            print("|cFFFF0000[Housing Decor Guide]|r SetRecipeItemNameFilter not available in this WoW version.")
        end

    elseif cmd == "debuglumber" then
        -- Debug: dump lumber tracking state
        print("|cFF14b8a6[HDG]|r Lumber Tracking Debug:")

        -- Check settings
        print(string.format("  lumberCollapsed: %s", tostring(GetConfig("lumberCollapsed"))))
        print(string.format("  lumberPinned: %s", tostring(GetUIState("lumberPinned"))))

        -- Check LUMBER_TYPES
        local lumberTypes = HDG_ExpansionData.LUMBER_TYPES
        local lumberOrder = HDG_ExpansionData.LUMBER_ORDER
        local typeCount = 0
        for _ in pairs(lumberTypes or {}) do typeCount = typeCount + 1 end
        print(string.format("  LUMBER_TYPES entries: %d", typeCount))
        print(string.format("  LUMBER_ORDER entries: %d", lumberOrder and #lumberOrder or 0))

        -- Check session state
        if HDG_LumberTracker then
            local activeLumber = HDG_LumberTracker.GetActiveFarmingLumber and HDG_LumberTracker.GetActiveFarmingLumber()
            print(string.format("  Active farming lumber ID: %s", tostring(activeLumber)))

            if activeLumber and lumberTypes[activeLumber] then
                print(string.format("  Active lumber name: %s", lumberTypes[activeLumber].shortName or lumberTypes[activeLumber].name))
            end

            -- Check pinned widget state
            local isPinnedVisible = HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible()
            print(string.format("  Pinned widget visible: %s", tostring(isPinnedVisible)))

            -- Show previous counts vs current counts
            local prevCounts = HDG_LumberTracker.GetPreviousCounts and HDG_LumberTracker.GetPreviousCounts()
            local sessionStats = HDG_LumberTracker.GetSessionStats and HDG_LumberTracker.GetSessionStats()

            print("  |cFFfcd34dLumber Tracking State:|r")
            for _, lumberId in ipairs(lumberOrder or {}) do
                local info = lumberTypes[lumberId]
                local currentCount = HDG_Data.GetItemCount(lumberId) or 0
                local prevCount = prevCounts and prevCounts[lumberId] or "?"
                local stats = sessionStats and sessionStats[lumberId]
                local name = info and info.shortName or tostring(lumberId)

                local statStr = ""
                if stats then
                    statStr = string.format(" |cFF88FF88[Session: +%d, %.1f/hr]|r", stats.collected or 0, stats.perHour or 0)
                end

                print(string.format("    %s: now=%d, prev=%s%s", name, currentCount, tostring(prevCount), statStr))
            end
        else
            print("  |cFFFF0000HDG_LumberTracker not found!|r")
        end

        print("  |cFF888888Pick up lumber to test session tracking|r")

    elseif cmd == "debugprof" then
        -- Debug: dump profession skill data to a scrollable window
        local debugFrame = CreateFrame("Frame", "HDG_DebugFrame", UIParent, "BackdropTemplate")
        debugFrame:SetSize(700, 500)
        debugFrame:SetPoint("CENTER")
        debugFrame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        debugFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
        debugFrame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
        debugFrame:SetMovable(true)
        debugFrame:EnableMouse(true)
        debugFrame:RegisterForDrag("LeftButton")
        debugFrame:SetScript("OnDragStart", debugFrame.StartMoving)
        debugFrame:SetScript("OnDragStop", debugFrame.StopMovingOrSizing)
        debugFrame:SetFrameStrata("DIALOG")

        -- Title
        local title = debugFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOPLEFT", 12, -10)
        title:SetText("|cFF14b8a6HDG Debug: Profession Data|r")

        -- Close button
        local closeBtn = CreateFrame("Button", nil, debugFrame, "UIPanelCloseButton")
        closeBtn:SetPoint("TOPRIGHT", -2, -2)
        closeBtn:SetScript("OnClick", function() debugFrame:Hide() end)

        -- Scrollable edit box
        local scrollFrame = CreateFrame("ScrollFrame", nil, debugFrame, "UIPanelScrollFrameTemplate")
        scrollFrame:SetPoint("TOPLEFT", 10, -40)
        scrollFrame:SetPoint("BOTTOMRIGHT", -30, 10)

        local editBox = CreateFrame("EditBox", nil, scrollFrame)
        editBox:SetMultiLine(true)
        editBox:SetFontObject(GameFontHighlightSmall)
        editBox:SetWidth(640)
        editBox:SetAutoFocus(false)
        editBox:EnableMouse(true)
        editBox:SetScript("OnEscapePressed", function() debugFrame:Hide() end)
        scrollFrame:SetScrollChild(editBox)

        -- Build debug output
        local lines = {}
        table.insert(lines, "=== HDG Debug: Profession Data ===\n")

        -- Part 1: What does the WoW API return right now?
        table.insert(lines, "--- API Data (C_TradeSkillUI.GetChildProfessionInfos) ---")
        if C_TradeSkillUI and C_TradeSkillUI.GetChildProfessionInfos then
            local childInfos = C_TradeSkillUI.GetChildProfessionInfos()
            if childInfos and #childInfos > 0 then
                table.insert(lines, "Returned " .. #childInfos .. " entries:\n")
                for i, info in ipairs(childInfos) do
                    local expName = info.expansionName or "nil"
                    local profName = info.professionName or "nil"
                    local skill = info.skillLevel or 0
                    local maxSkill = info.maxSkillLevel or 0
                    table.insert(lines, string.format("  [%d] expansionName='%s'  skill=%d/%d", i, expName, skill, maxSkill))
                    table.insert(lines, string.format("       professionName='%s'", profName))

                    -- Show what normalization produces
                    if info.expansionName and HDG_ExpansionData then
                        local normalized = HDG_ExpansionData.NormalizeApiName(info.expansionName)
                        table.insert(lines, string.format("       -> Normalized to: '%s'", normalized))
                    end
                    table.insert(lines, "")
                end
            else
                table.insert(lines, "  ** No data returned - is a profession window open? **\n")
            end
        else
            table.insert(lines, "  ** GetChildProfessionInfos not available **\n")
        end

        -- Part 2: What's in SavedVariables?
        table.insert(lines, "\n--- SavedVariables (Store.characters) ---")
        local charKey = HDG_Data.GetCharacterKey()
        table.insert(lines, "Current character: " .. (charKey or "nil") .. "\n")

        local characters = GetCharacters()
        if characters and next(characters) then
            for cKey, charData in pairs(characters) do
                table.insert(lines, "Character: " .. cKey)
                if charData.professions then
                    for profName, profData in pairs(charData.professions) do
                        table.insert(lines, string.format("  Profession: %s (known=%d/%d)",
                            profName, profData.knownCount or 0, profData.totalCount or 0))
                        if profData.skillLevels then
                            local count = 0
                            for expName, skillData in pairs(profData.skillLevels) do
                                count = count + 1
                                table.insert(lines, string.format("    [%s] = %d/%d",
                                    expName, skillData.current or 0, skillData.max or 0))
                            end
                            if count == 0 then
                                table.insert(lines, "    (no skill levels stored)")
                            end
                        else
                            table.insert(lines, "    skillLevels = nil")
                        end
                    end
                else
                    table.insert(lines, "  (no professions)")
                end
                table.insert(lines, "")
            end
        else
            table.insert(lines, "  ** No character data in SavedVariables **\n")
        end

        -- Part 3: What does EXPANSION_ORDER contain?
        table.insert(lines, "\n--- Expansion Order (used by Alts tab) ---")
        local expansionOrder = HDG_Data.EXPANSION_ORDER or {}
        for i, expName in ipairs(expansionOrder) do
            table.insert(lines, string.format("  [%d] '%s'", i, expName))
        end

        -- Set text
        editBox:SetText(table.concat(lines, "\n"))
        print("|cFF14b8a6[HDG]|r Debug window opened. Press ESC or X to close.")

    elseif cmd == "exportreagents" then
        -- Export reagent data from current profession window to copyable popup
        HDG_ExportReagents()

    elseif cmd == "tsm" then
        -- Debug: Test TSM integration
        print("|cFF14b8a6[Housing Decor Guide]|r Testing TSM integration...")

        -- Check if TSM_API exists
        if not TSM_API then
            print("|cFFFF0000TSM_API global not found|r - TSM may not be installed or loaded")
            print("  Checking for Auctionator instead...")
            if Auctionator and Auctionator.API then
                print("|cFF00FF00Auctionator detected|r")
            else
                print("|cFFFF0000No price addon detected|r")
            end
            return
        end

        print("|cFF00FF00TSM_API found!|r")

        -- Check available methods
        local methods = {"GetCustomPriceValue", "ToItemString", "GetPriceSourceDescription"}
        for _, method in ipairs(methods) do
            if TSM_API[method] then
                print(string.format("  |cFF00FF00%s|r: available", method))
            else
                print(string.format("  |cFFFF0000%s|r: NOT available", method))
            end
        end

        -- Test price lookup with a decor item (Valdrakken Hanging Cauldron = 256430)
        local testItemID = 256430
        local testItemName = C_Item.GetItemNameByID(testItemID) or "Valdrakken Hanging Cauldron"
        print(string.format("\nTesting price lookup for %s (ID: %d):", testItemName, testItemID))

        -- Test different price sources (include sale rate variants)
        local priceSources = {"DBMarket", "DBMinBuyout", "DBHistorical", "DBRegionMarketAvg",
                              "DBRegionSaleRate", "dbregionsalerate", "DBRegionSoldPerDay", "dbregionsoldperday",
                              "DBRegionSaleAvg", "dbregionsaleavg"}
        local saleRateSources = {DBRegionSaleRate=true, dbregionsalerate=true, DBRegionSoldPerDay=true, dbregionsoldperday=true}
        for _, source in ipairs(priceSources) do
            local itemString = "i:" .. testItemID
            local price = TSM_API.GetCustomPriceValue(source, itemString)
            if price and price > 0 then
                if saleRateSources[source] then
                    -- Sale rate is a decimal (sales per day), not copper
                    print(string.format("  |cFF00FF00%s|r: %.4f/day", source, price))
                else
                    local gold = math.floor(price / 10000)
                    local silver = math.floor((price % 10000) / 100)
                    print(string.format("  |cFF00FF00%s|r: %d.%02dg", source, gold, silver))
                end
            else
                print(string.format("  |cFFFF0000%s|r: no data (raw: %s)", source, tostring(price)))
            end
        end

        -- Test the addon's GetItemPrice function
        print("\nTesting HDG_PriceIntegration.GetItemPrice:")
        local price, source = HDG_PriceIntegration.GetItemPrice(testItemID)
        if price then
            print(string.format("  Price: %s (source: %s)", HDG_PriceIntegration.FormatGold(price), source or "unknown"))
        else
            print("  |cFFFF0000No price returned|r")
        end

        -- Test TSM GetAuctionQuantity (for #AH column)
        print("\nTesting TSM_API.GetAuctionQuantity:")
        if TSM_API.GetAuctionQuantity then
            local itemString = "i:" .. testItemID
            local ahQty = TSM_API.GetAuctionQuantity(itemString)
            if ahQty then
                print(string.format("  AH Quantity: %d", ahQty))
            else
                print("  |cFFFF0000No AH quantity returned|r")
            end
        else
            print("  |cFFFF0000GetAuctionQuantity not available|r")
        end

        -- List all available price sources
        print("\nAvailable TSM Price Sources:")
        if TSM_API.GetPriceSourceKeys then
            local sources = {}
            TSM_API.GetPriceSourceKeys(sources)
            for _, key in ipairs(sources) do
                print(string.format("  %s", key))
            end
            print(string.format("  Total: %d sources", #sources))
        else
            print("  |cFFFF0000GetPriceSourceKeys not available|r")
        end

        -- Test ALL price sources for the decor item to find which ones have data
        print("\nTesting ALL price sources for decor item:")
        if TSM_API.GetPriceSourceKeys then
            local sources = {}
            TSM_API.GetPriceSourceKeys(sources)
            local itemString = "i:" .. testItemID
            for _, key in ipairs(sources) do
                local value = TSM_API.GetCustomPriceValue(key, itemString)
                if value and value > 0 then
                    print(string.format("  |cFF00FF00%s|r: %s", key, tostring(value)))
                end
            end
        end

    elseif cmd == "testquote" or cmd == "testquote shaman" or cmd == "testquote goblin" then
        -- Test quote display
        local HDG_CraftQuotes = privateDB.HDG_CraftQuotes
        local HDG_Vamoose = privateDB.HDG_Vamoose

        if not HDG_CraftQuotes or not HDG_Vamoose then
            print("|cFFFF0000[HDG]|r Quote system not loaded")
            return
        end

        -- Check for lumber speaker test
        local arg = msg:match("testquote%s+(%w+)")
        if arg == "shaman" or arg == "goblin" then
            local speakerType = arg:upper()
            local speaker = HDG_CraftQuotes.GetSpeaker(speakerType)
            if speaker then
                local testQuote = "This is a test quote for the " .. arg .. " speaker. DisplayID: " .. speaker.displayID
                print(string.format("|cFF14b8a6[HDG]|r Testing %s (displayID: %d)", speaker.name, speaker.displayID))
                HDG_Vamoose.PlayTalkingHead(testQuote, speakerType, speaker.displayID)
            else
                print(string.format("|cFFFF0000[HDG]|r No speaker found for: %s", arg))
            end
            return
        end

        -- List of professions and expansions for random selection
        local professions = {"Alchemy", "Blacksmithing", "Cooking", "Enchanting", "Engineering", "Inscription", "Jewelcrafting", "Leatherworking", "Tailoring"}
        local expansions = {"Classic", "TBC", "WotLK", "Cata", "MoP", "WoD", "Legion", "BfA", "SL", "DF", "TWW", "Midnight"}

        -- Pick random profession and expansion
        local profession = professions[math.random(1, #professions)]
        local expansion = expansions[math.random(1, #expansions)]

        -- Get a quote
        local quote = HDG_CraftQuotes.GetRandomQuote(profession, expansion)
        if quote then
            local speaker = HDG_CraftQuotes.GetSpeaker(profession)
            if speaker then
                print(string.format("|cFF14b8a6[HDG]|r Testing quote: %s (%s) - displayID: %d", profession, expansion, speaker.displayID))
                HDG_Vamoose.PlayTalkingHead(quote, profession, speaker.displayID)
            else
                print(string.format("|cFFFF0000[HDG]|r No speaker found for profession: %s", profession))
            end
        else
            print(string.format("|cFFFF0000[HDG]|r No quote found for %s / %s", profession, expansion))
        end

    elseif cmd == "dyes" or cmd == "dye" then
        if VamoosesDyeStudio and VamoosesDyeStudio.Toggle then
            VamoosesDyeStudio.Toggle()
        else
            print("|cFF00CED1Vamoose's Dye Studio|r is a standalone addon. Get it on CurseForge or Wago!")
        end

    elseif cmd == "hud" then
        -- Toggle lumber compass HUD
        if privateDB.HDG_LumberHUD then
            privateDB.HDG_LumberHUD.Toggle()
        else
            print("|cFFFF0000[Housing Decor Guide]|r Lumber HUD not available")
        end

    elseif cmd == "reset" then
        -- Emergency window reset: clear saved position and force to center
        local store = GetStore()
        if store then
            local state = store:GetState()
            if state and state.layout then state.layout.MainFrame = nil end
            if state and state.ui then state.ui.framePosition = nil end
        end
        local f = _G["HDG_MainFrame"]
        if f then
            f:ClearAllPoints()
            f:SetPoint("CENTER")
            f:Show()
            HDG_UI.RefreshUI()
            print("|cFF14b8a6[Housing Decor Guide]|r Window reset to center.")
        else
            -- Frame doesn't exist yet, create it fresh
            HDG_UI.CreateUI()
            f = _G["HDG_MainFrame"]
            if f then
                f:ClearAllPoints()
                f:SetPoint("CENTER")
                f:Show()
                HDG_UI.RefreshUI()
                print("|cFF14b8a6[Housing Decor Guide]|r Window created and reset to center.")
            else
                print("|cFFFF0000[Housing Decor Guide]|r Window reset failed. Try /reload then /hdg reset")
            end
        end

    else
        HDG_UI.Toggle()
    end
end

-- ============================================================================
-- EXPORT REAGENTS UTILITY
-- Extracts reagent item IDs from open profession window for database validation
-- ============================================================================

-- Export reagent data from current profession window (only House Decor recipes)
HDG_ExportReagents = function()
    if not C_TradeSkillUI.IsTradeSkillReady() then
        print("|cFFFF0000[HDG]|r Open a profession window first!")
        return
    end

    local recipeIDs = C_TradeSkillUI.GetAllRecipeIDs()
    if not recipeIDs or #recipeIDs == 0 then
        print("|cFFFF0000[HDG]|r No recipes found in profession window")
        return
    end

    local lines = {"RecipeSpellID,RecipeName,ReagentItemID,ReagentName,Qty"}
    local decorCount = 0

    for _, recipeID in ipairs(recipeIDs) do
        local recipeInfo = C_TradeSkillUI.GetRecipeInfo(recipeID)
        local isHouseDecor = false

        -- Check if recipe belongs to "House Decor" category
        if recipeInfo and recipeInfo.categoryID then
            local categoryInfo = C_TradeSkillUI.GetCategoryInfo(recipeInfo.categoryID)
            if categoryInfo and categoryInfo.name == "House Decor" then
                isHouseDecor = true
            end
        end

        if isHouseDecor then
            decorCount = decorCount + 1
            local schematic = C_TradeSkillUI.GetRecipeSchematic(recipeID, false)
            if schematic and schematic.reagentSlotSchematics then
                for _, slot in ipairs(schematic.reagentSlotSchematics) do
                    if slot.reagents then
                        for _, reagent in ipairs(slot.reagents) do
                            if reagent.itemID then
                                local itemName = C_Item.GetItemNameByID(reagent.itemID) or "Unknown"
                                table.insert(lines, string.format("%d,%s,%d,%s,%d",
                                    recipeID,
                                    schematic.name or "Unknown",
                                    reagent.itemID,
                                    itemName,
                                    slot.quantityRequired or 1
                                ))
                            end
                        end
                    end
                end
            end
        end
    end

    if decorCount == 0 then
        print("|cFFfcd34d[HDG]|r No House Decor recipes found in this profession window")
        return
    end

    -- Show popup window
    HDG_ShowExportWindow(table.concat(lines, "\n"), decorCount)
end

-- Create/show export popup window
HDG_ShowExportWindow = function(text, recipeCount)
    if not HDG_ReagentExportFrame then
        local f = CreateFrame("Frame", "HDG_ReagentExportFrame", UIParent, "BackdropTemplate")
        f:SetSize(600, 450)
        f:SetPoint("CENTER")
        f:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
        })
        f:SetBackdropColor(0.1, 0.1, 0.12, 0.95)
        f:SetBackdropBorderColor(0.08, 0.72, 0.65, 1)
        f:SetMovable(true)
        f:EnableMouse(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)
        f:SetFrameStrata("DIALOG")

        local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOP", 0, -12)
        title:SetText("|cFF14b8a6Export Reagent Data|r")

        local instr = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        instr:SetPoint("TOP", title, "BOTTOM", 0, -5)
        instr:SetText("|cFF888888Select all (Ctrl+A) then copy (Ctrl+C)|r")

        local sf = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", 15, -55)
        sf:SetPoint("BOTTOMRIGHT", -35, 45)

        local eb = CreateFrame("EditBox", nil, sf)
        eb:SetMultiLine(true)
        eb:SetFontObject("GameFontHighlightSmall")
        eb:SetWidth(540)
        eb:SetAutoFocus(false)
        eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        sf:SetScrollChild(eb)
        f.editBox = eb

        local closeBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        closeBtn:SetPoint("BOTTOM", 0, 10)
        closeBtn:SetSize(80, 22)
        closeBtn:SetText("Close")
        closeBtn:SetScript("OnClick", function() f:Hide() end)

        HDG_ReagentExportFrame = f
    end

    HDG_ReagentExportFrame.editBox:SetText(text)
    HDG_ReagentExportFrame.editBox:HighlightText()
    HDG_ReagentExportFrame.editBox:SetFocus()
    HDG_ReagentExportFrame:Show()
    print("|cFF14b8a6[Housing Decor Guide]|r Exported reagents from " .. recipeCount .. " recipes.")
end

-- Throttle inventory updates
local inventoryUpdatePending = false
local pendingProfDataTimer = nil
local function QueueInventoryUpdate()
    if inventoryUpdatePending then return end
    inventoryUpdatePending = true

    C_Timer.After(0.3, function()
        inventoryUpdatePending = false

        -- v2.1.4: Save current character's lumber inventory for account-wide tracking
        if HDG_Data and HDG_Data.SaveCharacterLumberInventory then
            HDG_Data.SaveCharacterLumberInventory()
        end

        -- v2.0.5: Check for lumber collection changes (updates farming stats)
        local lumberChanged = HDG_LumberTracker and HDG_LumberTracker.CheckForLumberChanges and HDG_LumberTracker.CheckForLumberChanges()

        local mainWindowVisible = privateDB.mainFrame and privateDB.mainFrame:IsShown()
        local pinnedLumberVisible = HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible()

        -- Refresh main window if visible
        if mainWindowVisible then
            HDG_UI.RefreshCraftingQueue()
            HDG_UI.RefreshMaterials()
            HDG_UI.RefreshLumberTracking()
            Log(LOG_LEVEL.DEBUG, "Inventory updated - materials refreshed")
        -- Refresh pinned lumber widget even when main window is closed
        elseif pinnedLumberVisible then
            HDG_LumberTracker.RefreshPinned()
            Log(LOG_LEVEL.DEBUG, "Inventory updated - pinned lumber refreshed" .. (lumberChanged and " (lumber collected)" or ""))
        end

        -- Refresh standalone materials list if shown
        if HDG_MaterialsList and HDG_MaterialsList.IsShown and HDG_MaterialsList.IsShown() then
            HDG_MaterialsList.Refresh()
        end
    end)
end

-- Throttle recipe updates
local recipeUpdatePending = false
local function QueueRecipeUpdate()
    if recipeUpdatePending then return end
    recipeUpdatePending = true
    
    C_Timer.After(0.5, function()
        recipeUpdatePending = false
        if privateDB.mainFrame and privateDB.mainFrame:IsShown() then
            HDG_UI.RefreshUI()
            Log(LOG_LEVEL.DEBUG, "Recipe learned - UI refreshed")
        end
    end)
end

-- Set to true in PLAYER_ENTERING_WORLD (on initial login / reload) and cleared
-- on first BAG_UPDATE_DELAYED. Used to defer restoring pinned UI windows until
-- bag contents are guaranteed loaded, replacing timer-based guesses.
local loginRestorePending = false
local loginRestoreFallbackTimer = nil

local function RunPinnedRestore()
    if not loginRestorePending then return end
    loginRestorePending = false
    if loginRestoreFallbackTimer then loginRestoreFallbackTimer:Cancel(); loginRestoreFallbackTimer = nil end
    if GetUIState("lumberPinned") and HDG_LumberTracker and HDG_LumberTracker.RestorePinnedState then
        HDG_LumberTracker.RestorePinnedState()
    end
    if GetUIState("vendorListPinned") and HDG_VendorShoppingList and HDG_VendorShoppingList.Show then
        HDG_VendorShoppingList.Show()
    end
    if GetUIState("materialsListPinned") and HDG_MaterialsList and HDG_MaterialsList.Show then
        HDG_MaterialsList.Show()
    end
end

-- Event Handling
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("HOUSE_DECOR_ADDED_TO_CHEST")
eventFrame:RegisterEvent("HOUSING_STORAGE_ENTRY_UPDATED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("TRADE_SKILL_LIST_UPDATE")
eventFrame:RegisterEvent("TRADE_SKILL_SHOW")  -- v2.0.7: Auto-refresh alts tab when prof window opened
eventFrame:RegisterEvent("NEW_RECIPE_LEARNED")
eventFrame:RegisterEvent("BAG_UPDATE_DELAYED")
eventFrame:RegisterEvent("MAIL_INBOX_UPDATE")
eventFrame:RegisterEvent("MAIL_SHOW")  -- v2.0.6: Track mail open for lumber farming
eventFrame:RegisterEvent("MAIL_CLOSED")
eventFrame:RegisterEvent("MERCHANT_SHOW")  -- v2.1.4: Track merchant open for lumber farming
eventFrame:RegisterEvent("MERCHANT_CLOSED")
eventFrame:RegisterEvent("TRADE_SHOW")  -- v2.1.5: Track trade open for lumber farming
eventFrame:RegisterEvent("TRADE_CLOSED")
eventFrame:RegisterEvent("BANKFRAME_OPENED")  -- v2.0.6: Track bank open for lumber farming
eventFrame:RegisterEvent("BANKFRAME_CLOSED")
eventFrame:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_SHOW")  -- v2.2.x: Portable warband bank
eventFrame:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_HIDE")
eventFrame:RegisterEvent("PLAYER_MONEY")
eventFrame:RegisterEvent("TRADE_SKILL_ITEM_CRAFTED_RESULT")  -- v1.9.5: Crafting history tracking
eventFrame:RegisterEvent("PLAYER_LOGOUT")  -- v2.0.5: Save farming sessions on logout
eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")  -- v2.2.2: Re-init map context after zone transition
eventFrame:RegisterEvent("ACHIEVEMENT_EARNED")  -- Lumber mounted-harvesting achievement progress

eventFrame:SetScript("OnEvent", function(self, event, arg1, ...)
    if event == "ADDON_LOADED" and arg1 == addonName then
        -- Validate settings (legacy - still needed for first-time defaults)
        local success, err = pcall(privateDB.ValidateSettings)
        if not success then
            Log(LOG_LEVEL.ERROR, "Settings validation failed: %s", err)
        end

        -- Phase 2: Initialize Store and migrate settings
        if privateDB.InitSettingsProxy then
            local proxySuccess, proxyErr = pcall(privateDB.InitSettingsProxy)
            if not proxySuccess then
                Log(LOG_LEVEL.ERROR, "Store initialization failed: %s", proxyErr)
            end
        end

        -- Phase 3: Initialize Theme Engine
        local HDG = _G.HousingDecorGuide
        local themeSuccess, themeErr = pcall(function() HDG.Theme:Initialize() end)
        if not themeSuccess then
            Log(LOG_LEVEL.ERROR, "Theme Engine initialization failed: %s", themeErr)
        end

        -- Apply locale override if set (after SavedVariables loaded)
        if HDG_L and HDG_L.ApplyOverride then
            HDG_L:ApplyOverride()
        end

        -- Initialize minimap button after settings are loaded
        if privateDB.HDG_Minimap then
            privateDB.HDG_Minimap.Initialize()
        end

        -- Register Projects topology-capture event hooks
        if privateDB.HDG_TopologyCapture and privateDB.HDG_TopologyCapture.RegisterEvents then
            privateDB.HDG_TopologyCapture.RegisterEvents()
        end

        -- Initialize Recent Activity tracker
        if HDG.RecentActivity and HDG.RecentActivity.Initialize then
            HDG.RecentActivity:Initialize()
        end

        -- Shopping list: auto-decrement on vendor purchase
        hooksecurefunc("BuyMerchantItem", function(merchantIndex, qty)
            if not GetConfig("shopListAutoDecrement") then return end
            local link = GetMerchantItemLink(merchantIndex)
            local itemID = link and tonumber(link:match("item:(%d+)"))
            if not itemID then return end
            local HDG = _G.HousingDecorGuide
            local list = HDG and HDG.Store and HDG.Store:GetActiveShoppingList()
            if not list or not list.items then return end
            local amount, removed = qty or 1, false
            for _ = 1, amount do
                local found = false
                for _, entry in ipairs(list.items) do
                    if entry.itemID == itemID then
                        HDG.Store:RemoveFromVendorShoppingList(itemID, entry.npcID)
                        found = true; removed = true
                        break
                    end
                end
                if not found then break end
            end
            if removed and HDG_VendorShoppingList and HDG_VendorShoppingList.IsShown
               and HDG_VendorShoppingList.IsShown() then
                HDG_VendorShoppingList.Refresh()
            end
        end)

        -- Load persisted direct AH price cache
        HDG_PriceIntegration.LoadCache()

        -- Initialize waypoint map pins (deferred until WorldMapFrame loads)
        C_Timer.After(2, function() HDG.Waypoints:InitMapPins() end)

        -- v2.0: Collect character profession data for multi-character tracking
        C_Timer.After(1, function()
            HDG_Data.CollectCharacterProfessions()
            Log(LOG_LEVEL.DEBUG, "Character profession data collected")
        end)

        -- Tooltip: tag housing decor reagents
        TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item, function(tooltip, data)
            if tooltip ~= GameTooltip then return end
            local store = HDG and HDG.Store
            local state = store and store:GetState()
            if not state or not state.config.tooltipDecorTag then return end
            local itemID = data and data.id
            if not itemID then return end
            if not HDG_PowerCrafting or not HDG_PowerCrafting.GetRecipesForReagent then return end
            local recipes = HDG_PowerCrafting.GetRecipesForReagent(itemID)
            if not recipes or #recipes == 0 then return end
            tooltip:AddLine(string.format("|cFF14b8a6Housing Decor reagent - %d recipe%s|r", #recipes, #recipes == 1 and "" or "s"))
        end)

        Log(LOG_LEVEL.INFO, "Addon loaded successfully")
        if HDG.Store and HDG.Store:GetState().config.debug then
            print(string.format("|cFF00FF00Vamoose's Housing Decor Helper v%s|r loaded! Type |cFFFFFF00/hdg|r to open.", privateDB.ADDON_VERSION))
        end

    elseif event == "PLAYER_ENTERING_WORLD" then
        local isInitialLogin, isReloadingUi = arg1, ...
        -- v2.0.5: Initialize lumber session tracking (only on login/reload, NOT zone transitions)
        -- PLAYER_ENTERING_WORLD fires on zone changes too — re-init would wipe active sessions
        if (isInitialLogin or isReloadingUi) and HDG_LumberTracker and HDG_LumberTracker.InitializeSessionTracking then
            HDG_LumberTracker.InitializeSessionTracking()
        elseif HDG_LumberTracker and HDG_LumberTracker.SetUISuspended then
            -- Safety reset: clear uiSuspended on zone transitions
            -- Prevents stuck state if merchant/bank was open when player zoned
            HDG_LumberTracker.SetUISuspended(false)
        end

        -- Collection search runs lazily on main-window open via HDG_UI.RefreshAll
        -- (mirroring the Refresh button). Eager warm at PEW+1s was removed because
        -- the housing catalog is not yet synced that early on cold login -- it
        -- returned 0 results and locked the cache into a falsely-warmed state.

        -- v2.1.4: Save current character's lumber inventory on login
        -- Delay longer to ensure warband bank data is fully loaded
        if HDG_Data and HDG_Data.SaveCharacterLumberInventory then
            C_Timer.After(3.0, function()
                HDG_Data.SaveCharacterLumberInventory()
                -- v2.1.5: Refresh pinned lumber tracker after saving inventory
                -- Invalidate first to force re-render with fresh counts
                if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
                    HDG_LumberTracker.InvalidateDisplayState()
                    HDG_LumberTracker.RefreshPinned()
                end
            end)
        end

        -- Auto-restore pinned floating windows on login/reload (not zone transitions).
        -- Deferred until BAG_UPDATE_DELAYED so counts render correctly; 5s fallback
        -- timer runs the restore anyway if BAG_UPDATE_DELAYED never fires.
        if isInitialLogin or isReloadingUi then
            loginRestorePending = true
            if loginRestoreFallbackTimer then loginRestoreFallbackTimer:Cancel() end
            loginRestoreFallbackTimer = C_Timer.NewTimer(5, function()
                if loginRestorePending then
                    Log(LOG_LEVEL.DEBUG, "Login restore fallback fired (BAG_UPDATE_DELAYED did not fire)")
                    RunPinnedRestore()
                end
            end)
        end

        -- Zone scanner on login/reload (ZONE_CHANGED_NEW_AREA doesn't fire on /reload).
        -- HDG.Store has already loaded SavedVariables by PEW, so persisted
        -- ownership is in memory at state.collection.ownedDecorIDs.
        -- HDG_Data.IsItemCollected / IsDecorCollected fall back to that Store
        -- read when the module-local cache hasn't been warmed yet, and the
        -- IsCollectionCacheWarmed gate accepts Store data — so the scanner
        -- can fire here without triggering WarmCollectionCache. Cold-cold
        -- sessions (first install, no SavedVariables) silently no-op; the
        -- user populates Store the first time they open the HDG window.
        if (isInitialLogin or isReloadingUi) and HDG_ZoneMode and HDG_ZoneMode.OnZoneChanged then
            local mapID = C_Map.GetBestMapForUnit("player")
            if mapID then HDG_ZoneMode.OnZoneChanged(mapID) end
        end

    elseif event == "ZONE_CHANGED_NEW_AREA" then
        -- v2.2.2: Re-initialize map context after zone transition
        -- The map API can return nil briefly during zone transitions, which breaks lumber tracking
        -- Debounce rapid zone transitions
        if privateDB._zoneChangeTimer then privateDB._zoneChangeTimer:Cancel() end
        privateDB._zoneChangeTimer = C_Timer.NewTimer(0.5, function()
            privateDB._zoneChangeTimer = nil
            local mapID = C_Map.GetBestMapForUnit("player")
            if mapID then
                -- Refresh pinned lumber tracker to use new zone's map context
                if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
                    HDG_LumberTracker.InvalidateDisplayState()
                    HDG_LumberTracker.RefreshPinned()
                end
                -- Notify lumber HUD of zone change so it can refresh its blip positions
                if privateDB.HDG_LumberHUD and privateDB.HDG_LumberHUD.OnZoneChanged then
                    privateDB.HDG_LumberHUD.OnZoneChanged(mapID)
                end
                -- Notify zone mode of zone change for zone scanner
                if HDG_ZoneMode and HDG_ZoneMode.OnZoneChanged then
                    HDG_ZoneMode.OnZoneChanged(mapID)
                end
                Log(LOG_LEVEL.DEBUG, "Zone changed - lumber tracker refreshed (mapID: %d)", mapID)
            end
        end)

    elseif event == "HOUSE_DECOR_ADDED_TO_CHEST" then
        local decorID = ...
        Log(LOG_LEVEL.DEBUG, "Decor added to collection (decorID: " .. tostring(decorID) .. ")")

        -- Log acquisition to history. Everything else (collection state, lumber
        -- cache, shopping list, main UI) refreshes via HDG_COLLECTION_READY when
        -- HOUSING_STORAGE_ENTRY_UPDATED lands ~300ms later.
        local HDG_DataTab = privateDB.HDG_DataTab
        if decorID and HDG_DataTab and HDG_DataTab.LogAcquisition then
            HDG_DataTab.LogAcquisition(decorID)
        end

    elseif event == "HOUSING_STORAGE_ENTRY_UPDATED" then
        -- Blizzard's catalog cache is now authoritative for this entry; safe to
        -- repaint. Only refresh if the main frame is visible.
        if privateDB.mainFrame and privateDB.mainFrame:IsShown() then
            HDG_UI.RefreshUI()
        end

    elseif event == "TRADE_SKILL_LIST_UPDATE" or event == "NEW_RECIPE_LEARNED" then
        HDG_Data.InvalidateRecipeCache()
        HDG_Data.InvalidateProfessionCache()
        QueueRecipeUpdate()
        if HDG_LumberTracker and HDG_LumberTracker.InvalidateLumberNeeds then HDG_LumberTracker.InvalidateLumberNeeds() end
        -- v2.0: Update character profession data when recipes change (debounced)
        if pendingProfDataTimer then pendingProfDataTimer:Cancel() end
        pendingProfDataTimer = C_Timer.NewTimer(0.5, function()
            pendingProfDataTimer = nil
            HDG_Data.CollectCharacterProfessions()
        end)

    elseif event == "TRADE_SKILL_SHOW" then
        -- Refresh profession data once Blizzard's tradeskill data is populated.
        -- TRADE_SKILL_SHOW fires before the list itself is ready; the actual
        -- "data usable" signal is TRADE_SKILL_LIST_UPDATE (or IsTradeSkillReady
        -- returning true if Blizzard cached it). Was a blind 0.3s wait.
        local function collectProfData()
            HDG_Data.CollectCharacterProfessions()
            HDG.EventBus:Trigger("HDG_PROFESSION_DATA_UPDATED")
        end
        if C_TradeSkillUI and C_TradeSkillUI.IsTradeSkillReady and C_TradeSkillUI.IsTradeSkillReady() then
            collectProfData()
        else
            local f = CreateFrame("Frame")
            local done = false
            local function finish()
                if done then return end
                done = true
                f:UnregisterAllEvents()
                f:SetScript("OnEvent", nil)
                collectProfData()
            end
            f:RegisterEvent("TRADE_SKILL_LIST_UPDATE")
            f:SetScript("OnEvent", finish)
            -- Safety fallback: if the event somehow never fires, collect anyway
            -- after 2s so we don't silently miss a profession-data refresh.
            C_Timer.After(2, finish)
        end

    elseif event == "MAIL_SHOW" or event == "BANKFRAME_OPENED" or
           event == "MERCHANT_SHOW" or event == "TRADE_SHOW" then
        -- Suspend lumber tracking when item-moving UIs are open
        if HDG_LumberTracker and HDG_LumberTracker.SetUISuspended then
            HDG_LumberTracker.SetUISuspended(true)
        end
        if event == "BANKFRAME_OPENED" then QueueInventoryUpdate() end

    elseif event == "MAIL_CLOSED" or event == "BANKFRAME_CLOSED" or
           event == "MERCHANT_CLOSED" or event == "TRADE_CLOSED" then
        -- Resume lumber tracking when item-moving UIs close
        if HDG_LumberTracker and HDG_LumberTracker.SetUISuspended then
            HDG_LumberTracker.SetUISuspended(false)
        end
        QueueInventoryUpdate()

    elseif event == "PLAYER_INTERACTION_MANAGER_FRAME_SHOW" then
        -- Portable warband bank (type 68 = AccountBanker) or standard banker (type 8)
        local interactionType = arg1
        if interactionType == Enum.PlayerInteractionType.AccountBanker or
           interactionType == Enum.PlayerInteractionType.Banker then
            if HDG_LumberTracker and HDG_LumberTracker.SetUISuspended then
                HDG_LumberTracker.SetUISuspended(true)
            end
        end

    elseif event == "PLAYER_INTERACTION_MANAGER_FRAME_HIDE" then
        local interactionType = arg1
        if interactionType == Enum.PlayerInteractionType.AccountBanker or
           interactionType == Enum.PlayerInteractionType.Banker then
            if HDG_LumberTracker and HDG_LumberTracker.SetUISuspended then
                HDG_LumberTracker.SetUISuspended(false)
            end
            QueueInventoryUpdate()
        end

    elseif event == "BAG_UPDATE_DELAYED" or
           event == "MAIL_INBOX_UPDATE" or
           event == "PLAYER_MONEY" then
        if event == "BAG_UPDATE_DELAYED" and loginRestorePending then
            -- First bag-update burst after login/reload has settled; pinned
            -- floating windows can now restore with accurate counts.
            RunPinnedRestore()
        end
        QueueInventoryUpdate()

    elseif event == "TRADE_SKILL_ITEM_CRAFTED_RESULT" then
        -- TRADE_SKILL_ITEM_CRAFTED_RESULT: result table with hyperlink, quantity, isCrit
        local result = arg1
        if result and result.hyperlink then
            HDG_Data.LogCraftFromItemLink(result.hyperlink, result.quantity or 1)

            -- Trigger craft quote for decor items
            local itemID = tonumber(result.hyperlink:match("item:(%d+)"))
            -- Only show craft quotes if master toggle enabled AND chattiness check passes
            local quotesEnabled = GetConfig("quotesEnabled")
            if quotesEnabled == nil then quotesEnabled = true end
            local chattiness = GetConfig("vamooseChattiness") or 30
            local craftQuotesEnabled = GetConfig("craftQuotesEnabled")
            if craftQuotesEnabled == nil then craftQuotesEnabled = true end
            local decorInfo = itemID and privateDB.decorItem and privateDB.decorItem[itemID]

            if quotesEnabled and decorInfo and craftQuotesEnabled and ShouldShowCraftQuote(chattiness) then
                if decorInfo.profession and decorInfo.expansion then
                    local expansionAbbr = HDG_ExpansionData.GetAbbreviation(decorInfo.expansion)
                    local HDG_CraftQuotes = privateDB.HDG_CraftQuotes
                    local HDG_Vamoose = privateDB.HDG_Vamoose
                    if HDG_CraftQuotes and HDG_Vamoose then
                        local quote = HDG_CraftQuotes.GetRandomQuote(decorInfo.profession, expansionAbbr)
                        if quote then
                            local speaker = HDG_CraftQuotes.GetSpeaker(decorInfo.profession)
                            if speaker then
                                local onlyChat = GetConfig("quotesOnlyChat") or false
                                if not onlyChat then
                                    HDG_Vamoose.PlayTalkingHead(quote, decorInfo.profession, speaker.displayID)
                                end
                                if onlyChat then
                                    print(string.format("|cFF14b8a6[%s]|r %s crafted! %s", speaker.name, result.hyperlink, quote))
                                end
                            end
                        end
                    end
                end
            end

            -- v2.0.8: Auto-decrement crafting queue when decor item crafted
            local queue = GetCraftingQueue()
            if decorInfo and queue then
                -- Crafting queue stores item.id as the itemID (not DecorDB key)
                do
                    local craftedQty = result.quantity or 1
                    for _, item in ipairs(queue) do
                        if item.id == itemID then
                            local newQty = item.quantity - craftedQty
                            if newQty <= 0 then
                                Dispatch("REMOVE_FROM_CRAFTING_QUEUE", { itemID = itemID })
                                print(string.format("|cFF14b8a6[HDG]|r %s removed from crafting queue (crafting complete)", item.name or "Item"))
                            else
                                Dispatch("UPDATE_CRAFTING_QUEUE_ITEM_QTY", { itemID = itemID, quantity = newQty })
                                print(string.format("|cFF14b8a6[HDG]|r %s: %d remaining in crafting queue", item.name or "Item", newQty))
                            end

                            -- Track pending crafts for multi-craft scenarios
                            -- Decrement pending count if this matches our tracked craft
                            if privateDB.pendingCraftInfo and privateDB.pendingCraftInfo.itemID == itemID then
                                privateDB.pendingCraftInfo.remaining = privateDB.pendingCraftInfo.remaining - craftedQty
                            end

                            -- Only close profession window if craft was initiated from addon
                            -- (pendingCraftInfo.itemID is set when crafting from addon UI)
                            local wasAddonInitiated = privateDB.pendingCraftInfo and privateDB.pendingCraftInfo.itemID == itemID

                            if wasAddonInitiated then
                                -- Check if all pending crafts are complete
                                local allCraftsComplete = (privateDB.pendingCraftInfo.remaining <= 0)

                                if allCraftsComplete then
                                    -- Clear tracking state
                                    privateDB.pendingCraftInfo.itemID = nil
                                    privateDB.pendingCraftInfo.remaining = 0

                                    -- Close profession window after crafting (if setting enabled)
                                    local closeAfterCraft = GetConfig("closeProfWindowAfterCraft")
                                    if closeAfterCraft == nil then closeAfterCraft = true end
                                    if closeAfterCraft then
                                        C_Timer.After(0.5, function()
                                            if InCombatLockdown() then return end
                                            if ProfessionsFrame and ProfessionsFrame:IsShown() then
                                                HideUIPanel(ProfessionsFrame)
                                            elseif C_TradeSkillUI and C_TradeSkillUI.CloseTradeSkill then
                                                C_TradeSkillUI.CloseTradeSkill()
                                            end
                                        end)
                                    end
                                end
                            end

                            break
                        end
                    end
                end
                -- Refresh lumber display after queue change (Dispatch only invalidates cache)
                HDG_UI.RefreshLumberTracking()
            end
        end

    elseif event == "ACHIEVEMENT_EARNED" then
        if HDG_LumberTracker and HDG_LumberTracker.IsLumberAchievement
           and HDG_LumberTracker.IsLumberAchievement(arg1) then
            HDG_LumberTracker.InvalidateDisplayState()
            if HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
                HDG_LumberTracker.RefreshPinned()
            end
        end
        -- Refresh achievements display on Data tab for any housing achievement
        if HDG_ExpansionData.IsHousingAchievement(arg1) then
            local HDG_DataTab = privateDB.HDG_DataTab
            if HDG_DataTab and HDG_DataTab.RefreshAchievementsDisplay then
                HDG_DataTab.RefreshAchievementsDisplay()
            end
        end

    elseif event == "PLAYER_LOGOUT" then
        -- v2.0.5: Save farming sessions to history on logout
        if HDG_LumberTracker and HDG_LumberTracker.SaveAllSessions then
            HDG_LumberTracker.SaveAllSessions()
        end

        -- Phase 2: Flush Store to SavedVariables on logout
        HDG.Store:Flush()
    end
end)

-- ============================================================================
-- ADDON COMPARTMENT FUNCTIONS (Referenced by TOC metadata)
-- These global functions are called by Blizzard's addon compartment system
-- ============================================================================

function HousingDecorGuide_OnAddonCompartmentClick(addonName, buttonName)
    HDG_UI.Toggle()
end

function HousingDecorGuide_OnAddonCompartmentEnter(addonName, menuItem)
    GameTooltip:SetOwner(menuItem, "ANCHOR_RIGHT")
    GameTooltip:AddLine("|cFFfcd34dHousing Decor Guide|r", 1, 1, 1)
    GameTooltip:AddLine("Track and craft housing decor items", 0.7, 0.7, 0.7)
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine("|cFFFFFFFFClick:|r Toggle addon window", 0.7, 0.7, 0.7)
    GameTooltip:Show()
end

function HousingDecorGuide_OnAddonCompartmentLeave(addonName, menuItem)
    GameTooltip:Hide()
end

-- Debug accessor: HousingDecorGuide.PrivateDB.HDG_ProjectsStore
HousingDecorGuide.PrivateDB = privateDB
