local addonName, privateDB = ...
local L = HDG_L or {}

-- ============================================================================
-- DEBUG MODULE
-- Centralized debug utilities, slash commands, and beta testing tools
-- ============================================================================

local HDG_Debug = {}
privateDB.HDG_Debug = HDG_Debug

-- Theme helper (delegating to ThemeEngine)
local HDG = _G.HousingDecorGuide
local function GetScheme() return HDG.Theme.GetCurrentScheme() end

-- Debug state
local debugEnabled = false
local debugUnlocked = nil  -- Session override for debugModeEnabled setting (nil = use setting)
local eventLogging = false
local eventLog = {}
local MAX_EVENT_LOG = 100

-- Debug output window
local debugWindow = nil
local debugScrollFrame = nil
local debugEditBox = nil
local debugOutputLines = {}

-- ============================================================================
-- DEBUG OUTPUT WINDOW
-- ============================================================================

local function CreateDebugWindow()
    if debugWindow then return debugWindow end

    -- Create main frame
    debugWindow = CreateFrame("Frame", "HDGDebugWindow", UIParent, "BackdropTemplate")
    debugWindow:SetSize(700, 500)
    debugWindow:SetPoint("CENTER")
    debugWindow:SetMovable(true)
    debugWindow:SetResizable(true)
    debugWindow:SetClampedToScreen(true)
    debugWindow:EnableMouse(true)
    debugWindow:SetFrameStrata("DIALOG")

    -- Backdrop
    debugWindow:SetBackdrop({
        bgFile = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    local scheme = GetScheme()
    if scheme then
        debugWindow:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.95)
        debugWindow:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    else
        debugWindow:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
        debugWindow:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
    end

    -- Make draggable
    debugWindow:SetScript("OnMouseDown", function(self, button)
        if button == "LeftButton" then
            self:StartMoving()
        end
    end)
    debugWindow:SetScript("OnMouseUp", function(self)
        self:StopMovingOrSizing()
    end)

    -- Title bar
    local title = debugWindow:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", 0, -10)
    title:SetText("|cFF00FFFF[HDG Debug Output]|r")

    -- Close button
    local closeBtn = CreateFrame("Button", nil, debugWindow, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -5, -5)

    -- Clear button
    local clearBtn = CreateFrame("Button", nil, debugWindow, "UIPanelButtonTemplate")
    clearBtn:SetSize(60, 22)
    clearBtn:SetPoint("TOPRIGHT", closeBtn, "TOPLEFT", -10, -3)
    clearBtn:SetText("Clear")
    clearBtn:SetScript("OnClick", function()
        HDG_Debug.ClearWindow()
    end)

    -- Copy button
    local copyBtn = CreateFrame("Button", nil, debugWindow, "UIPanelButtonTemplate")
    copyBtn:SetSize(60, 22)
    copyBtn:SetPoint("RIGHT", clearBtn, "LEFT", -5, 0)
    copyBtn:SetText("Copy")
    copyBtn:SetScript("OnClick", function()
        if debugEditBox then
            debugEditBox:HighlightText()
            debugEditBox:SetFocus()
        end
    end)

    -- Scroll frame
    debugScrollFrame = CreateFrame("ScrollFrame", "HDGDebugScrollFrame", debugWindow, "UIPanelScrollFrameTemplate")
    debugScrollFrame:SetPoint("TOPLEFT", 10, -40)
    debugScrollFrame:SetPoint("BOTTOMRIGHT", -30, 10)

    -- Edit box for text (allows copy/paste)
    debugEditBox = CreateFrame("EditBox", "HDGDebugEditBox", debugScrollFrame)
    debugEditBox:SetMultiLine(true)
    debugEditBox:SetFontObject(GameFontHighlightSmall)
    debugEditBox:SetWidth(debugScrollFrame:GetWidth())
    debugEditBox:SetAutoFocus(false)
    debugEditBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)

    debugScrollFrame:SetScrollChild(debugEditBox)

    -- Resize grip
    local resizeGrip = CreateFrame("Button", nil, debugWindow)
    resizeGrip:SetSize(16, 16)
    resizeGrip:SetPoint("BOTTOMRIGHT", -6, 6)
    resizeGrip:SetNormalTexture("Interface/ChatFrame/UI-ChatIM-SizeGrabber-Up")
    resizeGrip:SetHighlightTexture("Interface/ChatFrame/UI-ChatIM-SizeGrabber-Highlight")
    resizeGrip:SetPushedTexture("Interface/ChatFrame/UI-ChatIM-SizeGrabber-Down")
    resizeGrip:SetScript("OnMouseDown", function()
        debugWindow:StartSizing("BOTTOMRIGHT")
    end)
    resizeGrip:SetScript("OnMouseUp", function()
        debugWindow:StopMovingOrSizing()
        debugEditBox:SetWidth(debugScrollFrame:GetWidth())
    end)

    debugWindow:SetResizeBounds(400, 300, 1200, 800)

    -- Hide by default
    debugWindow:Hide()

    return debugWindow
end

