-- ============================================================
-- Decor Vendor Gallery
-- Debug.lua
-- Merged debug commands, catalog checks, dumps, and copy helpers
-- ============================================================
--
-- Slash Commands:
--
-- /dvg or /dvgallery
--   Toggles/opens the Gallery if no subcommand is given.
--
-- /dvg debug
--   Prints Gallery catalog/item/source counts.
--
-- /dvg reloadcatalog
--   Clears and reloads Gallery catalog records, rebuilds items, refreshes the grid,
--   then prints debug counts.
--
-- /dvg dumpmissing
--   Creates SavedVariables dump tables for catalog-only and manual-only items.
--
-- /dvg dumpformatted
--   Creates formatted one-line ActiveItems-style dump lines in SavedVariables.
--
-- /dvgdump or /dvgdump help
--   Shows catalog detail dumper commands.
--
-- /dvgdump details
--   Builds generated Gallery.ItemDetails and Gallery.VendorDetails starter dumps.
--
-- /dvgdump show item
--   Opens a copy/paste box for generated Gallery.ItemDetails starter lines.
--
-- /dvgdump show vendor
--   Opens a copy/paste box for generated Gallery.VendorDetails starter lines.
--
-- /dvgdump show raw
--   Opens a copy/paste box for raw catalog source text.
--
-- /dvgdump clear
--   Clears DecorVendorGalleryDump SavedVariables data.
--
-- ============================================================

local addonName, Gallery = ...

Gallery = _G.DecorVendorGallery or Gallery or {}

_G.DecorVendorGallery = Gallery
_G.DecorVendor_Gallery = Gallery
_G[addonName] = Gallery

Gallery.C = Gallery.C or {}
local C = Gallery.C

-- ============================================================
-- Shared DecorVendorData bridge
-- ============================================================

local DVD = _G.DecorVendorData or _G.DecorVendor_Data

Gallery.Data = DVD
Gallery.ActiveItems = DVD and DVD.ActiveItems or Gallery.ActiveItems or {}

-- Optional compatibility only.
-- Gallery can still work with DecorVendor when it exists, but this debug file
-- does not require the main DecorVendor addon just to load.
local dv = _G.DecorVendor or _G.Decorvendor
if dv then
    dv.Gallery = Gallery
end

local function DVGPrint(msg)
    DEFAULT_CHAT_FRAME:AddMessage(tostring(msg))
end

local function GetSourceList(item)
    local list = {}

    if item and item.sources then
        for sourceType in pairs(item.sources) do
            table.insert(list, sourceType)
        end
    end

    table.sort(list)
    return table.concat(list, ", ")
end

local function BuildActiveLookups()
    local activeByItemID = {}
    local activeByDecorID = {}

    local activeSource = Gallery.ActiveItems or (DVD and DVD.ActiveItems) or (dv and (dv.ActiveItems or dv.decorItem)) or {}

    for itemID, itemData in pairs(activeSource) do
        if type(itemID) == "number" and type(itemData) == "table" then
            activeByItemID[itemID] = true

            if itemData.decorID then
                activeByDecorID[itemData.decorID] = true
            end
        end
    end

    return activeByItemID, activeByDecorID
end

local function ItemExistsInActiveData(item, activeByItemID, activeByDecorID)
    if not item then return false end

    local data = item.data or {}
    local itemID = item.itemID or data.itemID
    local decorID = item.decorID or data.decorID

    if itemID and activeByItemID[itemID] then
        return true
    end

    if decorID and activeByDecorID[decorID] then
        return true
    end

    return false
end

local function PrintGalleryDebug()
    local catalogRecordCount = Gallery.catalogRecords and #Gallery.catalogRecords or 0
    local galleryCount = Gallery.items and #Gallery.items or 0

    local activeByItemID, activeByDecorID = BuildActiveLookups()

    local blizzardCatalogItems = 0
    local missingFromActiveItems = 0
    local activeOnlyItems = 0
    local catalogSourceItems = 0
    local sourceCounts = {}

    for _, item in ipairs(Gallery.items or {}) do
        local data = item.data or {}

        if data.catalogRecord then
            blizzardCatalogItems = blizzardCatalogItems + 1

            if not ItemExistsInActiveData(item, activeByItemID, activeByDecorID) then
                missingFromActiveItems = missingFromActiveItems + 1
            end
        else
            activeOnlyItems = activeOnlyItems + 1
        end

        if item.sources then
            for sourceType in pairs(item.sources) do
                sourceCounts[sourceType] = (sourceCounts[sourceType] or 0) + 1

                if sourceType == "catalog" then
                    catalogSourceItems = catalogSourceItems + 1
                end
            end
        end
    end

    DVGPrint("|cffffd100Decor Vendor Gallery Debug|r")
    DVGPrint("Catalog records loaded: " .. tostring(catalogRecordCount))
    DVGPrint("Gallery items total: " .. tostring(galleryCount))
    DVGPrint("Items from Blizzard catalog: " .. tostring(blizzardCatalogItems))
    DVGPrint("Catalog source items: " .. tostring(catalogSourceItems))
    DVGPrint("Missing from ActiveItems: " .. tostring(missingFromActiveItems))
    DVGPrint("ActiveItems/manual-only items: " .. tostring(activeOnlyItems))

    DVGPrint("|cffffd100Source counts:|r")

    local order = Gallery.C and Gallery.C.SOURCE_ORDER or {
	"vendor",
	"shop",
	"quest",
	"achievement",
	"drop",
	"oldevent",
	"promo",
	"profession",
	"catalog",
	"unreleased",
	"other",
    }

    for _, sourceType in ipairs(order) do
        if sourceCounts[sourceType] then
            DVGPrint(sourceType .. ": " .. tostring(sourceCounts[sourceType]))
        end
    end
end

