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

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

    W("Current HDG_Settings:")
    W("")

    if HDG_Settings 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(HDG_Settings, "  ")
    else
        W("[WARN] HDG_Settings 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)