-- Add a line to the debug window
function HDG_Debug.WindowPrint(...)
    local args = { ... }
    local line = ""
    for i, v in ipairs(args) do
        if i > 1 then line = line .. " " end
        line = line .. tostring(v)
    end
    -- Strip color codes for editbox (they don't render there)
    local plainLine = line:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
    table.insert(debugOutputLines, plainLine)

    -- Update editbox
    if debugEditBox then
        debugEditBox:SetText(table.concat(debugOutputLines, "\n"))
    end
end

-- Clear the debug window
function HDG_Debug.ClearWindow()
    wipe(debugOutputLines)
    if debugEditBox then
        debugEditBox:SetText("")
    end
end

-- Show the debug window
function HDG_Debug.ShowWindow(title)
    CreateDebugWindow()
    if title then
        local titleText = debugWindow:GetRegions()
        if titleText and titleText.SetText then
            titleText:SetText("|cFF00FFFF[HDG Debug: " .. title .. "]|r")
        end
    end
    debugWindow:Show()
end

-- Hide the debug window
function HDG_Debug.HideWindow()
    if debugWindow then
        debugWindow:Hide()
    end
end

-- Dump every House Decor recipe's reagent list from the open profession
-- window into the copy-paste debug window. CSV-shaped output for easy
-- pipe-into-spreadsheet validation against HDG_ReagentsDB / HDG_DecorDB.
-- Used by /hdgdebug exportreagents (ProfTools pipeline helper).
function HDG_Debug.ExportReagents()
    if not (C_TradeSkillUI and C_TradeSkillUI.IsTradeSkillReady
        and 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

    HDG_Debug.ClearWindow()
    HDG_Debug.ShowWindow("Reagent Export (CSV)")
    HDG_Debug.WindowPrint("RecipeSpellID,RecipeName,ReagentItemID,ReagentName,Qty")

    local decorCount = 0
    for _, recipeID in ipairs(recipeIDs) do
        local recipeInfo = C_TradeSkillUI.GetRecipeInfo(recipeID)
        local isHouseDecor = false
        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"
                                HDG_Debug.WindowPrint(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
        HDG_Debug.HideWindow()
        print("|cFFfcd34d[HDG]|r No House Decor recipes found in this profession window")
        return
    end
    print(string.format("|cFF14b8a6[HDG]|r Exported reagents from %d recipes -- copy from window.",
        decorCount))
end

-- ============================================================================
-- CORE DEBUG UTILITIES
-- ============================================================================

-- Debug function to dump table contents
function HDG_Debug.DumpTable(tbl, indent, maxDepth)
    indent = indent or ""
    maxDepth = maxDepth or 3
    if #indent / 2 >= maxDepth then
        print(indent .. "...")
        return
    end

    if type(tbl) ~= "table" then
        print(indent .. tostring(tbl))
        return
    end

    for k, v in pairs(tbl) do
        local keyStr = tostring(k)
        if type(v) == "table" then
            print(indent .. "|cFFfcd34d" .. keyStr .. "|r = {")
            HDG_Debug.DumpTable(v, indent .. "  ", maxDepth)
            print(indent .. "}")
        else
            print(indent .. "|cFFfcd34d" .. keyStr .. "|r = |cFF00FF00" .. tostring(v) .. "|r")
        end
    end
end

-- Print a debug message (only if debug mode enabled)
function HDG_Debug.Print(...)
    if debugEnabled then
        print("|cFF00FFFF[HDG Debug]|r", ...)
    end
end

-- Print a debug message (always, for important debug output)
function HDG_Debug.PrintAlways(...)
    print("|cFF00FFFF[HDG Debug]|r", ...)
end

-- Check if debug mode is enabled
function HDG_Debug.IsEnabled()
    return debugEnabled
end

-- Separate opt-in toggle for collection-load timer traces (cold-login measurement
-- + CatalogSearcher callback→sweep timings). Default OFF even when general debug
-- is ON. Toggle via /hdgdebug timer.
local timerDebug = false
function HDG_Debug.IsTimerDebug()
    return timerDebug
end

-- Run fn, report elapsed ms when debug mode is on. Returns fn's result. No-op
-- overhead when debug is off. Intended for wrapping slow paths (tab switches,
-- data refreshes, list builds) during performance investigation.
-- Uses GetTimePreciseSec (per-call) so nested Timed() calls don't clobber each
-- other like debugprofilestart() would (that's a single global timer).
--   HDG_Debug.Timed("label", function() HDG_UI.RefreshUI() end)
function HDG_Debug.Timed(label, fn)
    if not debugEnabled then return fn() end
    local start = GetTimePreciseSec()
    local result = fn()
    print(string.format("|cFFfcd34d[HDG perf]|r %s: %.2fms", tostring(label),
        (GetTimePreciseSec() - start) * 1000))
    return result
end

-- Restore persisted debug on/off state on login. The unlock state already
-- persists via config.debugModeEnabled; this adds the current on/off toggle
-- so investigations survive /reload without re-running /hdgdebug on each time.
local debugRestoreFrame = CreateFrame("Frame")
debugRestoreFrame:RegisterEvent("PLAYER_LOGIN")
debugRestoreFrame:SetScript("OnEvent", function(self)
    self:UnregisterAllEvents()
    local db = _G.HDG_DB
    if db and db.config and db.config.debugOnAtLoad then
        debugEnabled = true
        print("|cFF00FFFF[HDG Debug]|r Debug mode |cFF00FF00ENABLED|r (restored from last session)")
    end
    if db and db.config and db.config.timerDebugEnabled then
        timerDebug = true
        print("|cFF00FFFF[HDG Debug]|r Collection timer traces |cFF00FF00ON|r (restored)")
    end
end)

-- One-shot diagnostic: hook a frame's :Show() call and time each descendant's
-- OnShow handler so we can see which child owns the cost. Installs wrappers
-- that print timing on the next :Show() then uninstall themselves.
--   HDG_Debug.ProfileShow(someFrame)
--   someFrame:Show()  -- logs each child, sorted by elapsed ms
function HDG_Debug.ProfileShow(frame, maxDepth)
    if not frame then return end
    maxDepth = maxDepth or 3
    local results = {}
    local wrapped = {}

    local function wrapDescendant(f, depth, path)
        if not f or depth > maxDepth then return end
        if wrapped[f] then return end
        wrapped[f] = true
        local existing = f:GetScript("OnShow")
        if existing then
            f:SetScript("OnShow", function(self, ...)
                debugprofilestart()
                local ok, err = pcall(existing, self, ...)
                local ms = debugprofilestop()
                results[#results + 1] = { path = path, ms = ms, ok = ok, err = err }
            end)
            -- Restore after the parent:Show cascade completes
            wrapped[f] = existing
        end
        if f.GetChildren then
            local children = { f:GetChildren() }
            for i, child in ipairs(children) do
                wrapDescendant(child, depth + 1, path .. " > child[" .. i .. "]" ..
                    (child.GetName and child:GetName() and "(" .. child:GetName() .. ")" or ""))
            end
        end
    end

    wrapDescendant(frame, 0, "root")

    -- Schedule the report + cleanup for next frame
    C_Timer.After(0, function()
        table.sort(results, function(a, b) return a.ms > b.ms end)
        print(string.format("|cFFfcd34d[HDG perf]|r ProfileShow found %d OnShow handlers, top contributors:", #results))
        for i = 1, math.min(10, #results) do
            local r = results[i]
            print(string.format("  %.2fms  %s", r.ms, r.path))
            if not r.ok then print("    (errored: " .. tostring(r.err) .. ")") end
        end
        -- Restore original scripts
        for f, original in pairs(wrapped) do
            if type(original) == "function" then f:SetScript("OnShow", original) end
        end
    end)
end

-- Set debug unlocked state (called from config UI)
function HDG_Debug.SetUnlocked(enabled)
    debugUnlocked = enabled
end

-- Check if debug commands are unlocked
function HDG_Debug.IsUnlocked()
    if debugUnlocked ~= nil then
        return debugUnlocked
    end
    local db = _G.HDG_DB
    return db and db.config and db.config.debugModeEnabled
end

-- ============================================================================
-- EVENT LOGGING
-- ============================================================================

function HDG_Debug.LogEvent(event, ...)
    if not eventLogging then return end

    local args = { ... }
    local argStr = ""
    for i, arg in ipairs(args) do
        argStr = argStr .. tostring(arg)
        if i < #args then argStr = argStr .. ", " end
    end

    local entry = {
        time = date("%H:%M:%S"),
        event = event,
        args = argStr
    }

    table.insert(eventLog, 1, entry)

    -- Trim log
    while #eventLog > MAX_EVENT_LOG do
        table.remove(eventLog)
    end

    -- Print to chat if verbose
    if debugEnabled then
        print(string.format("|cFF888888[%s]|r |cFFfcd34d%s|r %s", entry.time, event, argStr))
    end
end

function HDG_Debug.GetEventLog()
    return eventLog
end

function HDG_Debug.ClearEventLog()
    wipe(eventLog)
    HDG_Debug.PrintAlways("Event log cleared")
end

-- ============================================================================
-- SLASH COMMANDS
-- ============================================================================

-- Main debug command
SLASH_HDGDEBUG1 = "/hdgdebug"
SlashCmdList["HDGDEBUG"] = function(msg)
    -- Check if debug mode is enabled in settings (unless enabling it)
    local args = {}
    for word in msg:gmatch("%S+") do
        table.insert(args, word:lower())
    end
    local cmd = args[1] or "help"

    -- Allow "unlock" command to bypass the setting check
    if cmd ~= "unlock" then
        -- Check session override first, then fall back to saved setting
        local isUnlocked = debugUnlocked
        if isUnlocked == nil then
            local db = _G.HDG_DB
            isUnlocked = db and db.config and db.config.debugModeEnabled
        end
        if not isUnlocked then
            print("|cFF00FFFF[HDG]|r Debug mode is disabled. Enable it in HDG Config > Advanced, or type |cFFfcd34d/hdgdebug unlock|r")
            return
        end
    end

    if cmd == "unlock" then
        -- Toggle debug mode (session override + persist to settings)
        debugUnlocked = not debugUnlocked
        -- Also persist to Store config
        HDG.Store:SetConfig("debugModeEnabled", debugUnlocked)
        if debugUnlocked then
            print("|cFF00FFFF[HDG Debug]|r Debug mode |cFF00FF00UNLOCKED|r - /hdgdebug commands now available")
        else
            print("|cFF00FFFF[HDG Debug]|r Debug mode |cFFFF0000LOCKED|r")
        end
        return
    elseif cmd == "on" or cmd == "enable" then
        debugEnabled = true
        if HDG and HDG.Store and HDG.Store.SetConfig then
            HDG.Store:SetConfig("debugOnAtLoad", true)
        end
        HDG_Debug.PrintAlways("Debug mode |cFF00FF00ENABLED|r (persisted across reloads)")
    elseif cmd == "off" or cmd == "disable" then
        debugEnabled = false
        if HDG and HDG.Store and HDG.Store.SetConfig then
            HDG.Store:SetConfig("debugOnAtLoad", false)
        end
        HDG_Debug.PrintAlways("Debug mode |cFFFF0000DISABLED|r")
    elseif cmd == "events" then
        local subcmd = args[2] or "status"
        if subcmd == "on" then
            eventLogging = true
            HDG_Debug.PrintAlways("Event logging |cFF00FF00ENABLED|r")
        elseif subcmd == "off" then
            eventLogging = false
            HDG_Debug.PrintAlways("Event logging |cFFFF0000DISABLED|r")
        elseif subcmd == "show" then
            HDG_Debug.PrintAlways("Recent events (" .. #eventLog .. "):")
            for i = 1, math.min(10, #eventLog) do
                local e = eventLog[i]
                print(string.format("  |cFF888888[%s]|r |cFFfcd34d%s|r %s", e.time, e.event, e.args))
            end
        elseif subcmd == "clear" then
            HDG_Debug.ClearEventLog()
        else
            HDG_Debug.PrintAlways("Event logging: " .. (eventLogging and "|cFF00FF00ON|r" or "|cFFFF0000OFF|r"))
        end
    elseif cmd == "item" then
        local itemID = tonumber(args[2])
        if itemID then
            HDG_Debug.DumpItemInfo(itemID)
        else
            HDG_Debug.PrintAlways("Usage: /hdgdebug item <itemID>")
        end
    elseif cmd == "decor" then
        local decorID = tonumber(args[2])
        if decorID then
            HDG_Debug.DumpDecorInfo(decorID)
        else
            HDG_Debug.PrintAlways("Usage: /hdgdebug decor <decorID>")
        end
    elseif cmd == "cache" then
        HDG_Debug.DumpCacheStats()
    elseif cmd == "catalog" then
        HDG_Debug.TestCatalogSearch()
    elseif cmd == "search" then
        local searchTerm = table.concat(args, " ", 2)
        if searchTerm and searchTerm ~= "" then
            HDG_Debug.SearchCatalogByName(searchTerm)
        else
            HDG_Debug.PrintAlways("Usage: /hdgdebug search <name> (e.g., /hdgdebug search lorewalker)")
        end
    elseif cmd == "checkids" then
        local ids = {unpack(args, 2)}
        HDG_Debug.CheckDecorIDs(unpack(ids))
    elseif cmd == "lore" then
        -- Quick check for Lorewalker's Bookcase decorIDs
        print("|cFF00FFFF[HDG Debug]|r Checking Lorewalker decorIDs...")
        for _, id in ipairs({3839, 11873}) do
            if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
                local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, id, true)
                if ok and info and info.name then
                    print(string.format("  decorID %d = %s", id, info.name))
                else
                    print(string.format("  decorID %d = NOT FOUND", id))
                end
            else
                print("  C_HousingCatalog API not available")
                break
            end
        end
    elseif cmd == "scan" then
        HDG_Debug.ScanMissing()
    elseif cmd == "incomplete" then
        HDG_Debug.ScanIncomplete()
    elseif cmd == "tags" then
        HDG_Debug.DumpBlizzardTags()
    elseif cmd == "models" then
        HDG_Debug.DumpModelAssets()
    elseif cmd == "export" then
        HDG_Debug.ExportCatalog()
    elseif cmd == "api" then
        HDG_Debug.DumpHousingAPI()
    elseif cmd == "settings" then
        HDG_Debug.DumpSettings()
    elseif cmd == "status" then
        HDG_Debug.PrintStatus()
    elseif cmd == "memory" or cmd == "mem" then
        HDG_Debug.ToggleMemoryMonitor()
    elseif cmd == "timer" then
        local sub = args[2]
        if sub == "on" then
            timerDebug = true
        elseif sub == "off" then
            timerDebug = false
        else
            timerDebug = not timerDebug
        end
        if HDG and HDG.Store and HDG.Store.SetConfig then
            HDG.Store:SetConfig("timerDebugEnabled", timerDebug)
        end
        HDG_Debug.PrintAlways("Collection load timer traces: " ..
            (timerDebug and "|cFF00FF00ON|r (persisted)" or "|cFFFF0000OFF|r (persisted)"))
    -- Commands relocated from /hdg as of 2026-05-09. Bodies extracted here.
    -- /hdg keeps a soft-deprecation hint that delegates back to this slash
    -- so users see the new home but old muscle memory still works.

    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 == "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 == "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 == "collection" then
        -- Test catalog searcher: probe what methods exist, run a sync + async
        -- search, dump the first few results' fields. Useful when Blizzard
        -- changes searcher API shape between patches.
        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")
        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
        if catalogSearcher.SetOwnedOnly then catalogSearcher:SetOwnedOnly(true) end
        if catalogSearcher.SetCollected then catalogSearcher:SetCollected(true) end
        if catalogSearcher.GetAllSearchItems then
            local items = catalogSearcher:GetAllSearchItems()
            if items then
                print(string.format("|cFFfcd34dGetAllSearchItems returned %d items|r", #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
        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)))
                            for k, v in pairs(info) do
                                print(string.format("    %s = %s", tostring(k), tostring(v)))
                            end
                            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

    elseif cmd == "featuredtest" then
        -- Probe C_HousingCatalog.GetFeaturedSmallProducts() -- 12.0.5+ replacement
        -- for GetFeaturedDecor. Dumps 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])
            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
        local cidTally = {}
        for _, p in ipairs(products) do
            local function harvest(t)
                for _, 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 == "debuglumber" then
        -- Dump lumber tracking state: settings, LUMBER_TYPES count, session
        -- stats per lumber type. Useful when farming counts look off.
        local function GetCfg(k)
            local s = HDG and HDG.Store and HDG.Store:GetState()
            return s and s.config and s.config[k]
        end
        local function GetUI(k)
            local s = HDG and HDG.Store and HDG.Store:GetState()
            return s and s.ui and s.ui[k]
        end
        print("|cFF14b8a6[HDG]|r Lumber Tracking Debug:")
        print(string.format("  lumberCollapsed: %s", tostring(GetCfg("lumberCollapsed"))))
        print(string.format("  lumberPinned: %s", tostring(GetUI("lumberPinned"))))
        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))
        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
            local isPinnedVisible = HDG_LumberTracker.IsPinnedVisible
                and HDG_LumberTracker.IsPinnedVisible()
            print(string.format("  Pinned widget visible: %s", tostring(isPinnedVisible)))
            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 == "order" then
        if _G.HDG_PrintOrder then _G.HDG_PrintOrder() end

    elseif cmd == "exportreagents" then
        HDG_Debug.ExportReagents()

    elseif cmd == "scantags" then
        -- Renamed from /hdg tags to avoid collision with /hdgdebug tags (which
        -- dumps Blizzard's tag taxonomy). This counts unique tag occurrences
        -- across every known decor item -- different operation.
        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 _, 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
        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(string.format("|cFFfcd34dFound %d unique tags across %d/%d items:|r",
            #tags, itemsWithTags, totalItems))
        for _, tagData in ipairs(tags) do
            print(string.format("  |cFF14b8a6%s|r: %d items", tagData.name, tagData.count))
        end

    elseif cmd == "testquote" then
        -- Test the talking-head craft-quote system. Optional 2nd arg
        -- ("shaman"|"goblin") forces a specific lumber-flavour speaker;
        -- no arg picks a random profession+expansion combo.
        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
        local arg = args[2]
        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
        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"}
        local profession = professions[math.random(1, #professions)]
        local expansion = expansions[math.random(1, #expansions)]
        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

    -- /lumberhud test|show|testdist relocated here -- consolidates dev slashes.
    elseif cmd == "hudtest" then
        SlashCmdList["LUMBERHUD"]("test")
    elseif cmd == "hudshow" then
        SlashCmdList["LUMBERHUD"]("show")
    elseif cmd == "huddist" then
        SlashCmdList["LUMBERHUD"]("testdist")

    else
        -- Help
        HDG_Debug.PrintAlways("Housing Decor Guide - Debug Commands")
        print("  |cFFfcd34d/hdgdebug on|off|r - Enable/disable debug mode")
        print("  |cFFfcd34d/hdgdebug events on|off|show|clear|r - Event logging")
        print("  |cFFfcd34d/hdgdebug item <itemID>|r - Dump item/decor info")
        print("  |cFFfcd34d/hdgdebug decor <decorID>|r - Dump decor by recordID")
        print("  |cFFfcd34d/hdgdebug search <name>|r - Search catalog for decor by name")
        print("  |cFFfcd34d/hdgdebug cache|r - Show cache statistics")
        print("  |cFFfcd34d/hdgdebug catalog|r - Test catalog search API")
        print("  |cFFfcd34d/hdgdebug scan|r - Find items missing from AllDecorDB")
        print("  |cFFfcd34d/hdgdebug incomplete|r - Find AllDecorDB entries with unknown source data")
        print("  |cFFfcd34d/hdgdebug tags|r - Dump Blizzard tags for all items")
        print("  |cFFfcd34d/hdgdebug scantags|r - Count unique tag occurrences across decor")
        print("  |cFFfcd34d/hdgdebug models|r - Export item IDs + model file IDs for batch rendering")
        print("  |cFFfcd34d/hdgdebug export|r - Export full catalog to SavedVariables")
        print("  |cFFfcd34d/hdgdebug api|r - List C_HousingCatalog functions")
        print("  |cFFfcd34d/hdgdebug settings|r - Dump current settings")
        print("  |cFFfcd34d/hdgdebug status|r - Show addon status")
        print("  |cFFfcd34d/hdgdebug memory|r - Toggle memory chart window")
        print("  |cFFfcd34d/hdgdebug timer [on|off]|r - Collection-load timer traces (default off)")
        print("  |cFF999999--- relocated from /hdg ---|r")
        print("  |cFFfcd34d/hdgdebug featuredtest|r - probe C_HousingCatalog.GetFeaturedSmallProducts")
        print("  |cFFfcd34d/hdgdebug costtest|r - validate HDG_VendorDB against catalog API")
        print("  |cFFfcd34d/hdgdebug collection|r - test catalog searcher")
        print("  |cFFfcd34d/hdgdebug testquote [shaman|goblin]|r - test talking-head craft quote")
        print("  |cFFfcd34d/hdgdebug exportreagents|r - dump open prof window's reagent list")
        print("  |cFFfcd34d/hdgdebug tsm|r - test TSM_API integration")
        print("  |cFFfcd34d/hdgdebug debuglumber|r - dump lumber tracker state")
        print("  |cFFfcd34d/hdgdebug debugprof|r - dump profession data")
        print("  |cFFfcd34d/hdgdebug order|r - print HouseTab widget order")
        print("  |cFFfcd34d/hdgdebug hudtest|hudshow|huddist|r - lumber HUD probes (was /lumberhud)")
    end
end

-- ============================================================================
-- DEBUG DUMP FUNCTIONS
-- ============================================================================

function HDG_Debug.DumpItemInfo(itemID)
    HDG_Debug.PrintAlways("Dumping info for itemID: " .. itemID)

    -- Basic item info
    local itemName, itemLink = C_Item.GetItemInfo(itemID)
    if itemName then
        print("  |cFFfcd34dItem:|r " .. (itemLink or itemName))
    else
        print("  |cFFFF6600Item info not cached yet|r")
    end

    -- Housing catalog info
    if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
        local decorInfo = C_HousingCatalog.GetCatalogEntryInfoByItem(itemID, true)
        if decorInfo then
            print("  |cFF00FF00Decor Info:|r")
            HDG_Debug.DumpTable(decorInfo, "    ")

            -- Category info
            if decorInfo.categoryIDs and decorInfo.categoryIDs[1] then
                local catID = decorInfo.categoryIDs[1]
                if C_HousingCatalog.GetCatalogCategoryInfo then
                    local catInfo = C_HousingCatalog.GetCatalogCategoryInfo(catID)
                    if catInfo then
                        print("  |cFF00FF00Category:|r " .. (catInfo.name or "unknown"))
                    end
                end
            end
        else
            print("  |cFFFF6600Not a housing decor item|r")
        end
    else
        print("  |cFFFF0000C_HousingCatalog not available|r")
    end

    -- Check our DB
    if HDG_DecorDB then
        for _, recipe in pairs(HDG_DecorDB) do
            if recipe.itemID == itemID then
                print("  |cFF00FF00Found in HDG_DecorDB:|r")
                print("    |cFFfcd34dname:|r " .. (recipe.name or "nil"))
                print("    |cFFfcd34dprofession:|r " .. (recipe.profession or "nil"))
                print("    |cFFfcd34dexpansion:|r " .. (recipe.expansion or "nil"))
                break
            end
        end
    end
end

function HDG_Debug.DumpDecorInfo(decorID)
    HDG_Debug.PrintAlways("Dumping info for decorID (recordID): " .. decorID)

    if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        local decorInfo = C_HousingCatalog.GetCatalogEntryInfoByRecordID(1, decorID, true)
        if decorInfo then
            print("  |cFF00FF00Decor Info:|r")
            HDG_Debug.DumpTable(decorInfo, "    ")
        else
            print("  |cFFFF0000No decor found with recordID " .. decorID .. "|r")
        end
    else
        print("  |cFFFF0000C_HousingCatalog.GetCatalogEntryInfoByRecordID not available|r")
    end
end

-- Search housing catalog by name pattern and print matching decorIDs
-- Uses chunked processing to avoid freezing the client
function HDG_Debug.SearchCatalogByName(searchTerm)
    searchTerm = searchTerm:lower()
    HDG_Debug.PrintAlways("Searching catalog for: |cFFfcd34d" .. searchTerm .. "|r (this may take a moment...)")

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

    local matches = {}
    local currentID = 1
    local maxID = 15000
    local chunkSize = 500

    local function ProcessChunk()
        local endID = math.min(currentID + chunkSize - 1, maxID)
        for decorID = currentID, endID do
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, decorID, true)
            if ok and info and info.name then
                if info.name:lower():find(searchTerm, 1, true) then
                    table.insert(matches, { id = decorID, name = info.name, collected = ((info.quantity or 0) > 0 or (info.numPlaced or 0) > 0) })
                end
            end
        end
        currentID = endID + 1

        if currentID <= maxID then
            C_Timer.After(0.01, ProcessChunk)  -- Process next chunk after brief delay
        else
            -- Done - print results
            if #matches == 0 then
                print("  |cFFFF6600No matches found|r")
            else
                print("  |cFF00FF00Found " .. #matches .. " matches:|r")
                for _, m in ipairs(matches) do
                    local status = m.collected and "|cFF00FF00[Collected]|r" or "|cFF888888[Not collected]|r"
                    print(string.format("    decorID |cFFfcd34d%d|r = %s %s", m.id, m.name, status))
                end
            end
        end
    end

    ProcessChunk()
end

-- Quick lookup of specific decorIDs (for debugging data issues)
function HDG_Debug.CheckDecorIDs(...)
    local ids = {...}
    if #ids == 0 then
        HDG_Debug.PrintAlways("Usage: /hdgdebug checkids 3839 11873")
        return
    end

    HDG_Debug.PrintAlways("Checking decorIDs:")
    for _, decorID in ipairs(ids) do
        local id = tonumber(decorID)
        if id then
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, id, true)
            if ok and info and info.name then
                local status = ((info.quantity or 0) > 0 or (info.numPlaced or 0) > 0) and "|cFF00FF00[Collected]|r" or ""
                print(string.format("  |cFFfcd34d%d|r = %s %s", id, info.name, status))
            else
                print(string.format("  |cFFfcd34d%d|r = |cFFFF0000not found|r", id))
            end
        end
    end
end

-- Export model file IDs for all decor items (for batch rendering with wow.export / WMV)
-- Output: itemID,decorID,assetFileID,modelSceneID,name
function HDG_Debug.DumpModelAssets()
    if not C_HousingCatalog or not C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        HDG_Debug.PrintAlways("|cFFFF0000C_HousingCatalog API not available|r")
        return
    end
    if not HDG_AllDecorDB then
        HDG_Debug.PrintAlways("|cFFFF0000HDG_AllDecorDB not loaded|r")
        return
    end

    HDG_Debug.PrintAlways("Exporting model asset IDs for all items...")

    local items = {}
    for itemID, entry in pairs(HDG_AllDecorDB) do
        items[#items + 1] = { itemID = itemID, decorID = entry[1] }
    end
    table.sort(items, function(a, b) return a.itemID < b.itemID end)

    local results = { "itemID,decorID,assetFileID,modelSceneID,name" }
    local count = 0
    local idx = 1
    local chunkSize = 200

    local function ProcessChunk()
        local endIdx = math.min(idx + chunkSize - 1, #items)
        for i = idx, endIdx do
            local item = items[i]
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, item.decorID, true)
            if ok and info then
                local name = (info.name or ""):gsub(",", ";"):gsub("\n", " "):gsub("\r", " ")
                local asset = info.asset or ""
                local sceneID = info.uiModelSceneID or ""
                results[#results + 1] = item.itemID .. "," .. item.decorID .. "," .. asset .. "," .. sceneID .. "," .. name
                count = count + 1
            end
        end
        idx = endIdx + 1

        if idx <= #items then
            C_Timer.After(0.01, ProcessChunk)
        else
            local text = table.concat(results, "\n")

            if not HDG_Debug._tagExportFrame then
                local f = CreateFrame("Frame", "HDGTagExportFrame", UIParent, "BackdropTemplate")
                f:SetSize(900, 600)
                f:SetPoint("CENTER")
                f:SetBackdrop({ bgFile = "Interface/Tooltips/UI-Tooltip-Background", edgeFile = "Interface/Tooltips/UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
                f:SetBackdropColor(0.1, 0.1, 0.12, 0.95)
                f:SetBackdropBorderColor(0.08, 0.72, 0.65, 1)
                f:SetFrameStrata("DIALOG")
                f:SetMovable(true)
                f:EnableMouse(true)
                f:RegisterForDrag("LeftButton")
                f:SetScript("OnDragStart", f.StartMoving)
                f:SetScript("OnDragStop", f.StopMovingOrSizing)

                local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
                title:SetPoint("TOP", 0, -10)
                f.title = title

                local instr = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
                instr:SetPoint("TOP", title, "BOTTOM", 0, -5)
                instr:SetText(L["TIP_SELECT_ALL_COPY"])

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

                local eb = CreateFrame("EditBox", nil, sf)
                eb:SetMultiLine(true)
                eb:SetFontObject("GameFontHighlightSmall")
                eb:SetWidth(860)
                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_Debug._tagExportFrame = f
            end

            local f = HDG_Debug._tagExportFrame
            f.title:SetText("Model Assets: " .. count .. "/" .. #items .. " items")
            f:Show()
            f.editBox:SetText(text)
            f.editBox:HighlightText()
            f.editBox:SetFocus()

            HDG_Debug.PrintAlways(string.format(
                "|cFF00FF00Model asset export complete: %d/%d items.|r Select all + copy.",
                count, #items))
        end
    end

    ProcessChunk()
end

-- Dump Blizzard dataTagsByID for all items in AllDecorDB to debug window
-- Output format: itemID|decorID|name|tag1,tag2,tag3 (pipeline-parseable)
-- Usage: /hdgdebug tags (or title bar bug icon)
function HDG_Debug.DumpBlizzardTags()
    if not C_HousingCatalog or not C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        HDG_Debug.PrintAlways("|cFFFF0000C_HousingCatalog API not available|r")
        return
    end
    if not HDG_AllDecorDB then
        HDG_Debug.PrintAlways("|cFFFF0000HDG_AllDecorDB not loaded|r")
        return
    end

    HDG_Debug.PrintAlways("Dumping full catalog info for all items...")

    -- Serialize a value (plain ASCII only, no unicode, no tabs, no pipes)
    local SEP = ","
    local function Clean(s)
        s = s:gsub("|c%x%x%x%x%x%x%x%x", "")  -- strip color codes
        s = s:gsub("|r", "")                      -- strip color reset
        s = s:gsub("|T[^|]*|t", "")              -- strip textures (icons)
        s = s:gsub("|H[^|]*|h", "")              -- strip hyperlinks
        s = s:gsub("|h", "")                      -- strip hyperlink close
        s = s:gsub("|n", " ")                     -- WoW newlines
        s = s:gsub("\n", " "):gsub("\r", " "):gsub("\t", " ")
        s = s:gsub(string.char(10), " "):gsub(string.char(13), " ")
        s = s:gsub(",", ";")   -- escape commas since we use CSV
        s = s:gsub("  +", " ") -- collapse multiple spaces
        return s:match("^%s*(.-)%s*$") or s  -- trim
    end
    local function Serialize(val)
        if val == nil then return "" end
        local t = type(val)
        if t == "boolean" then return val and "true" or "false" end
        if t == "number" then return tostring(val) end
        if t == "string" then return Clean(val) end
        if t == "table" then
            local parts = {}
            for k, v in pairs(val) do
                parts[#parts + 1] = tostring(k) .. "=" .. Clean(tostring(v))
            end
            table.sort(parts)
            return table.concat(parts, ";")
        end
        return Clean(tostring(val))
    end

    -- Collect all decorIDs from AllDecorDB
    local items = {}
    for itemID, entry in pairs(HDG_AllDecorDB) do
        items[#items + 1] = { itemID = itemID, decorID = entry[1] }
    end
    table.sort(items, function(a, b) return a.itemID < b.itemID end)

    -- First pass: discover all field names from first valid item
    local fieldNames = {}
    local fieldOrder = {}
    for _, item in ipairs(items) do
        local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, item.decorID, true)
        if ok and info then
            for k in pairs(info) do
                if not fieldNames[k] then
                    fieldNames[k] = true
                    fieldOrder[#fieldOrder + 1] = k
                end
            end
            break
        end
    end
    table.sort(fieldOrder)

    local results = {}
    local count = 0
    local idx = 1
    local chunkSize = 200

    local function ProcessChunk()
        local endIdx = math.min(idx + chunkSize - 1, #items)
        for i = idx, endIdx do
            local item = items[i]
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, item.decorID, true)
            if ok and info then
                local row = {}
                row[1] = tostring(item.itemID)
                row[2] = tostring(item.decorID)
                for _, field in ipairs(fieldOrder) do
                    row[#row + 1] = Serialize(info[field])
                end
                results[#results + 1] = table.concat(row, SEP)
                count = count + 1
            end
        end
        idx = endIdx + 1

        if idx <= #items then
            C_Timer.After(0.01, ProcessChunk)
        else
            -- Done — build CSV text and show copy dialog
            local header = "itemID" .. SEP .. "decorID" .. SEP .. table.concat(fieldOrder, SEP)
            local lines = { header }
            for _, row in ipairs(results) do lines[#lines + 1] = row end
            local text = table.concat(lines, "\n")

            -- Reuse or create a simple copy-paste dialog
            if not HDG_Debug._tagExportFrame then
                local f = CreateFrame("Frame", "HDGTagExportFrame", UIParent, "BackdropTemplate")
                f:SetSize(900, 600)
                f:SetPoint("CENTER")
                f:SetBackdrop({ bgFile = "Interface/Tooltips/UI-Tooltip-Background", edgeFile = "Interface/Tooltips/UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
                f:SetBackdropColor(0.1, 0.1, 0.12, 0.95)
                f:SetBackdropBorderColor(0.08, 0.72, 0.65, 1)
                f:SetFrameStrata("DIALOG")
                f:SetMovable(true)
                f:EnableMouse(true)
                f:RegisterForDrag("LeftButton")
                f:SetScript("OnDragStart", f.StartMoving)
                f:SetScript("OnDragStop", f.StopMovingOrSizing)

                local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
                title:SetPoint("TOP", 0, -10)
                f.title = title

                local instr = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
                instr:SetPoint("TOP", title, "BOTTOM", 0, -5)
                instr:SetText(L["TIP_SELECT_ALL_COPY"])

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

                local eb = CreateFrame("EditBox", nil, sf)
                eb:SetMultiLine(true)
                eb:SetFontObject("GameFontHighlightSmall")
                eb:SetWidth(860)
                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_Debug._tagExportFrame = f
            end

            local f = HDG_Debug._tagExportFrame
            f.title:SetText("Catalog Dump: " .. count .. "/" .. #items .. " items")
            f:Show()
            f.editBox:SetText(text)
            f.editBox:HighlightText()
            f.editBox:SetFocus()

            HDG_Debug.PrintAlways(string.format(
                "|cFF00FF00Catalog dump complete: %d/%d items. %d fields per item.|r Select all + copy.",
                count, #items, #fieldOrder))
        end
    end

    ProcessChunk()
end

-- Scan catalog for items missing from AllDecorDB, output in DB format for copy-paste
-- Usage: /hdgdebug scan
function HDG_Debug.ScanMissing()
    if not C_HousingCatalog or not C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        HDG_Debug.PrintAlways("|cFFFF0000C_HousingCatalog API not available|r")
        return
    end

    HDG_Debug.PrintAlways("Scanning catalog for items not in AllDecorDB...")

    local missing = {}
    local currentID = 1
    local maxID = 20000
    local chunkSize = 500

    -- Strip WoW escape sequences from sourceText
    local function StripEscapes(text)
        if not text then return "" end
        text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
        text = text:gsub("|r", "")
        text = text:gsub("|T[^|]*|t", "")
        text = text:gsub("|H[^|]*|h", "")
        text = text:gsub("|h", "")
        text = text:gsub("|n", "\n")
        return text:match("^%s*(.-)%s*$") or text
    end

    -- Parse sourceText into sourceType, sourceName, sourceDetail
    local function ClassifySource(rawSource)
        if not rawSource or rawSource == "" then return 0, "", "" end
        local text = StripEscapes(rawSource)
        if text == "" then return 0, "", "" end

        -- Simple keyword types
        if text == "Crafted" then return 6, "", "" end
        if text == "Learned" then return 7, "", "" end
        if text == "Gathering" then return 8, "", "" end

        -- Parse key-value lines
        local kv = {}
        local isPromotion = false
        for line in text:gmatch("[^\n]+") do
            line = line:match("^%s*(.-)%s*$") or line
            if line == "Promotion" then
                isPromotion = true
            else
                local key, val = line:match("^([A-Za-z ]+):%s*(.+)$")
                if key and not kv[key] then
                    kv[key] = val
                end
            end
        end

        -- Classify by priority
        if kv["Achievement"] then return 1, kv["Achievement"], kv["Category"] or "" end
        if kv["World Quest"] then return 3, kv["World Quest"], kv["Zone"] or "" end
        if kv["Quest"] then return 2, kv["Quest"], kv["Zone"] or "" end
        if kv["Drop"] then return 4, kv["Drop"], kv["Zone"] or "" end
        if kv["Vendor"] or kv["Vendors"] then return 5, kv["Vendor"] or kv["Vendors"] or "", kv["Zone"] or "" end
        if kv["Treasure"] then return 9, kv["Treasure"], "" end
        if kv["Profession"] then return 11, kv["Profession"], "" end
        if isPromotion then return 10, "Promotion", "" end
        return 0, "", ""
    end

    -- Escape a string for Lua output
    local function LuaEscape(s)
        return s:gsub("\\", "\\\\"):gsub('"', '\\"')
    end

    local function ProcessChunk()
        local endID = math.min(currentID + chunkSize - 1, maxID)
        for decorID = currentID, endID do
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, decorID, true)
            if ok and info and info.name and info.itemID then
                if not HDG_AllDecorDB or not HDG_AllDecorDB[info.itemID] then
                    local sourceType, sourceName, sourceDetail = ClassifySource(info.sourceText)
                    table.insert(missing, {
                        itemID = info.itemID,
                        decorID = decorID,
                        name = info.name,
                        sourceType = sourceType,
                        sourceName = sourceName,
                        sourceDetail = sourceDetail,
                        rawSource = info.sourceText,
                    })
                end
            end
        end
        currentID = endID + 1

        if currentID <= maxID then
            C_Timer.After(0.01, ProcessChunk)
        else
            -- Done - output to debug window
            HDG_Debug.ClearWindow()
            HDG_Debug.ShowWindow("Missing Items (" .. #missing .. ")")
            local W = HDG_Debug.WindowPrint

            if #missing == 0 then
                W("No missing items found - AllDecorDB is up to date!")
                HDG_Debug.PrintAlways("|cFF00FF00No missing items found.|r")
                return
            end

            -- Sort by itemID for clean output
            table.sort(missing, function(a, b) return a.itemID < b.itemID end)

            W("-- Missing items: " .. #missing .. " (paste into HDG_AllDecorDB.lua)")
            W("-- Format: [itemID] = {decorID, sourceType, \"sourceName\", \"sourceDetail\", 0}")
            W("")

            local timestamp = time()
            for _, item in ipairs(missing) do
                local raw = StripEscapes(item.rawSource or ""):gsub("\n", " | ")
                local comment = item.name
                if raw ~= "" then comment = comment .. " -- " .. raw end
                local line = string.format('    [%d] = {%d, %d, "%s", "%s", %d}, -- %s',
                    item.itemID, item.decorID, item.sourceType,
                    LuaEscape(item.sourceName), LuaEscape(item.sourceDetail), timestamp, comment)
                W(line)
            end

            HDG_Debug.PrintAlways(string.format("|cFF00FF00Scan complete: %d missing items.|r Click Copy in debug window.", #missing))
        end
    end

    ProcessChunk()
end

-- Scan AllDecorDB for entries with incomplete data (sourceType=0, no expansion, etc.)
-- Re-checks the Blizzard API for updated sourceText and outputs corrected DB lines
-- Usage: /hdgdebug incomplete
function HDG_Debug.ScanIncomplete()
    if not C_HousingCatalog or not C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        HDG_Debug.PrintAlways("|cFFFF0000C_HousingCatalog API not available|r")
        return
    end
    if not HDG_AllDecorDB then
        HDG_Debug.PrintAlways("|cFFFF0000HDG_AllDecorDB not loaded|r")
        return
    end

    HDG_Debug.PrintAlways("Scanning AllDecorDB for incomplete entries...")

    -- Reuse source classifier from ScanMissing
    local function StripEscapes(text)
        if not text then return "" end
        text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
        text = text:gsub("|r", "")
        text = text:gsub("|T[^|]*|t", "")
        text = text:gsub("|H[^|]*|h", "")
        text = text:gsub("|h", "")
        text = text:gsub("|n", "\n")
        return text:match("^%s*(.-)%s*$") or text
    end

    local function ClassifySource(rawSource)
        if not rawSource or rawSource == "" then return 0, "", "" end
        local text = StripEscapes(rawSource)
        if text == "" then return 0, "", "" end
        if text == "Crafted" then return 6, "", "" end
        if text == "Learned" then return 7, "", "" end
        if text == "Gathering" then return 8, "", "" end
        local kv = {}
        local isPromotion = false
        for line in text:gmatch("[^\n]+") do
            line = line:match("^%s*(.-)%s*$") or line
            if line == "Promotion" then
                isPromotion = true
            else
                local key, val = line:match("^([A-Za-z ]+):%s*(.+)$")
                if key and not kv[key] then kv[key] = val end
            end
        end
        if kv["Achievement"] then return 1, kv["Achievement"], kv["Category"] or "" end
        if kv["World Quest"] then return 3, kv["World Quest"], kv["Zone"] or "" end
        if kv["Quest"] then return 2, kv["Quest"], kv["Zone"] or "" end
        if kv["Drop"] then return 4, kv["Drop"], kv["Zone"] or "" end
        if kv["Vendor"] or kv["Vendors"] then return 5, kv["Vendor"] or kv["Vendors"] or "", kv["Zone"] or "" end
        if kv["Treasure"] then return 9, kv["Treasure"], "" end
        if kv["Profession"] then return 11, kv["Profession"], "" end
        if isPromotion then return 10, "Promotion", "" end
        return 0, "", ""
    end

    local function LuaEscape(s)
        return s:gsub("\\", "\\\\"):gsub('"', '\\"')
    end

    local incomplete = {}
    for itemID, entry in pairs(HDG_AllDecorDB) do
        local decorID = entry[1]
        local sourceType = entry[2]
        local sourceName = entry[3]
        -- Flag if sourceType is unknown or sourceName is empty
        if sourceType == 0 or (sourceName == "" and sourceType ~= 6 and sourceType ~= 7 and sourceType ~= 8) then
            table.insert(incomplete, { itemID = itemID, decorID = decorID, oldSourceType = sourceType })
        end
    end

    if #incomplete == 0 then
        HDG_Debug.PrintAlways("|cFF00FF00No incomplete entries found.|r")
        return
    end

    table.sort(incomplete, function(a, b) return a.itemID < b.itemID end)
    HDG_Debug.PrintAlways(string.format("Found %d incomplete entries, re-checking API...", #incomplete))

    -- Process in chunks to avoid freezing
    local idx = 1
    local chunkSize = 100
    local updated = {}
    local stillUnknown = {}

    local function ProcessChunk()
        local endIdx = math.min(idx + chunkSize - 1, #incomplete)
        for i = idx, endIdx do
            local item = incomplete[i]
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, item.decorID, true)
            if ok and info then
                local newType, newName, newDetail = ClassifySource(info.sourceText)
                if newType ~= 0 or (newName ~= "" and item.oldSourceType == 0) then
                    table.insert(updated, {
                        itemID = item.itemID,
                        decorID = item.decorID,
                        name = info.name or "?",
                        sourceType = newType,
                        sourceName = newName,
                        sourceDetail = newDetail,
                        rawSource = info.sourceText,
                    })
                else
                    -- Gather extra API data for unknown items
                    local catName, subcatName = "", ""
                    if info.categoryIDs and info.categoryIDs[1] and C_HousingCatalog.GetCatalogCategoryInfo then
                        local catInfo = C_HousingCatalog.GetCatalogCategoryInfo(info.categoryIDs[1])
                        if catInfo then catName = catInfo.name or "" end
                    end
                    if info.subcategoryIDs and info.subcategoryIDs[1] and C_HousingCatalog.GetCatalogCategoryInfo then
                        local subInfo = C_HousingCatalog.GetCatalogCategoryInfo(info.subcategoryIDs[1])
                        if subInfo then subcatName = subInfo.name or "" end
                    end
                    local tags = ""
                    if info.tags then
                        local tagList = {}
                        for _, tag in ipairs(info.tags) do tagList[#tagList + 1] = tag end
                        tags = table.concat(tagList, ", ")
                    end
                    local placement = ""
                    if info.canPlaceIndoors and info.canPlaceOutdoors then placement = "Both"
                    elseif info.canPlaceIndoors then placement = "Indoors"
                    elseif info.canPlaceOutdoors then placement = "Outdoors" end
                    table.insert(stillUnknown, {
                        itemID = item.itemID,
                        decorID = item.decorID,
                        name = info.name or "?",
                        rawSource = info.sourceText,
                        category = catName,
                        subcategory = subcatName,
                        tags = tags,
                        placement = placement,
                        sizeValue = info.sizeValue or 0,
                        collected = ((info.quantity or 0) > 0 or (info.numPlaced or 0) > 0),
                    })
                end
            end
        end
        idx = endIdx + 1

        if idx <= #incomplete then
            C_Timer.After(0.01, ProcessChunk)
        else
            -- Output results
            HDG_Debug.ClearWindow()
            HDG_Debug.ShowWindow("Incomplete Entries (" .. #incomplete .. ")")
            local W = HDG_Debug.WindowPrint

            if #updated > 0 then
                W("-- UPDATED: " .. #updated .. " entries now have source data (replace in HDG_AllDecorDB.lua)")
                W("")
                local timestamp = time()
                for _, item in ipairs(updated) do
                    local expField = ""
                    local oldEntry = HDG_AllDecorDB[item.itemID]
                    if oldEntry and oldEntry.exp then expField = ', exp = "' .. oldEntry.exp .. '"' end
                    local line = string.format('    [%d] = {%d, %d, "%s", "%s", %d%s}, -- %s',
                        item.itemID, item.decorID, item.sourceType,
                        LuaEscape(item.sourceName), LuaEscape(item.sourceDetail),
                        timestamp, expField, item.name)
                    W(line)
                end
                W("")
            end

            if #stillUnknown > 0 then
                W("-- STILL UNKNOWN: " .. #stillUnknown .. " entries (API has no source data yet)")
                W("")
                for _, item in ipairs(stillUnknown) do
                    local raw = StripEscapes(item.rawSource or ""):gsub("\n", " | ")
                    local cat = item.category
                    if item.subcategory ~= "" then cat = cat .. " > " .. item.subcategory end
                    local status = item.collected and "COLLECTED" or "not collected"
                    W(string.format("[%d] %s  (decorID %d)", item.itemID, item.name, item.decorID))
                    W(string.format("  Category: %s | Placement: %s | Size: %d | %s",
                        cat, item.placement, item.sizeValue, status))
                    if item.tags ~= "" then W("  Tags: " .. item.tags) end
                    if raw ~= "" then W("  Source: " .. raw) end
                    W("")
                end
            end

            HDG_Debug.PrintAlways(string.format("|cFF00FF00Scan complete: %d updated, %d still unknown.|r", #updated, #stillUnknown))
        end
    end

    ProcessChunk()
end

-- Export full catalog to SavedVariables for offline analysis
-- Usage: /hdgdebug export → /reload → read HDG_DB.catalogExport
function HDG_Debug.ExportCatalog()
    if not C_HousingCatalog or not C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        HDG_Debug.PrintAlways("|cFFFF0000C_HousingCatalog API not available|r")
        return
    end

    HDG_Debug.PrintAlways("Exporting full catalog to SavedVariables...")

    local export = {}
    local count = 0
    local currentID = 1
    local maxID = 20000
    local chunkSize = 500

    local function ProcessChunk()
        local endID = math.min(currentID + chunkSize - 1, maxID)
        for decorID = currentID, endID do
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, decorID, true)
            if ok and info and info.name then
                local entry = {
                    name = info.name,
                    collected = ((info.quantity or 0) > 0 or (info.numPlaced or 0) > 0),
                }
                if info.itemID then entry.itemID = info.itemID end
                if info.sourceText and info.sourceText ~= "" then
                    entry.source = info.sourceText
                end
                if info.dataTagsByID then
                    local tags = {}
                    for _, t in pairs(info.dataTagsByID) do tags[#tags+1] = t end
                    if #tags > 0 then entry.tags = table.concat(tags, ",") end
                end
                export[decorID] = entry
                count = count + 1
            end
        end
        currentID = endID + 1

        if currentID <= maxID then
            C_Timer.After(0.01, ProcessChunk)
        else
            -- Done - save to HDG_DB
            if _G.HDG_DB then
                _G.HDG_DB.catalogExport = export
                HDG_Debug.PrintAlways(string.format("|cFF00FF00Exported %d catalog items to HDG_DB.catalogExport|r", count))
                HDG_Debug.PrintAlways("Type |cFFfcd34d/reload|r then check SavedVariables file")
            else
                HDG_Debug.PrintAlways("|cFFFF0000HDG_DB not available|r")
            end
        end
    end

    ProcessChunk()
end

function HDG_Debug.DumpCacheStats()
    HDG_Debug.PrintAlways("Cache Statistics:")

    local HDG_Data = privateDB.HDG_Data
    if HDG_Data then
        -- Try to access cache info if available
        print("  |cFFfcd34dHDG_Data module loaded|r")
    else
        print("  |cFFFF6600HDG_Data not available|r")
    end

    -- Shopping list
    if HDG_ShoppingList then
        local count = 0
        for _ in pairs(HDG_ShoppingList) do count = count + 1 end
        print("  |cFFfcd34dShopping List:|r " .. count .. " items")
    end

    -- Character data
    if HDG_CharacterData then
        local charCount = 0
        for _ in pairs(HDG_CharacterData) do charCount = charCount + 1 end
        print("  |cFFfcd34dCharacter Data:|r " .. charCount .. " characters")
    end
end

function HDG_Debug.TestCatalogSearch()
    -- Use debug window for output
    local W = HDG_Debug.WindowPrint
    HDG_Debug.ClearWindow()
    HDG_Debug.ShowWindow("Catalog Search API")

    W("Testing Catalog Search API...")
    W("")

    if not C_HousingCatalog then
        W("[ERROR] C_HousingCatalog not available")
        return
    end

    -- Check for CreateCatalogSearcher
    if not C_HousingCatalog.CreateCatalogSearcher then
        W("[ERROR] CreateCatalogSearcher not available")
        W("")
        W("Available functions:")
        for k, v in pairs(C_HousingCatalog) do
            W("  " .. k .. " - " .. type(v))
        end
        return
    end

    W("[OK] CreateCatalogSearcher exists")

    local catalogSearcher = C_HousingCatalog.CreateCatalogSearcher()
    if not catalogSearcher then
        W("[ERROR] CreateCatalogSearcher returned nil")
        return
    end

    W("[OK] Searcher created")
    W("Searcher type: " .. type(catalogSearcher))
    W("")

    -- Check for metatable (WoW objects often use metatables)
    local mt = getmetatable(catalogSearcher)
    if mt then
        W("[OK] Has metatable")
        if mt.__index and type(mt.__index) == "table" then
            W("Metatable methods:")
            for k, v in pairs(mt.__index) do
                W("  " .. tostring(k) .. " - " .. type(v))
            end
        end
    else
        W("[WARN] No metatable found")
    end
    W("")

    -- Direct method checks
    W("Direct method checks:")
    W("  SetOwnedOnly: " .. type(catalogSearcher.SetOwnedOnly))
    W("  SetCollected: " .. type(catalogSearcher.SetCollected))
    W("  SetUncollected: " .. type(catalogSearcher.SetUncollected))
    W("  SetResultsUpdatedCallback: " .. type(catalogSearcher.SetResultsUpdatedCallback))
    W("  RunSearch: " .. type(catalogSearcher.RunSearch))
    W("  GetNumResults: " .. type(catalogSearcher.GetNumResults))
    W("  GetResult: " .. type(catalogSearcher.GetResult))
    W("  GetCatalogSearchResults: " .. type(catalogSearcher.GetCatalogSearchResults))
    W("  GetAllSearchItems: " .. type(catalogSearcher.GetAllSearchItems))
    W("")

    -- Try to configure and run search
    W("Configuring search...")

    if catalogSearcher.SetOwnedOnly then
        catalogSearcher:SetOwnedOnly(false)
        W("  SetOwnedOnly(false) - OK")
    else
        W("  [WARN] SetOwnedOnly not available")
    end

    if catalogSearcher.SetCollected then
        catalogSearcher:SetCollected(true)
        W("  SetCollected(true) - OK")
    else
        W("  [WARN] SetCollected not available")
    end

    if catalogSearcher.SetUncollected then
        catalogSearcher:SetUncollected(true)
        W("  SetUncollected(true) - OK")
    else
        W("  [WARN] SetUncollected not available")
    end
    W("")

    -- Set callback
    catalogSearcher:SetResultsUpdatedCallback(function()
        W("[OK] Search callback fired!")
        W("")

        -- Try multiple methods to get results count
        local count = 0
        if catalogSearcher.GetNumResults then
            count = catalogSearcher:GetNumResults()
            W("GetNumResults: " .. count)
        else
            W("[WARN] GetNumResults not available")
        end

        -- Try GetCatalogSearchResults (what HDG_DecorPreviewTab uses)
        local allItems = nil
        if catalogSearcher.GetCatalogSearchResults then
            allItems = catalogSearcher:GetCatalogSearchResults()
            W("GetCatalogSearchResults: " .. (allItems and #allItems or "nil") .. " items")
        else
            W("[WARN] GetCatalogSearchResults not available")
        end

        -- Try GetAllSearchItems (fallback method)
        if catalogSearcher.GetAllSearchItems then
            local items2 = catalogSearcher:GetAllSearchItems()
            W("GetAllSearchItems: " .. (items2 and #items2 or "nil") .. " items")
            if not allItems or #allItems == 0 then
                allItems = items2
            end
        else
            W("[WARN] GetAllSearchItems not available")
        end

        -- Try GetResult (indexed access)
        if catalogSearcher.GetResult then
            W("[OK] GetResult method available")
            if count > 0 then
                W("First 3 results via GetResult:")
                for i = 1, math.min(3, count) do
                    local result = catalogSearcher:GetResult(i)
                    if result then
                        for k, v in pairs(result) do
                            W("    " .. tostring(k) .. " = " .. tostring(v))
                        end
                    end
                end
            end
        else
            W("[WARN] GetResult not available")
        end
        W("")

        -- Show first few results from allItems if we got any
        if allItems and #allItems > 0 then
            W("First 5 items from allItems:")
            W("")
            for i = 1, math.min(5, #allItems) do
                local entry = allItems[i]
                if entry then
                    W("--- Entry " .. i .. " (type: " .. type(entry) .. ") ---")
                    if type(entry) == "table" then
                        for k, v in pairs(entry) do
                            W("  " .. tostring(k) .. " = " .. tostring(v))
                        end
                        -- Try GetCatalogEntryInfo with table entry
                        if C_HousingCatalog.GetCatalogEntryInfo then
                            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfo, entry)
                            if ok and info then
                                W("  [GetCatalogEntryInfo]:")
                                W("    name: " .. tostring(info.name))
                                W("    iconTexture: " .. tostring(info.iconTexture))
                                W("    recordID: " .. tostring(info.recordID))
                                if info.dataTagsByID then
                                    local tags = {}
                                    for _, tag in pairs(info.dataTagsByID) do
                                        table.insert(tags, tag)
                                    end
                                    W("    tags: " .. table.concat(tags, ", "))
                                end
                            else
                                W("  [ERROR] GetCatalogEntryInfo failed: " .. tostring(info))
                            end
                        end
                    elseif type(entry) == "userdata" then
                        W("  Entry is userdata - checking fields:")
                        W("    recordID: " .. tostring(entry.recordID))
                        W("    entryType: " .. tostring(entry.entryType))
                        -- Try GetCatalogEntryInfo
                        if C_HousingCatalog.GetCatalogEntryInfo then
                            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfo, entry)
                            if ok and info then
                                W("  [GetCatalogEntryInfo]:")
                                for k, v in pairs(info) do
                                    W("    " .. tostring(k) .. " = " .. tostring(v))
                                end
                            else
                                W("  [ERROR] GetCatalogEntryInfo failed")
                            end
                        end
                    else
                        W("  Value: " .. tostring(entry))
                    end
                    W("")
                end
            end
        end

        W("=== Search Complete ===")

        -- Release
        if catalogSearcher.Release then
            catalogSearcher:Release()
        end
    end)

    -- Run search
    W("Running search...")
    if catalogSearcher.RunSearch then
        catalogSearcher:RunSearch()
        W("[OK] RunSearch called - waiting for callback...")
    else
        W("[ERROR] RunSearch not available")
    end
end

function HDG_Debug.DumpHousingAPI()
    local W = HDG_Debug.WindowPrint
    HDG_Debug.ClearWindow()
    HDG_Debug.ShowWindow("C_HousingCatalog API")

    W("C_HousingCatalog API Functions:")
    W("")

    if C_HousingCatalog then
        local funcs = {}
        for k, _ in pairs(C_HousingCatalog) do
            table.insert(funcs, k)
        end
        table.sort(funcs)

        for _, name in ipairs(funcs) do
            W("  " .. name .. " - " .. type(C_HousingCatalog[name]))
        end
        W("")
        W("Total: " .. #funcs .. " functions")
    else
        W("[ERROR] C_HousingCatalog not available")
    end
end

function HDG_Debug.DumpSettings()
    local W = HDG_Debug.WindowPrint
    HDG_Debug.ClearWindow()
    HDG_Debug.ShowWindow("Settings")

    -- HDG_Settings retired v2.41; dump HDG.Store.state instead
    W("HDG Store State (HDG_Settings retired v2.41):")
    W("")

    local state = HDG and HDG.Store and HDG.Store.state
    if state then
        local function dumpToWindow(tbl, indent)
            indent = indent or ""
            for k, v in pairs(tbl) do
                if type(v) == "table" then
                    W(indent .. tostring(k) .. " = {")
                    dumpToWindow(v, indent .. "  ")
                    W(indent .. "}")
                else
                    W(indent .. tostring(k) .. " = " .. tostring(v))
                end
            end
        end
        dumpToWindow(state, "  ")
    else
        W("[WARN] HDG.Store.state not loaded")
    end
end

function HDG_Debug.PrintStatus()
    local W = HDG_Debug.WindowPrint
    HDG_Debug.ClearWindow()
    HDG_Debug.ShowWindow("Addon Status")

    W("Housing Decor Guide - Status")
    W("")
    W("Debug Mode: " .. (debugEnabled and "ON" or "OFF"))
    W("Event Logging: " .. (eventLogging and "ON" or "OFF"))
    W("Events Logged: " .. #eventLog)
    W("")

    -- Module status
    W("Modules:")
    local modules = { "HDG_Data", "HDG_UI", "HDG_RecipesTab", "HDG_DecorPreviewTab", "HDG_LumberTracker" }
    for _, modName in ipairs(modules) do
        local loaded = privateDB[modName] ~= nil
        W("  " .. modName .. ": " .. (loaded and "loaded" or "not loaded"))
    end
    W("")

    -- API availability
    W("APIs:")
    W("  C_HousingCatalog: " .. (C_HousingCatalog and "available" or "missing"))
    W("  C_TradeSkillUI: " .. (C_TradeSkillUI and "available" or "missing"))
end

-- ============================================================================
-- MEMORY CHART WINDOW
-- ============================================================================

local MEM_MAX_SAMPLES = 300
local MEM_CHART_WIDTH = 340
local MEM_CHART_HEIGHT = 140

local memData = {}
local memDataHead = 0
local memDataCount = 0
local memPeak = 0

local memChartFrame, memBars, memGridLines, memGridLabels, memXLabels, memHeaderText
local memoryTicker = nil

local function NiceScale(maxVal)
    if maxVal <= 0 then return 1 end
    local exp = math.floor(math.log10(maxVal))
    local base = 10 ^ exp
    local frac = maxVal / base
    if frac <= 1 then return base
    elseif frac <= 2 then return 2 * base
    elseif frac <= 5 then return 5 * base
    else return 10 * base end
end

local function FormatKB(kb)
    if kb >= 1024 then
        return string.format("%.1f MB", kb / 1024)
    else
        return string.format("%.0f KB", kb)
    end
end

local function CreateMemoryChart()
    if memChartFrame then return memChartFrame end

    memChartFrame = CreateFrame("Frame", "HDGMemoryChart", UIParent, "BackdropTemplate")
    memChartFrame:SetSize(400, 200)
    memChartFrame:SetPoint("TOPRIGHT", -200, -200)
    memChartFrame:SetMovable(true)
    memChartFrame:SetClampedToScreen(true)
    memChartFrame:EnableMouse(true)
    memChartFrame:SetFrameStrata("DIALOG")

    memChartFrame:SetBackdrop({
        bgFile = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    local scheme = GetScheme()
    if scheme then
        memChartFrame:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.95)
        memChartFrame:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    else
        memChartFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
        memChartFrame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
    end

    memChartFrame:SetScript("OnMouseDown", function(self, button)
        if button == "LeftButton" then self:StartMoving() end
    end)
    memChartFrame:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end)

    -- Header
    memHeaderText = memChartFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    memHeaderText:SetPoint("TOP", 0, -8)
    memHeaderText:SetText("|cFF00FFFF[HDG Memory]|r")

    -- Close button
    local closeBtn = CreateFrame("Button", nil, memChartFrame, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -2, -2)
    closeBtn:SetScript("OnClick", function() HDG_Debug.ToggleMemoryMonitor() end)

    -- Chart area (inset from frame edges)
    local chartArea = CreateFrame("Frame", nil, memChartFrame)
    chartArea:SetPoint("TOPLEFT", 45, -28)
    chartArea:SetPoint("BOTTOMRIGHT", -15, 25)

    -- Bar textures
    memBars = {}
    local barWidth = MEM_CHART_WIDTH / MEM_MAX_SAMPLES
    for i = 1, MEM_MAX_SAMPLES do
        local bar = chartArea:CreateTexture(nil, "ARTWORK")
        bar:SetColorTexture(0.0, 0.8, 0.8, 0.7)
        bar:SetWidth(barWidth)
        bar:SetHeight(0.001)
        bar:SetPoint("BOTTOMLEFT", (i - 1) * barWidth, 0)
        bar:Hide()
        memBars[i] = bar
    end

    -- Grid lines + Y-axis labels
    memGridLines = {}
    memGridLabels = {}
    for i = 1, 4 do
        local line = chartArea:CreateTexture(nil, "BACKGROUND")
        line:SetColorTexture(0.4, 0.4, 0.4, 0.3)
        line:SetHeight(1)
        line:SetPoint("LEFT", 0, 0)
        line:SetPoint("RIGHT", 0, 0)
        memGridLines[i] = line

        local label = memChartFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("RIGHT", chartArea, "LEFT", -3, 0)
        label:SetJustifyH("RIGHT")
        label:SetTextColor(0.6, 0.6, 0.6)
        memGridLabels[i] = label
    end

    -- X-axis labels
    memXLabels = {}
    local xTexts = {"5m", "4m", "3m", "2m", "1m", "now"}
    for i = 1, 6 do
        local label = memChartFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOP", chartArea, "BOTTOMLEFT", ((i - 1) / 5) * MEM_CHART_WIDTH, -3)
        label:SetTextColor(0.6, 0.6, 0.6)
        label:SetText(xTexts[i])
        memXLabels[i] = label
    end

    memChartFrame:Hide()
    return memChartFrame
end

local function UpdateMemoryChart()
    UpdateAddOnMemoryUsage()
    local kb = GetAddOnMemoryUsage("HousingDecorGuide")

    -- Write to circular buffer
    memDataHead = (memDataHead % MEM_MAX_SAMPLES) + 1
    memData[memDataHead] = kb
    if memDataCount < MEM_MAX_SAMPLES then memDataCount = memDataCount + 1 end
    if kb > memPeak then memPeak = kb end

    -- Find max in current buffer for auto-scale
    local maxVal = 0
    for i = 1, memDataCount do
        local idx = ((memDataHead - memDataCount + i - 1) % MEM_MAX_SAMPLES) + 1
        if memData[idx] > maxVal then maxVal = memData[idx] end
    end

    local ceiling = NiceScale(maxVal * 1.1)
    if ceiling <= 0 then ceiling = 1 end

    -- Update header
    memHeaderText:SetText(string.format("|cFF00FFFF[HDG Memory]|r %s  |  Peak: %s", FormatKB(kb), FormatKB(memPeak)))

    -- Update bars — oldest on left, newest on right
    local chartHeight = MEM_CHART_HEIGHT
    local startSlot = MEM_MAX_SAMPLES - memDataCount + 1

    for i = 1, MEM_MAX_SAMPLES do
        if i < startSlot then
            memBars[i]:Hide()
        else
            local dataIdx = i - startSlot + 1
            local bufIdx = ((memDataHead - memDataCount + dataIdx - 1) % MEM_MAX_SAMPLES) + 1
            local val = memData[bufIdx] or 0
            local h = math.max((val / ceiling) * chartHeight, 0.001)
            memBars[i]:SetHeight(h)
            memBars[i]:Show()
        end
    end

    -- Update grid lines and labels (4 evenly spaced)
    for i = 1, 4 do
        local frac = i / 4
        local yOff = frac * chartHeight
        memGridLines[i]:ClearAllPoints()
        local parent = memBars[1]:GetParent()
        memGridLines[i]:SetPoint("LEFT", parent, "BOTTOMLEFT", 0, yOff)
        memGridLines[i]:SetPoint("RIGHT", parent, "BOTTOMRIGHT", 0, yOff)
        memGridLabels[i]:ClearAllPoints()
        memGridLabels[i]:SetPoint("RIGHT", parent, "BOTTOMLEFT", -3, yOff)
        memGridLabels[i]:SetText(FormatKB(ceiling * frac))
    end
end

function HDG_Debug.ToggleMemoryMonitor()
    CreateMemoryChart()

    if memoryTicker then
        memoryTicker:Cancel()
        memoryTicker = nil
        memChartFrame:Hide()
        HDG_Debug.PrintAlways("Memory chart |cFFFF0000STOPPED|r")
        return
    end

    -- Fresh start — wipe data
    memData = {}
    memDataHead = 0
    memDataCount = 0
    memPeak = 0

    -- Take first sample immediately
    UpdateMemoryChart()
    memChartFrame:Show()

    HDG_Debug.PrintAlways("Memory chart |cFF00FF00STARTED|r (/hdgdebug mem to stop)")
    memoryTicker = C_Timer.NewTicker(1, UpdateMemoryChart)
end

-- ============================================================================
-- EXPORTS
-- ============================================================================

-- Export to global namespace for easy access
HousingDecorGuide.Debug = HDG_Debug
HousingDecorGuide.DumpTable = HDG_Debug.DumpTable

-- (debug load message removed — use /hdgdebug for commands)