local function DumpMissingGalleryItems()
    DecorVendorGalleryDump = DecorVendorGalleryDump or {}
    DecorVendorGalleryDump.catalogOnly = {}
    DecorVendorGalleryDump.manualOnly = {}
    DecorVendorGalleryDump.createdAt = date("%Y-%m-%d %H:%M:%S")

    local activeByItemID, activeByDecorID = BuildActiveLookups()

    for _, item in ipairs(Gallery.items or {}) do
        local data = item.data or {}

        if data.catalogRecord then
            if not ItemExistsInActiveData(item, activeByItemID, activeByDecorID) then
                table.insert(DecorVendorGalleryDump.catalogOnly, {
                    name = item.name or data.name or "Unknown",
                    itemID = item.itemID or data.itemID,
                    decorID = item.decorID or data.decorID,
                    model3D = data.model3D or data.asset,
                    uiModelSceneID = data.uiModelSceneID,
                    sourceText = data.sourceText,
                    sources = GetSourceList(item),
                })
            end
        else
            table.insert(DecorVendorGalleryDump.manualOnly, {
                name = item.name or data.name or "Unknown",
                itemID = item.itemID or data.itemID,
                decorID = item.decorID or data.decorID,
                model3D = data.model3D or data.asset,
                source = data.source or data.sourceType or data.category,
                sources = GetSourceList(item),
                note = data.note,
                noxp = data.noxp,
            })
        end
    end

    table.sort(DecorVendorGalleryDump.catalogOnly, function(a, b)
        return tostring(a.name) < tostring(b.name)
    end)

    table.sort(DecorVendorGalleryDump.manualOnly, function(a, b)
        return tostring(a.name) < tostring(b.name)
    end)

    DVGPrint("|cffffd100Decor Vendor Gallery Dump Created|r")
    DVGPrint("Catalog-only / missing from ActiveItems: " .. tostring(#DecorVendorGalleryDump.catalogOnly))
    DVGPrint("Manual/ActiveItems-only items: " .. tostring(#DecorVendorGalleryDump.manualOnly))
    DVGPrint("|cff88ff88Run /reload to save it to WTF SavedVariables.|r")

    DVGPrint("|cffffd100First few catalog-only items:|r")
    for i = 1, math.min(10, #DecorVendorGalleryDump.catalogOnly) do
        local dumpedItem = DecorVendorGalleryDump.catalogOnly[i]
        DVGPrint(i .. ". " .. tostring(dumpedItem.name) .. " itemID: " .. tostring(dumpedItem.itemID) .. " decorID: " .. tostring(dumpedItem.decorID))
    end

    DVGPrint("|cffffd100First few manual-only items:|r")
    for i = 1, math.min(10, #DecorVendorGalleryDump.manualOnly) do
        local dumpedItem = DecorVendorGalleryDump.manualOnly[i]
        DVGPrint(i .. ". " .. tostring(dumpedItem.name) .. " itemID: " .. tostring(dumpedItem.itemID) .. " decorID: " .. tostring(dumpedItem.decorID) .. " source: " .. tostring(dumpedItem.source))
    end
end

local function LuaString(value)
    if Gallery.LuaString then
        return Gallery.LuaString(value)
    end

    value = tostring(value or "")
    value = value:gsub("\\", "\\\\")
    value = value:gsub('"', '\\"')
    return '"' .. value .. '"'
end

local function FormatGalleryItemLine(item, sourceOverride)
    local data = item.data or {}

    local itemID = item.itemID or data.itemID
    local decorID = item.decorID or data.decorID
    local name = item.name or data.name
    local model3D = data.model3D or data.asset
    local source = sourceOverride or data.source or data.sourceType or data.category

    if not itemID or not decorID then
        return nil
    end

    local parts = {}

    table.insert(parts, "decorID = " .. tostring(decorID))

    if model3D then
        table.insert(parts, "model3D = " .. tostring(model3D))
    end

    if name and name ~= "" then
        table.insert(parts, "name = " .. LuaString(name))
    end

    if source and source ~= "" then
        table.insert(parts, "source = " .. LuaString(source))
    end

    if data.noxp then
        table.insert(parts, "noxp = true")
    end

    if data.note and data.note ~= "" then
        table.insert(parts, "note = " .. LuaString(data.note))
    end

    return string.format("[%d] = { %s },", itemID, table.concat(parts, ", "))
end

local function DumpFormattedMissingGalleryItems()
    DecorVendorGalleryDump = DecorVendorGalleryDump or {}
    DecorVendorGalleryDump.formatted = {
        catalogOnly = {},
        manualOnly = {},
        createdAt = date("%Y-%m-%d %H:%M:%S"),
    }

    local activeByItemID, activeByDecorID = BuildActiveLookups()

    for _, item in ipairs(Gallery.items or {}) do
        local data = item.data or {}

        if data.catalogRecord then
            if not ItemExistsInActiveData(item, activeByItemID, activeByDecorID) then
                local line = FormatGalleryItemLine(item, "catalog")

                if line then
                    table.insert(DecorVendorGalleryDump.formatted.catalogOnly, line)
                end
            end
        else
            local line = FormatGalleryItemLine(item)

            if line then
                table.insert(DecorVendorGalleryDump.formatted.manualOnly, line)
            end
        end
    end

    table.sort(DecorVendorGalleryDump.formatted.catalogOnly)
    table.sort(DecorVendorGalleryDump.formatted.manualOnly)

    DVGPrint("|cffffd100Decor Vendor Gallery formatted dump created.|r")
    DVGPrint("Catalog-only formatted lines: " .. tostring(#DecorVendorGalleryDump.formatted.catalogOnly))
    DVGPrint("Manual-only formatted lines: " .. tostring(#DecorVendorGalleryDump.formatted.manualOnly))
    DVGPrint("|cff88ff88Now /reload, then copy from DecorVendorGallery.lua SavedVariables.|r")
end

local function DVGTrace_CleanText(text)
    if type(text) ~= "string" then
        return nil
    end

    text = text:gsub("|n", "\n")
    text = text:gsub("\r", "\n")
    text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
    text = text:gsub("|r", "")

    return text
end

local function DVGTrace_GetLineValue(text, label)
    text = DVGTrace_CleanText(text)

    if not text then
        return nil
    end

    for line in text:gmatch("[^\n]+") do
        local value = line:match("^%s*" .. label .. ":%s*(.-)%s*$")

        if value then
            value = value:gsub("^%s+", ""):gsub("%s+$", "")

            if value ~= "" then
                return value
            end
        end
    end

    return nil
end

local function DVGTrace_FindItem(query)
    query = query or ""

    local wantedID = tonumber(query)
    local wantedText = string.lower(query)

    for _, item in ipairs(Gallery.items or {}) do
        local data = item.data or {}

        if wantedID and (
            tonumber(item.itemID) == wantedID
            or tonumber(data.itemID) == wantedID
            or tonumber(item.decorID) == wantedID
            or tonumber(data.decorID) == wantedID
        ) then
            return item
        end

        local name = string.lower(tostring(item.name or data.name or ""))
        if wantedText ~= "" and name:find(wantedText, 1, true) then
            return item
        end
    end

    return nil
end

local function DVGTrace_SourceOfQuest(query)
    if not Gallery.items or #Gallery.items == 0 then
        if Gallery.BuildItemList then
            Gallery.BuildItemList()
        end
    end

    local item = DVGTrace_FindItem(query)

    if not item then
        DVGPrint("|cffff4040DVG Trace:|r Could not find item: " .. tostring(query))
        return
    end

    local data = item.data or {}
    local itemID = item.itemID or data.itemID
    local decorID = item.decorID or data.decorID

    local itemDetails =
        Gallery.ItemDetails
        and (
            Gallery.ItemDetails[itemID]
            or Gallery.ItemDetails[tonumber(itemID)]
            or Gallery.ItemDetails[tostring(itemID)]
        )
        or {}

    local rawText =
        data.sourceText
        or data.catalogSourceText
        or data.tooltipText

    if not rawText and Gallery.GetCatalogInfo and decorID then
        local info = Gallery.GetCatalogInfo(decorID)

        if info then
            rawText =
                info.sourceText
                or info.catalogSourceText
                or info.tooltipText
        end
    end

    local catalogQuest = DVGTrace_GetLineValue(rawText, "Quest")

    DVGPrint("|cffffd100DVG Quest Trace|r")
    DVGPrint("Name: " .. tostring(item.name or data.name))
    DVGPrint("itemID: " .. tostring(itemID) .. "  decorID: " .. tostring(decorID))
    DVGPrint("itemDetails.questName: " .. tostring(itemDetails.questName))
    DVGPrint("data.questName: " .. tostring(data.questName))
    DVGPrint("catalog Quest line: " .. tostring(catalogQuest))

    if catalogQuest then
        DVGPrint("|cff00ff00Result:|r Quest text is coming from Housing Catalog sourceText.")
    elseif itemDetails.questName then
        DVGPrint("|cff00ff00Result:|r Quest text is coming from Gallery.ItemDetails.")
    elseif data.questName then
        DVGPrint("|cff00ff00Result:|r Quest text is coming from item data / ActiveItems.")
    else
        DVGPrint("|cffffcc00Result:|r No quest source found by this trace.")
    end
end

-- ============================================================
-- Slash Commands
-- ============================================================

SLASH_DECORVENDORGALLERY1 = "/dvgallery"
SLASH_DECORVENDORGALLERY2 = "/dvg"

SlashCmdList["DECORVENDORGALLERY"] = function(msg)
    msg = string.lower(msg or "")
    msg = msg:gsub("^%s+", ""):gsub("%s+$", "")

    if msg == "debug" then
        PrintGalleryDebug()
        return
    end

    if msg == "reloadcatalog" or msg == "reload catalog" then
        if Gallery.ClearCatalogRecords then
            Gallery.ClearCatalogRecords()
        end

        if Gallery.LoadCatalogRecords then
            Gallery.LoadCatalogRecords(function()
                Gallery.BuildItemList()

                if Gallery.RefreshGrid then
                    Gallery.RefreshGrid()
                end

                PrintGalleryDebug()
            end)
        else
            DVGPrint("|cffff4040Decor Vendor Gallery:|r Catalog loader is not loaded.")
        end

        return
    end

    if msg == "dumpmissing" or msg == "dump missing" then
        if not Gallery.items or #Gallery.items == 0 then
            DVGPrint("|cffffd100Decor Vendor Gallery:|r Open the gallery first with /dvg so items can build.")
            return
        end

        DumpMissingGalleryItems()
        return
    end

    if msg == "dumpformatted" or msg == "dump formatted" then
        if not Gallery.items or #Gallery.items == 0 then
            DVGPrint("|cffffd100Decor Vendor Gallery:|r Open the gallery first with /dvg so items can build.")
            return
        end

        DumpFormattedMissingGalleryItems()
        return
    end

if msg:match("^tracequest%s+") then
    local query = msg:match("^tracequest%s+(.+)$")
    DVGTrace_SourceOfQuest(query)
    return
end
    if Gallery.Toggle then
        Gallery.Toggle()
    elseif Gallery.Open then
        Gallery.Open()
    elseif dv and dv.ToggleGallery then
        dv.ToggleGallery()
    elseif Gallery.frame then
        Gallery.frame:SetShown(not Gallery.frame:IsShown())
    else
        DVGPrint("|cffff4040Decor Vendor Gallery is not loaded.|r")
    end
end

-- ============================================================
-- Catalog Detail Dumper
-- Merged from old DebugCatalogDump.lua
-- ============================================================

-- ============================================================
-- Basic helpers
-- ============================================================

local function DVGDump_Print(msg)
    DEFAULT_CHAT_FRAME:AddMessage(tostring(msg))
end

local function DVGDump_GetDumpTable()
    DecorVendorGalleryDump = DecorVendorGalleryDump or {}
    return DecorVendorGalleryDump
end

local function DVGDump_LuaString(value)
    if value == nil then
        return "nil"
    end

    value = tostring(value)
    value = value:gsub("\\", "\\\\")
    value = value:gsub('"', '\\"')
    value = value:gsub("\r", "\\n")
    value = value:gsub("\n", "\\n")

    return '"' .. value .. '"'
end

local function DVGDump_LuaValue(value)
    if type(value) == "number" then
        return tostring(value)
    end

    if type(value) == "boolean" then
        return value and "true" or "false"
    end

    return DVGDump_LuaString(value)
end

local function DVGDump_CleanCatalogText(text)
    if type(text) ~= "string" or text == "" then
        return nil
    end

    text = text:gsub("|n", "\n")
    text = text:gsub("\r", "\n")

    -- Remove color codes but keep texture/hyperlink/icon strings.
    text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
    text = text:gsub("|r", "")

    return text
end

local function DVGDump_CleanLine(value)
    if type(value) ~= "string" then
        return nil
    end

    value = value:gsub("^%s+", ""):gsub("%s+$", "")

    if value == "" then
        return nil
    end

    return value
end

local function DVGDump_NormalizeName(name)
    if type(name) ~= "string" then
        return nil
    end

    name = name:lower()
    name = name:gsub("|c%x%x%x%x%x%x%x%x", "")
    name = name:gsub("|r", "")
    name = name:gsub("[“”]", '"')
    name = name:gsub("%s+", " ")
    name = name:gsub("^%s+", ""):gsub("%s+$", "")

    return name
end

local function DVGDump_GetLineValue(text, label)
    if not text then
        return nil
    end

    for line in text:gmatch("[^\n]+") do
        local value = line:match("^%s*" .. label .. ":%s*(.-)%s*$")
        value = DVGDump_CleanLine(value)

        if value then
            return value
        end
    end

    return nil
end

local function DVGDump_GetCatalogTextForItem(item)
    if not item then
        return nil
    end

    local data = item.data or {}

    local text =
        data.sourceText
        or data.catalogSourceText
        or data.tooltipText

    if text then
        return text
    end

    local decorID = data.decorID or item.decorID

    if Gallery.GetCatalogInfo and decorID then
        local info = Gallery.GetCatalogInfo(decorID)

        if info then
            return info.sourceText
                or info.catalogSourceText
                or info.tooltipText
        end
    end

    return nil
end

local function DVGDump_ItemHasSource(item, sourceKey)
    if not item or not sourceKey then
        return false
    end

    local data = item.data or {}

    local function normalize(source)
        if Gallery.NormalizeSource then
            return Gallery.NormalizeSource(source)
        end

        return source
    end

    if normalize(data.source) == sourceKey or normalize(data.sourceType) == sourceKey then
        return true
    end

    if type(data.sources) == "table" then
        for _, source in ipairs(data.sources) do
            if normalize(source) == sourceKey then
                return true
            end
        end
    end

    if type(item.sources) == "table" then
        for source in pairs(item.sources) do
            if normalize(source) == sourceKey then
                return true
            end
        end
    end

    return false
end

-- ============================================================
-- Vendor matching
-- ============================================================

local function DVGDump_FindVendorIDByNameAndZone(vendorName, zone)
    if not vendorName or not dv or not dv.npcs then
        return nil
    end

    local wantedName = DVGDump_NormalizeName(vendorName)
    local wantedZone = DVGDump_NormalizeName(zone)

    local fallbackVendorID

    for _, group in ipairs(dv.npcs or {}) do
        for _, vendor in ipairs(group.vendors or {}) do
            local title = vendor.title or vendor.name
            local vendorNameNorm = DVGDump_NormalizeName(title)

            if vendorNameNorm == wantedName then
                if not fallbackVendorID then
                    fallbackVendorID = vendor.id
                end

                local vendorZoneNorm = DVGDump_NormalizeName(vendor.zone)

                if wantedZone and vendorZoneNorm and vendorZoneNorm == wantedZone then
                    return vendor.id
                end
            end
        end
    end

    return fallbackVendorID
end

local function DVGDump_ParseRequirementFragment(kind, value)
    value = DVGDump_CleanLine(value)

    if not value then
        return nil
    end

    if kind == "renown" then
        local faction, rank = value:match("^(.-)%s+[Rr]ank%s+(%d+)$")

        if faction and rank then
            return "{ type = \"renown\", faction = " ..
                DVGDump_LuaString(faction) ..
                ", rank = " ..
                tostring(tonumber(rank) or rank) ..
                " }"
        end

        return "{ type = \"renown\", faction = " .. DVGDump_LuaString(value) .. " }"
    end

    if kind == "faction" or kind == "reputation" then
        local faction, standing = value:match("^(.-)%s+%-%s+(.+)$")

        if faction and standing then
            return "{ type = \"reputation\", faction = " ..
                DVGDump_LuaString(faction) ..
                ", rank = " ..
                DVGDump_LuaString(standing) ..
                " }"
        end

        return "{ type = \"reputation\", faction = " .. DVGDump_LuaString(value) .. " }"
    end

    return nil
end

local function DVGDump_LooksLikeCostLine(line)
    if type(line) ~= "string" then
        return false
    end

    -- Some catalog records put the currency line under the vendor without "Cost:".
    if line:match("^%s*%d+|Hcurrency:") then
        return true
    end

    if line:match("^%s*%d+|T") then
        return true
    end

    return false
end

local function DVGDump_ExtractVendorBlocks(text)
    local vendors = {}

    if not text then
        return vendors
    end

    local current

    local function pushCurrent()
        if current then
            if current.zoneList and #current.zoneList > 0 then
                current.displayZone = table.concat(current.zoneList, " / ")
                current.zone = current.zone or current.zoneList[1]
            end

            table.insert(vendors, current)
            current = nil
        end
    end

    for line in text:gmatch("[^\n]+") do
        local vendorName = DVGDump_CleanLine(line:match("^%s*Vendor:%s*(.-)%s*$"))
        local zone = DVGDump_CleanLine(line:match("^%s*Zone:%s*(.-)%s*$"))
        local cost = DVGDump_CleanLine(line:match("^%s*Cost:%s*(.-)%s*$"))
        local faction = DVGDump_CleanLine(line:match("^%s*Faction:%s*(.-)%s*$"))
        local renown = DVGDump_CleanLine(line:match("^%s*Renown:%s*(.-)%s*$"))

        if vendorName then
            pushCurrent()

            current = {
                name = vendorName,
                zoneList = {},
            }

        elseif zone and current then
            if not current.zone then
                current.zone = zone
            end

            table.insert(current.zoneList, zone)

        elseif cost and current then
            current.cost = cost

        elseif faction and current then
            current.factionRequirement = faction

        elseif renown and current then
            current.renownRequirement = renown

        elseif current and DVGDump_LooksLikeCostLine(line) then
            current.cost = DVGDump_CleanLine(line)
        end
    end

    pushCurrent()

    return vendors
end

-- ============================================================
-- Price / currency parsing for generated detail files
-- ============================================================

local DVG_GOLD_ICON = "|T" .. ((Gallery.C and Gallery.C.GOLD_ICON) or "Interface\\MoneyFrame\\UI-GoldIcon") .. ":14:14:0:0|t"

local function DVGDump_NormalizePriceText(priceText)
    if type(priceText) ~= "string" or priceText == "" then
        return nil
    end

    local hadGoldIcon =
        priceText:find("MONEYFRAME")
        or priceText:find("MoneyFrame")
        or priceText:find("UI%-GoldIcon")
        or priceText:find("UI_GoldIcon")

    priceText = priceText:gsub("|c%x%x%x%x%x%x%x%x", "")
    priceText = priceText:gsub("|r", "")

    -- Remove currency hyperlink wrappers but keep texture icons.
    priceText = priceText:gsub("|Hcurrency:%d+|h", "")
    priceText = priceText:gsub("|h", "")

    -- Fix invisible catalog texture sizes.
    priceText = priceText:gsub("(|T[^|]-):0:0:0:0(|t)", "%1:14:14:0:0%2")
    priceText = priceText:gsub("(|T[^|]-):0:0(|t)", "%1:14:14:0:0%2")
    priceText = priceText:gsub("(|T[^|]-):0(|t)", "%1:14:14:0:0%2")

    -- If texture has no size at all.
    priceText = priceText:gsub("^|T([^|:]+)|t$", "|T%1:14:14:0:0|t")

    -- Add spacing between number and icon.
    priceText = priceText:gsub("(%d+)(|T)", "%1 %2")

    priceText = priceText:gsub("^%s+", ""):gsub("%s+$", "")

    -- If it had a gold icon but cleanup left only the number, restore gold icon.
    local amountOnly = priceText:match("^(%d+)$")
    if amountOnly and hadGoldIcon then
        priceText = amountOnly .. " " .. DVG_GOLD_ICON
    end

    if priceText == "" then
        return nil
    end

    return priceText
end

local function DVGDump_CountCurrencyLinks(priceText)
    local count = 0

    if type(priceText) ~= "string" then
        return count
    end

    for _ in priceText:gmatch("|Hcurrency:%d+|h") do
        count = count + 1
    end

    return count
end

local function DVGDump_BuildPriceField(priceText)
    if type(priceText) ~= "string" or priceText == "" then
        return nil
    end

    priceText = priceText:gsub("^%s+", ""):gsub("%s+$", "")

    -- Single currency cost, best clean format:
    -- 250|Hcurrency:3316|h|TInterface\ICONS\Something.blp:0|t|h
    local amount, currencyID = priceText:match("^%s*(%d+)%s*|Hcurrency:(%d+)|h")

    if amount and currencyID and DVGDump_CountCurrencyLinks(priceText) == 1 then
        return "price = { currencyID = " .. tostring(currencyID) .. ", amount = " .. tostring(amount) .. " }"
    end

    -- Plain number, including annoying trailing spaces from the dump.
    -- Treat plain catalog numbers as gold.
    local plainAmount = priceText:match("^%s*(%d+)%s*$")

    if plainAmount then
        return "priceText = " .. DVGDump_LuaString(
            plainAmount .. " |TInterface\\MoneyFrame\\UI-GoldIcon:14:14:0:0|t"
        )
    end

    -- Gold icon cost.
    if priceText:find("MONEYFRAME")
        or priceText:find("MoneyFrame")
        or priceText:find("UI%-GoldIcon")
        or priceText:find("UI_GoldIcon")
    then
        local normalized = DVGDump_NormalizePriceText(priceText)

        if normalized then
            local normalizedPlainAmount = normalized:match("^%s*(%d+)%s*$")

            if normalizedPlainAmount then
                normalized = normalizedPlainAmount .. " " .. DVG_GOLD_ICON
            end

            return "priceText = " .. DVGDump_LuaString(normalized)
        end
    end

    -- Mixed currency, multi-currency, or weird catalog strings.
    local normalized = DVGDump_NormalizePriceText(priceText)

    if normalized then
        local normalizedPlainAmount = normalized:match("^%s*(%d+)%s*$")

        if normalizedPlainAmount then
            normalized = normalizedPlainAmount .. " " .. DVG_GOLD_ICON
        end

        return "priceText = " .. DVGDump_LuaString(normalized)
    end

    return nil
end
-- ============================================================
-- Quest / Achievement matching from main addon data
-- ============================================================

local function DVGDump_FindQuestForItemOrName(itemID, questName)
    if not dv or not dv.quests then
        return nil
    end

    local wantedQuestName = DVGDump_NormalizeName(questName)
    local visited = {}

    local function scan(value)
        if type(value) ~= "table" then return nil end
        if visited[value] then return nil end
        visited[value] = true

        if value.itemID == itemID then
            return value
        end

        local rowQuestName = value.questName or value.name or value.title

        if wantedQuestName and DVGDump_NormalizeName(rowQuestName) == wantedQuestName then
            return value
        end

        for _, child in pairs(value) do
            local found = scan(child)
            if found then
                return found
            end
        end

        return nil
    end

    return scan(dv.quests)
end

local function DVGDump_FindAchievementForItemOrName(itemID, achievementName)
    if not dv or not dv.achievements then
        return nil
    end

    local wantedAchievementName = DVGDump_NormalizeName(achievementName)
    local visited = {}

    local function scan(value)
        if type(value) ~= "table" then return nil end
        if visited[value] then return nil end
        visited[value] = true

        if value.itemID == itemID then
            return value
        end

        local rowName = value.achievementName or value.name or value.title

        if wantedAchievementName and DVGDump_NormalizeName(rowName) == wantedAchievementName then
            return value
        end

        for _, child in pairs(value) do
            local found = scan(child)
            if found then
                return found
            end
        end

        return nil
    end

    return scan(dv.achievements)
end

-- ============================================================
-- Lua line builders
-- ============================================================

local function DVGDump_BuildGalleryItemDetailLine(item, parsed)
    local itemID = item and item.itemID
    if not itemID then return nil end

    local fields = {}

    -- Only put item-level price here if there are no vendor blocks.
    -- Vendor prices belong in GalleryVendorDetails.lua.
    if parsed.priceText and not parsed.hasVendorBlocks then
        local priceField = DVGDump_BuildPriceField and DVGDump_BuildPriceField(parsed.priceText)

        if priceField then
            table.insert(fields, priceField)
        end
    end

    -- Normal quest fields.
    if parsed.questName then
        local quest = DVGDump_FindQuestForItemOrName(itemID, parsed.questName)
        local questID = quest and quest.id

        if questID then
            table.insert(fields, "questID = " .. tostring(questID))
        end

        table.insert(fields, "questName = " .. DVGDump_LuaString(parsed.questName))
    end

    -- Normal achievement fields.
    if parsed.achievementName then
        local achievement = DVGDump_FindAchievementForItemOrName(itemID, parsed.achievementName)
        local achievementID = achievement and achievement.id

        if achievementID then
            table.insert(fields, "achievementID = " .. tostring(achievementID))
        end

        table.insert(fields, "achievementName = " .. DVGDump_LuaString(parsed.achievementName))

        if parsed.category then
            table.insert(fields, "achievementCategory = " .. DVGDump_LuaString(parsed.category))
        end
    elseif parsed.category then
        table.insert(fields, "category = " .. DVGDump_LuaString(parsed.category))
    end

    if parsed.professionText then
        table.insert(fields, "professionText = " .. DVGDump_LuaString(parsed.professionText))
    end

    if parsed.dropName then
        table.insert(fields, "dropName = " .. DVGDump_LuaString(parsed.dropName))
    end

    if parsed.treasureName then
        table.insert(fields, "treasureName = " .. DVGDump_LuaString(parsed.treasureName))
    end

    if #fields == 0 then
        return nil
    end

    -- Merge-safe assignment line.
    -- This can be pasted below your template without replacing the whole table.
    return "Gallery.ItemDetails[" .. tostring(itemID) .. "] = { " .. table.concat(fields, ", ") .. " }"
end

local function DVGDump_GetItemDetailSortInfo(item, parsed)
    local itemID = tonumber(item and item.itemID) or 999999999

    if parsed.questName then
        return {
            groupOrder = 10,
            groupTitle = "Quests",
            subTitle = "Quest: " .. tostring(parsed.questName),
            sortText = tostring(parsed.questName):lower(),
            itemID = itemID,
        }
    end

    if parsed.achievementName then
        local category = parsed.category or "Uncategorized"

        return {
            groupOrder = 20,
            groupTitle = "Achievements",
            subTitle = "Achievement Category: " .. tostring(category),
            sortText = tostring(category):lower() .. " " .. tostring(parsed.achievementName):lower(),
            itemID = itemID,
        }
    end

    if parsed.professionText then
        return {
            groupOrder = 30,
            groupTitle = "Professions",
            subTitle = "Profession: " .. tostring(parsed.professionText),
            sortText = tostring(parsed.professionText):lower(),
            itemID = itemID,
        }
    end

    if parsed.dropName then
        return {
            groupOrder = 40,
            groupTitle = "Drops",
            subTitle = "Drop: " .. tostring(parsed.dropName),
            sortText = tostring(parsed.dropName):lower(),
            itemID = itemID,
        }
    end

    if parsed.treasureName then
        return {
            groupOrder = 50,
            groupTitle = "Treasures",
            subTitle = "Treasure: " .. tostring(parsed.treasureName),
            sortText = tostring(parsed.treasureName):lower(),
            itemID = itemID,
        }
    end

    if parsed.priceText then
        return {
            groupOrder = 60,
            groupTitle = "Price Only",
            subTitle = "Price Only",
            sortText = "",
            itemID = itemID,
        }
    end

    if parsed.category then
        return {
            groupOrder = 70,
            groupTitle = "Categories",
            subTitle = "Category: " .. tostring(parsed.category),
            sortText = tostring(parsed.category):lower(),
            itemID = itemID,
        }
    end

    return {
        groupOrder = 999,
        groupTitle = "Other",
        subTitle = "Other",
        sortText = "",
        itemID = itemID,
    }
end

local function DVGDump_GetSoldByVendorIDs(item)
    local ids = {}
    local seen = {}

    if not item then
        return ids
    end

    local data = item.data or {}

    local soldBy =
        data.soldBy
        or item.soldBy

    if not soldBy and data.vendorID then
        soldBy = { data.vendorID }
    end

    if type(soldBy) == "number" then
        soldBy = { soldBy }
    end

    if type(soldBy) ~= "table" then
        return ids
    end

    for _, entry in ipairs(soldBy) do
        local vendorID

        if type(entry) == "number" then
            vendorID = entry
        elseif type(entry) == "table" then
            vendorID =
                entry.vendorID
                or entry.vendorId
                or entry.npcID
                or entry.npcId
                or entry.id
                or entry[1]
        end

        vendorID = tonumber(vendorID)

        if vendorID and not seen[vendorID] then
            seen[vendorID] = true
            table.insert(ids, vendorID)
        end
    end

    table.sort(ids)

    return ids
end

local function DVGDump_IsGenericVendorName(name)
    local normalized = DVGDump_NormalizeName(name)

    return normalized == "world vendors"
        or normalized == "world vendor"
        or normalized == "vendors"
        or normalized == "vendor"
end

local function DVGDump_BuildGalleryVendorDetailLines(item, vendorBlocks)
    local itemID = item and item.itemID

    if not itemID or not vendorBlocks or #vendorBlocks == 0 then
        return nil
    end

    local lines = {}

    table.insert(lines, "    [" .. tostring(itemID) .. "] = {")

    for _, vendorInfo in ipairs(vendorBlocks) do
        local fields = {}

        if vendorInfo.cost then
            local priceField = DVGDump_BuildPriceField(vendorInfo.cost)

            if priceField then
                table.insert(fields, priceField)
            end
        end

        if vendorInfo.displayZone or vendorInfo.zone then
            table.insert(fields, "displayZone = " .. DVGDump_LuaString(vendorInfo.displayZone or vendorInfo.zone))
        end

        local requirement =
            DVGDump_ParseRequirementFragment("renown", vendorInfo.renownRequirement)
            or DVGDump_ParseRequirementFragment("faction", vendorInfo.factionRequirement)

        if requirement then
            table.insert(fields, "requirement = " .. requirement)
        end

        local vendorIDs = {}

        -- Generic catalog labels like "World Vendors" are not real NPC names.
        -- Use ActiveItems soldBy instead.
        if DVGDump_IsGenericVendorName(vendorInfo.name) then
            vendorIDs = DVGDump_GetSoldByVendorIDs(item)
        else
            local vendorID = DVGDump_FindVendorIDByNameAndZone(vendorInfo.name, vendorInfo.zone)

            if vendorID then
                table.insert(vendorIDs, vendorID)
            else
                -- If name matching fails, still try ActiveItems soldBy as a backup.
                vendorIDs = DVGDump_GetSoldByVendorIDs(item)
            end
        end

        if #vendorIDs > 0 then
            for _, vendorID in ipairs(vendorIDs) do
                table.insert(lines, "        [" .. tostring(vendorID) .. "] = { " .. table.concat(fields, ", ") .. " },")
            end
        else
            table.insert(lines, "        -- Could not match vendorID for " .. tostring(vendorInfo.name or "Unknown Vendor"))
            table.insert(lines, "        -- [VENDOR_ID] = { " .. table.concat(fields, ", ") .. " },")
        end
    end

    table.insert(lines, "    },")

    return table.concat(lines, "\n")
end
-- ============================================================
-- Dump builder
-- ============================================================

local function DVGDump_BuildCatalogDetailDump()
    local dump = DVGDump_GetDumpTable()

    dump.createdAt = date("%Y-%m-%d %H:%M:%S")

    if Gallery.BuildItemList and (not Gallery.items or #Gallery.items == 0) then
        Gallery.BuildItemList()
    end

    local itemLines = {
        "-- ============================================================",
        "-- Generated Gallery.ItemDetails starter lines",
        "-- Sorted by source/type for easier editing",
        "-- Paste below your template table in Data/GalleryItemDetails.lua",
        "-- ============================================================",
        "Gallery.ItemDetails = Gallery.ItemDetails or {}",
        "",
    }

    local vendorLines = {
        "-- ============================================================",
        "-- Generated Gallery.VendorDetails starter lines",
        "-- Review before pasting into Data/GalleryVendorDetails.lua",
        "-- ============================================================",
        "Gallery.VendorDetails = Gallery.VendorDetails or {",
    }

    local rawLines = {
        "-- ============================================================",
        "-- Raw catalog source text",
        "-- ============================================================",
    }

    local itemCount = 0
    local vendorCount = 0
    local rawCount = 0

    local itemOutputBlocks = {}
    local vendorOutputBlocks = {}

    for _, item in ipairs(Gallery.items or {}) do
        local itemID = item.itemID
        local rawText = DVGDump_GetCatalogTextForItem(item)
        local text = DVGDump_CleanCatalogText(rawText)

        if itemID and text then
            rawCount = rawCount + 1

            table.insert(rawLines, "===== ITEM " .. tostring(itemID) .. " =====")
            table.insert(rawLines, text)
            table.insert(rawLines, "")

            local vendorBlocks = DVGDump_ExtractVendorBlocks(text)

            local parsed = {
                priceText = DVGDump_GetLineValue(text, "Cost"),
                questName = DVGDump_GetLineValue(text, "Quest"),
                achievementName = DVGDump_GetLineValue(text, "Achievement"),
                professionText = DVGDump_GetLineValue(text, "Profession"),
                dropName = DVGDump_GetLineValue(text, "Drop"),
                treasureName = DVGDump_GetLineValue(text, "Treasure"),
                category = DVGDump_GetLineValue(text, "Category"),
                hasVendorBlocks = vendorBlocks and #vendorBlocks > 0,
            }

            -- ====================================================
            -- Build sorted item-detail output
            -- ====================================================
            local itemLine = DVGDump_BuildGalleryItemDetailLine(item, parsed)

            if itemLine then
                local sortInfo = DVGDump_GetItemDetailSortInfo(item, parsed)

                table.insert(itemOutputBlocks, {
                    groupOrder = sortInfo.groupOrder,
                    groupTitle = sortInfo.groupTitle,
                    subTitle = sortInfo.subTitle,
                    sortText = sortInfo.sortText,
                    itemID = sortInfo.itemID,
                    text = itemLine,
                })

                itemCount = itemCount + 1
            end

            -- ====================================================
            -- Build sorted vendor-detail output
            -- ====================================================
            local vendorBlock = DVGDump_BuildGalleryVendorDetailLines(item, vendorBlocks)

            if vendorBlock then
                local firstVendorID

                if vendorBlocks and vendorBlocks[1] then
                    firstVendorID = DVGDump_FindVendorIDByNameAndZone(vendorBlocks[1].name, vendorBlocks[1].zone)
                end

                -- Backup for generic catalog names like "World Vendors".
                if not firstVendorID and DVGDump_GetSoldByVendorIDs then
                    local soldByIDs = DVGDump_GetSoldByVendorIDs(item)

                    if soldByIDs and soldByIDs[1] then
                        firstVendorID = soldByIDs[1]
                    end
                end

                table.insert(vendorOutputBlocks, {
                    vendorID = tonumber(firstVendorID) or 999999999,
                    itemID = tonumber(itemID) or 999999999,
                    text = vendorBlock,
                })

                vendorCount = vendorCount + 1
            end
        end
    end

    -- ========================================================
    -- Sort and output item detail lines
    -- ========================================================

    table.sort(itemOutputBlocks, function(a, b)
        if a.groupOrder ~= b.groupOrder then
            return a.groupOrder < b.groupOrder
        end

        if a.subTitle ~= b.subTitle then
            return tostring(a.subTitle) < tostring(b.subTitle)
        end

        if a.sortText ~= b.sortText then
            return tostring(a.sortText) < tostring(b.sortText)
        end

        return a.itemID < b.itemID
    end)

    local lastGroupTitle
    local lastSubTitle

    for _, block in ipairs(itemOutputBlocks) do
        if block.groupTitle ~= lastGroupTitle then
            table.insert(itemLines, "")
            table.insert(itemLines, "-- ============================================================")
            table.insert(itemLines, "-- " .. tostring(block.groupTitle))
            table.insert(itemLines, "-- ============================================================")

            lastGroupTitle = block.groupTitle
            lastSubTitle = nil
        end

        if block.subTitle ~= lastSubTitle then
            table.insert(itemLines, "")
            table.insert(itemLines, "-- " .. tostring(block.subTitle))
            lastSubTitle = block.subTitle
        end

        table.insert(itemLines, block.text)
    end

    -- ========================================================
    -- Sort and output vendor detail lines
    -- ========================================================

    table.sort(vendorOutputBlocks, function(a, b)
        if a.vendorID ~= b.vendorID then
            return a.vendorID < b.vendorID
        end

        return a.itemID < b.itemID
    end)

    for _, block in ipairs(vendorOutputBlocks) do
        table.insert(vendorLines, block.text)
    end

    -- Item lines are assignment-style now, so DO NOT close itemLines with "}".
    -- Vendor lines still use a wrapped table, so close vendorLines.
    table.insert(vendorLines, "}")

    -- Pretty SavedVariables: line tables are easier to read than one giant escaped string.
    dump.catalogItemDetailsLines = itemLines
    dump.catalogVendorDetailsLines = vendorLines
    dump.catalogRawSourceTextLines = rawLines

    -- Copy-box versions.
    dump.catalogItemDetailsText = table.concat(itemLines, "\n")
    dump.catalogVendorDetailsText = table.concat(vendorLines, "\n")
    dump.catalogRawSourceText = table.concat(rawLines, "\n")

    dump.counts = {
        itemDetailLines = itemCount,
        vendorDetailGroups = vendorCount,
        rawCatalogRecords = rawCount,
    }

    DVGDump_Print("|cff00ccffDecor Vendor Gallery Dump:|r Catalog detail dump complete.")
    DVGDump_Print("|cffaaaaaaItem detail lines:|r " .. tostring(itemCount))
    DVGDump_Print("|cffaaaaaaVendor detail groups:|r " .. tostring(vendorCount))
    DVGDump_Print("|cffaaaaaaRaw catalog records with text:|r " .. tostring(rawCount))
    DVGDump_Print("|cff88ff88Use /dvgdump show item or /dvgdump show vendor to copy clean Lua.|r")
    DVGDump_Print("|cff88ff88Then /reload if you want it saved to SavedVariables.|r")
end



-- ============================================================
-- Copy window
-- ============================================================

local copyFrame

local function DVGDump_ShowCopyWindow(title, text)
    if not copyFrame then
        copyFrame = CreateFrame("Frame", "DecorVendorGalleryDumpCopyFrame", UIParent, "BackdropTemplate")
        copyFrame:SetSize(760, 520)
        copyFrame:SetPoint("CENTER")
        copyFrame:SetFrameStrata("DIALOG")
        copyFrame:SetMovable(true)
        copyFrame:EnableMouse(true)
        copyFrame:RegisterForDrag("LeftButton")
        copyFrame:SetScript("OnDragStart", copyFrame.StartMoving)
        copyFrame:SetScript("OnDragStop", copyFrame.StopMovingOrSizing)

        copyFrame:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
            tile = true,
            tileSize = 32,
            edgeSize = 32,
            insets = { left = 11, right = 12, top = 12, bottom = 11 },
        })

        copyFrame.title = copyFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        copyFrame.title:SetPoint("TOP", 0, -18)
        copyFrame.title:SetText("Decor Vendor Gallery Dump")

        copyFrame.close = CreateFrame("Button", nil, copyFrame, "UIPanelCloseButton")
        copyFrame.close:SetPoint("TOPRIGHT", -6, -6)

        copyFrame.scroll = CreateFrame("ScrollFrame", nil, copyFrame, "UIPanelScrollFrameTemplate")
        copyFrame.scroll:SetPoint("TOPLEFT", 24, -52)
        copyFrame.scroll:SetPoint("BOTTOMRIGHT", -42, 52)

        copyFrame.editBox = CreateFrame("EditBox", nil, copyFrame.scroll)
        copyFrame.editBox:SetMultiLine(true)
        copyFrame.editBox:SetAutoFocus(true)
        copyFrame.editBox:SetFontObject(ChatFontNormal)
        copyFrame.editBox:SetWidth(670)
        copyFrame.editBox:SetTextInsets(6, 6, 6, 6)
        copyFrame.editBox:SetScript("OnEscapePressed", function(self)
            self:ClearFocus()
        end)

        copyFrame.scroll:SetScrollChild(copyFrame.editBox)

        copyFrame.copyHint = copyFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        copyFrame.copyHint:SetPoint("BOTTOMLEFT", 24, 26)
        copyFrame.copyHint:SetText("Ctrl+A, Ctrl+C to copy. Review generated lines before pasting into your data files.")
    end

    copyFrame.title:SetText(title or "Decor Vendor Gallery Dump")

    text = text or "-- Nothing to show yet. Run /dvgdump details first."

    copyFrame.editBox:SetText(text)
    copyFrame.editBox:SetCursorPosition(0)
    copyFrame.editBox:HighlightText()
    copyFrame:Show()
end

local function DVGDump_ShowSavedText(kind)
    local dump = DVGDump_GetDumpTable()

    if not dump.catalogItemDetailsText and not dump.catalogVendorDetailsText then
        DVGDump_Print("|cffffd100Decor Vendor Gallery Dump:|r No dump found. Running /dvgdump details now.")
        DVGDump_BuildCatalogDetailDump()
    end

    if kind == "item" then
        DVGDump_ShowCopyWindow("GalleryItemDetails.lua starter lines", dump.catalogItemDetailsText)
    elseif kind == "vendor" then
        DVGDump_ShowCopyWindow("GalleryVendorDetails.lua starter lines", dump.catalogVendorDetailsText)
    elseif kind == "raw" then
        DVGDump_ShowCopyWindow("Raw Catalog Source Text", dump.catalogRawSourceText)
    else
        DVGDump_Print("|cffffd100Usage:|r /dvgdump show item")
        DVGDump_Print("|cffffd100Usage:|r /dvgdump show vendor")
        DVGDump_Print("|cffffd100Usage:|r /dvgdump show raw")
    end
end

-- ============================================================
-- Slash command
-- ============================================================

local function DVGDump_PrintHelp()
    DVGDump_Print("|cffffd100Decor Vendor Gallery Dump Commands|r")
    DVGDump_Print("|cff88ff88/dvgdump details|r - Build catalog item/vendor detail dumps")
    DVGDump_Print("|cff88ff88/dvgdump show item|r - Open copy box for GalleryItemDetails lines")
    DVGDump_Print("|cff88ff88/dvgdump show vendor|r - Open copy box for GalleryVendorDetails lines")
    DVGDump_Print("|cff88ff88/dvgdump show raw|r - Open copy box for raw catalog text")
    DVGDump_Print("|cff88ff88/dvgdump clear|r - Clear saved dump data")
end


SLASH_DECORVENDORGALLERYDUMP1 = "/dvgdump"

SlashCmdList["DECORVENDORGALLERYDUMP"] = function(msg)
    msg = string.lower(msg or "")
    msg = msg:gsub("^%s+", ""):gsub("%s+$", "")

    if msg == "" or msg == "help" then
        DVGDump_PrintHelp()
        return
    end

    if msg == "details"
        or msg == "dump"
        or msg == "dumpdetails"
        or msg == "dump details"
        or msg == "catalog"
        or msg == "catalog details"
    then
        DVGDump_BuildCatalogDetailDump()
        return
    end

    if msg == "show item"
        or msg == "show items"
        or msg == "item"
        or msg == "items"
    then
        DVGDump_ShowSavedText("item")
        return
    end

    if msg == "show vendor"
        or msg == "show vendors"
        or msg == "vendor"
        or msg == "vendors"
    then
        DVGDump_ShowSavedText("vendor")
        return
    end

    if msg == "show raw"
        or msg == "raw"
    then
        DVGDump_ShowSavedText("raw")
        return
    end

    if msg == "clear" then
        DecorVendorGalleryDump = {}
        DVGDump_Print("|cff00ccffDecor Vendor Gallery Dump:|r Cleared dump data.")
        return
    end

    DVGDump_PrintHelp()
end

-- ============================================================
-- /dvgmissinglive
-- Opens a copy/paste box with Gallery items NOT in live Blizzard catalog
-- ============================================================

local function DVG_OpenCopyBox(titleText, bodyText)
    local frame = _G.DVG_MissingLiveCopyFrame

    if not frame then
        frame = CreateFrame("Frame", "DVG_MissingLiveCopyFrame", UIParent, "BackdropTemplate")
        frame:SetSize(760, 560)
        frame:SetPoint("CENTER")
        frame:SetFrameStrata("DIALOG")
        frame:SetFrameLevel(500)
        frame:SetMovable(true)
        frame:EnableMouse(true)
        frame:RegisterForDrag("LeftButton")
        frame:SetScript("OnDragStart", frame.StartMoving)
        frame:SetScript("OnDragStop", frame.StopMovingOrSizing)

        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame:SetBackdropColor(0.04, 0.04, 0.06, 0.98)
        frame:SetBackdropBorderColor(0.55, 0.42, 0.15, 1)

        frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        frame.title:SetPoint("TOPLEFT", 14, -12)
        frame.title:SetTextColor(1, 0.82, 0, 1)

        frame.close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
        frame.close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4)

        frame.copyBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
        frame.copyBtn:SetSize(130, 24)
        frame.copyBtn:SetPoint("TOPRIGHT", frame.close, "TOPLEFT", -8, -6)
        frame.copyBtn:SetText("Select All")

        local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -46)
        scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -32, 42)

        local edit = CreateFrame("EditBox", nil, scroll)
        edit:SetMultiLine(true)
        edit:SetAutoFocus(false)
        edit:SetFontObject(ChatFontNormal)
        edit:SetWidth(700)
        edit:SetTextColor(1, 1, 1, 1)
        edit:SetScript("OnEscapePressed", function(self)
            self:ClearFocus()
        end)

        scroll:SetScrollChild(edit)

        frame.edit = edit
        frame.scroll = scroll

        frame.copyBtn:SetScript("OnClick", function()
            frame.edit:SetFocus()
            frame.edit:HighlightText()
        end)

        frame.note = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        frame.note:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 14, 16)
        frame.note:SetTextColor(0.75, 0.75, 0.75, 1)
        frame.note:SetText("Ctrl+C to copy after selecting text.")
    end

    frame.title:SetText(titleText or "Decor Vendor Gallery Export")
    frame.edit:SetText(bodyText or "")
    frame.edit:SetCursorPosition(0)
    frame.edit:SetFocus()
    frame.edit:HighlightText()
    frame:Show()
end

SLASH_DVGMISSINGLIVE1 = "/dvgmissinglive"

SlashCmdList["DVGMISSINGLIVE"] = function(msg)
    local G = _G.DecorVendorGallery
    local D = _G.DecorVendorData

    if not G then
        print("|cffff4040DVG:|r DecorVendorGallery is not loaded.")
        return
    end

    if not D then
        print("|cffff4040DVG:|r DecorVendorData is not loaded.")
        return
    end

    if D.LoadCatalogRecords and not D.catalogLoaded then
        print("|cffffd100DVG:|r Loading live housing catalog first...")

        D.LoadCatalogRecords(function()
            if G.BuildItemList then
                G.BuildItemList()
            end

            SlashCmdList["DVGMISSINGLIVE"](msg)
        end)

        return
    end

    if G.BuildItemList and (not G.items or #G.items == 0) then
        G.BuildItemList()
    end

    local lines = {}
    local total = 0

    table.insert(lines, "Decor Vendor Gallery - Missing From Live Blizzard Catalog")
    table.insert(lines, "Generated from /dvgmissinglive")
    table.insert(lines, "")
    table.insert(lines, "Format: #. Name | itemID | decorID")
    table.insert(lines, "============================================================")
    table.insert(lines, "")

    for _, item in ipairs(G.items or {}) do
        local itemID = item.itemID
        local data = item.data or {}

        local decorID =
            item.decorID
            or item.decorIDValue
            or data.decorID

        if decorID and not (D.catalogByDecorID and D.catalogByDecorID[decorID]) then
            total = total + 1

            local name =
                (G.GetItemName and G.GetItemName(itemID, data))
                or data.name
                or item.name
                or ("Item " .. tostring(itemID))

            table.insert(
                lines,
                tostring(total)
                    .. ". "
                    .. tostring(name)
                    .. " | itemID: "
                    .. tostring(itemID)
                    .. " | decorID: "
                    .. tostring(decorID)
            )
        end
    end

    table.insert(lines, "")
    table.insert(lines, "============================================================")
    table.insert(lines, "Not in live catalog: " .. tostring(total))
    table.insert(lines, "Gallery item count: " .. tostring(#(G.items or {})))
    table.insert(lines, "Live catalog record count: " .. tostring(D.catalogRecords and #D.catalogRecords or 0))

    DVG_OpenCopyBox(
        "DVG Missing Live Catalog Items",
        table.concat(lines, "\n")
    )
end

-- ============================================================
-- /dvgnewcatalog
-- Finds Blizzard Housing Catalog decor that is NOT in ActiveItems
-- Useful for PTR/new patch checks
-- ============================================================

SLASH_DVGNEWCATALOG1 = "/dvgnewcatalog"
SLASH_DVGNEWCATALOG2 = "/dvgptrcatalog"
SLASH_DVGNEWCATALOG3 = "/dvcatalog"

SlashCmdList["DVGNEWCATALOG"] = function(msg)
    local G = _G.DecorVendorGallery or _G.DecorVendor_Gallery
    local D = _G.DecorVendorData or _G.DecorVendor_Data

    if not G then
        print("|cffff4040DVG:|r DecorVendorGallery is not loaded.")
        return
    end

    if not D then
        print("|cffff4040DVG:|r DecorVendorData is not loaded.")
        return
    end

    local function RunNewCatalogCheck()
        if G.BuildItemList and (not G.items or #G.items == 0) then
            G.BuildItemList()
        end

        local activeByItemID, activeByDecorID = BuildActiveLookups()

        local lines = {}
        local formattedLines = {}
        local total = 0

        table.insert(lines, "Decor Vendor Gallery - New Catalog Decor Check")
        table.insert(lines, "Generated from /dvgnewcatalog")
        table.insert(lines, "")
        table.insert(lines, "These are Blizzard Housing Catalog records that are NOT currently in ActiveItems.")
        table.insert(lines, "Useful for PTR/new patch checking.")
        table.insert(lines, "============================================================")
        table.insert(lines, "")

        for _, item in ipairs(G.items or {}) do
            local data = item.data or {}

            local itemID =
                item.itemID
                or data.itemID
                or data.id

            local decorID =
                item.decorID
                or item.decorIDValue
                or data.decorID

            local isCatalogItem =
                data.catalogRecord
                or data.isCatalogItem
                or (decorID and D.catalogByDecorID and D.catalogByDecorID[decorID])

            if isCatalogItem and not ItemExistsInActiveData(item, activeByItemID, activeByDecorID) then
                total = total + 1

                local name =
                    (G.GetItemName and G.GetItemName(itemID, data))
                    or item.name
                    or data.name
                    or data.itemName
                    or data.decorName
                    or ("Item " .. tostring(itemID))

                local model3D =
                    data.model3D
                    or data.asset
                    or data.model3DFileID
                    or data.uiModelSceneID

                table.insert(
                    lines,
                    tostring(total)
                        .. ". "
                        .. tostring(name)
                        .. " | itemID: "
                        .. tostring(itemID or "unknown")
                        .. " | decorID: "
                        .. tostring(decorID or "unknown")
                        .. " | model3D: "
                        .. tostring(model3D or "unknown")
                )

                if FormatGalleryItemLine then
                    local formatted = FormatGalleryItemLine(item, "catalog")

                    if formatted then
                        table.insert(formattedLines, formatted)
                    end
                elseif itemID and decorID then
                    table.insert(
                        formattedLines,
                        "[" .. tostring(itemID) .. "] = { decorID = " .. tostring(decorID)
                            .. (model3D and (", model3D = " .. tostring(model3D)) or "")
                            .. ', source = "catalog" },'
                    )
                end
            end
        end

        table.insert(lines, "")
        table.insert(lines, "============================================================")
        table.insert(lines, "New catalog decor missing from ActiveItems: " .. tostring(total))
        table.insert(lines, "Gallery item count: " .. tostring(#(G.items or {})))
        table.insert(lines, "Catalog record count: " .. tostring(D.catalogRecords and #D.catalogRecords or 0))

        if #formattedLines > 0 then
            table.insert(lines, "")
            table.insert(lines, "============================================================")
            table.insert(lines, "Starter ActiveItems lines")
            table.insert(lines, "Review these before pasting.")
            table.insert(lines, "============================================================")
            table.insert(lines, "")

            table.sort(formattedLines)

            for _, line in ipairs(formattedLines) do
                table.insert(lines, line)
            end
        end

        if total == 0 then
            print("|cff00ff66DVG:|r No new Housing Catalog decor missing from ActiveItems.")
            return
        end

        DVG_OpenCopyBox(
            "DVG New Catalog Decor",
            table.concat(lines, "\n")
        )
    end

    if D.LoadCatalogRecords and not D.catalogLoaded then
        print("|cffffd100DVG:|r Loading housing catalog first...")

        D.LoadCatalogRecords(function()
            if G.BuildItemList then
                G.BuildItemList()
            end

            RunNewCatalogCheck()
        end)

        return
    end

    RunNewCatalogCheck()
end

-- ============================================================
-- /dvgrecent
-- Shows ActiveItems/Gallery items not found in the loaded Blizzard catalog,
-- excluding "other", "catalog", and "unreleased" by default.
-- Useful for finding likely recently-added/PTR decor.
-- ============================================================

local function DVGRecent_LuaString(value)
    value = tostring(value or "")
    value = value:gsub("\\", "\\\\")
    value = value:gsub('"', '\\"')
    return '"' .. value .. '"'
end

local function DVGRecent_GetPrimarySource(item, data)
    data = data or {}

    local function Normalize(source)
        if Gallery.NormalizeSource then
            return Gallery.NormalizeSource(source)
        end

        if source then
            return string.lower(tostring(source))
        end

        return nil
    end

    local directSource =
        Normalize(data.source)
        or Normalize(data.sourceType)
        or Normalize(data.category)

    if directSource and directSource ~= "" then
        return directSource
    end

    if type(item.sources) == "table" then
        local order =
            Gallery.C
            and Gallery.C.SOURCE_ORDER
            or {
                "vendor",
                "shop",
                "quest",
                "achievement",
                "drop",
                "treasure",
                "boss",
                "promo",
                "profession",
                "catalog",
                "unreleased",
                "other",
            }

        for _, sourceType in ipairs(order) do
            if item.sources[sourceType] then
                return sourceType
            end
        end

        for sourceType in pairs(item.sources) do
            return tostring(sourceType)
        end
    end

    return "other"
end

local function DVGRecent_FormatActiveItemLine(item, data, source)
    data = data or {}

    local itemID =
        item.itemID
        or data.itemID
        or data.id

    local decorID =
        item.decorID
        or item.decorIDValue
        or data.decorID

    local model3D =
        data.model3D
        or data.asset
        or data.model3DFileID
        or data.uiModelSceneID

    local name =
        item.name
        or data.name
        or data.itemName
        or data.decorName

    if not itemID or not decorID then
        return nil
    end

    local parts = {}

    table.insert(parts, "decorID = " .. tostring(decorID))

    if model3D then
        table.insert(parts, "model3D = " .. tostring(model3D))
    end

    if source and source ~= "" then
        table.insert(parts, "source = " .. DVGRecent_LuaString(source))
    end

    table.insert(parts, "recentlyAdded = true")

    local line = "[" .. tostring(itemID) .. "] = { " .. table.concat(parts, ", ") .. " },"

    if name and name ~= "" then
        line = line .. " -- " .. tostring(name)
    end

    return line
end

SLASH_DVGRECENTADDED1 = "/dvgrecent"
SLASH_DVGRECENTADDED2 = "/dvgrecentcatalog"
SLASH_DVGRECENTADDED3 = "/dvgrecentlyadded"

SlashCmdList["DVGRECENTADDED"] = function(msg)
    msg = string.lower(msg or "")
    msg = msg:gsub("^%s+", ""):gsub("%s+$", "")

    local includeOther = msg == "all" or msg == "includeother" or msg == "include other"

    local G = _G.DecorVendorGallery or _G.DecorVendor_Gallery
    local D = _G.DecorVendorData or _G.DecorVendor_Data

    if not G then
        print("|cffff4040DVG:|r DecorVendorGallery is not loaded.")
        return
    end

    if not D then
        print("|cffff4040DVG:|r DecorVendorData is not loaded.")
        return
    end

    local function RunRecentCheck()
        if G.BuildItemList and (not G.items or #G.items == 0) then
            G.BuildItemList()
        end

        local results = {}
        local totalMissingCatalog = 0
        local skippedOther = 0

        for _, item in ipairs(G.items or {}) do
            local data = item.data or {}

            local itemID =
                item.itemID
                or data.itemID
                or data.id

            local decorID =
                item.decorID
                or item.decorIDValue
                or data.decorID

            decorID = tonumber(decorID)

            if decorID and not (D.catalogByDecorID and D.catalogByDecorID[decorID]) then
                totalMissingCatalog = totalMissingCatalog + 1

                local source = DVGRecent_GetPrimarySource(item, data)

                local skip =
                    not includeOther
                    and (
                        source == "other"
                        or source == "catalog"
                        or source == "unreleased"
                    )

                if skip then
                    skippedOther = skippedOther + 1
                else
                    local name =
                        (G.GetItemName and G.GetItemName(itemID, data))
                        or item.name
                        or data.name
                        or data.itemName
                        or data.decorName
                        or ("Item " .. tostring(itemID))

                    local model3D =
                        data.model3D
                        or data.asset
                        or data.model3DFileID
                        or data.uiModelSceneID

                    table.insert(results, {
                        name = name,
                        itemID = itemID,
                        decorID = decorID,
                        model3D = model3D,
                        source = source,
                        line = DVGRecent_FormatActiveItemLine(item, data, source),
                    })
                end
            end
        end

        table.sort(results, function(a, b)
            if tostring(a.source) ~= tostring(b.source) then
                return tostring(a.source) < tostring(b.source)
            end

            return tostring(a.name) < tostring(b.name)
        end)

        DecorVendorGalleryDump = DecorVendorGalleryDump or {}
        DecorVendorGalleryDump.recentlyAddedCandidates = results
        DecorVendorGalleryDump.recentlyAddedCreatedAt = date("%Y-%m-%d %H:%M:%S")

        local lines = {}

        table.insert(lines, "Decor Vendor Gallery - Recently Added Candidates")
        table.insert(lines, "Generated from /dvgrecent")
        table.insert(lines, "")
        table.insert(lines, "These are Gallery/ActiveItems items NOT found in the loaded Blizzard Housing Catalog.")
        table.insert(lines, "Default mode excludes source = other, catalog, and unreleased.")
        table.insert(lines, "Use /dvgrecent all to include everything.")
        table.insert(lines, "============================================================")
        table.insert(lines, "")
        table.insert(lines, "Recently Added candidates: " .. tostring(#results))
        table.insert(lines, "Total missing from catalog: " .. tostring(totalMissingCatalog))
        table.insert(lines, "Skipped other/catalog/unreleased: " .. tostring(skippedOther))
        table.insert(lines, "Gallery item count: " .. tostring(#(G.items or {})))
        table.insert(lines, "Catalog record count: " .. tostring(D.catalogRecords and #D.catalogRecords or 0))
        table.insert(lines, "")
        table.insert(lines, "============================================================")
        table.insert(lines, "List")
        table.insert(lines, "============================================================")
        table.insert(lines, "")

        for i, entry in ipairs(results) do
            table.insert(
                lines,
                tostring(i)
                    .. ". "
                    .. tostring(entry.name)
                    .. " | source: "
                    .. tostring(entry.source)
                    .. " | itemID: "
                    .. tostring(entry.itemID or "unknown")
                    .. " | decorID: "
                    .. tostring(entry.decorID or "unknown")
                    .. " | model3D: "
                    .. tostring(entry.model3D or "unknown")
            )
        end

        table.insert(lines, "")
        table.insert(lines, "============================================================")
        table.insert(lines, "Starter ActiveItems lines")
        table.insert(lines, "Review before pasting.")
        table.insert(lines, "============================================================")
        table.insert(lines, "")

        for _, entry in ipairs(results) do
            if entry.line then
                table.insert(lines, entry.line)
            end
        end

        if #results == 0 then
            print("|cff00ff66DVG:|r No Recently Added candidates found.")
            return
        end

        DVG_OpenCopyBox(
            "DVG Recently Added Candidates",
            table.concat(lines, "\n")
        )
    end

    if D.LoadCatalogRecords and not D.catalogLoaded then
        print("|cffffd100DVG:|r Loading housing catalog first...")

        D.LoadCatalogRecords(function()
            if G.BuildItemList then
                G.BuildItemList()
            end

            RunRecentCheck()
        end)

        return
    end

    RunRecentCheck()
end