--[[
============================================================================
ETB v10.0 - Commands.lua
============================================================================
All slash commands for the addon.
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB

-- =============================================================================
-- MAIN COMMAND
-- =============================================================================

SLASH_ETB1 = "/etb"
SLASH_ETB2 = "/transmog"

SlashCmdList["ETB"] = function(msg)
    local args = {}
    for word in msg:gmatch("%S+") do
        args[#args + 1] = word:lower()
    end

    local cmd = args[1] or ""

    -- v12.40.0: registry-table dispatcher (O(1) lookup). All commands are
    -- now in ETB.CommandRegistry (v12.51.0 removed the legacy if/elseif
    -- chain entirely). Gate enforces the user/debug split: user commands
    -- always dispatch; developer commands require /etb debug first.
    if ETB.CommandRegistry then
        -- If registry knows this command, enforce user/debug gate and dispatch.
        if ETB.CommandRegistry._byName[cmd] then
            if not ETB.CommandRegistry:IsUserCommand(cmd) and not ETB.debug then
                ETB:Print("Unknown command. Type |cffffd100/etb help|r for available commands.")
                ETB:Print("Developer commands require debug mode: |cff00ccff/etb debug|r")
                return
            end
            if ETB.CommandRegistry:Dispatch(cmd, args) then return end
        end
    end

    -- v12.51.0: legacy elseif chain fully removed. All commands now live
    -- in ETB.CommandRegistry. The dispatcher above handles known commands;
    -- this fallback runs only when the user types something unknown.
    --
    -- Migrated by version (cumulative):
    --   v12.40.2: show, hide, toggle, version, tui, transmogui, apply, erb
    --   v12.43.0: favorites/fav, minimap, macro, tooltips, debug, wishlist, farm
    --   v12.44.0: memory/mem, cache, tutorial/guide, sets, debugset, loadreport/lr, help
    --   v12.46.0: outfits, audit, reset, sourcetest, instance, api, purgedata,
    --             discover, bossloot/blt
    --   v12.49.0: stats, profiles/profile, switch, sources, sourcepanel/sp,
    --             diagnose/diag, customsets/csets, inspect
    --   v12.50.0: weapontest, categories/cats, shipinfo, armorcount/armorstats,
    --             filtertest, verify, classtest
    --   v12.51.0: rawapi, pipelineaudit, fixitems, deepfix, fixsets/scansets
    -- (50 commands total)
    ETB:Print("Unknown command. Type |cffffd100/etb help|r for available commands.")
end

-- =============================================================================
-- KEYBIND FUNCTION
-- =============================================================================

function ETB_Toggle()
    ETB.MainFrame:Toggle()
end

function ETB_ToggleTransmogUI()
    if ETB.TransmogUI then
        ETB.TransmogUI:Toggle()
    end
end

-- Macro creation helper: /etb macro
-- Creates a macro button that players can drag to their action bar
function ETB:CreateMacro()
    local macroName = "ETB"
    local macroIcon = "Interface\\Icons\\INV_Chest_Cloth_17"
    local macroBody = "/etb toggle"
    
    -- Check if macro already exists
    local existingIndex = GetMacroIndexByName(macroName)
    if existingIndex and existingIndex > 0 then
        EditMacro(existingIndex, macroName, macroIcon, macroBody)
        ETB:Print("|cff00ff00Macro updated:|r " .. macroName)
    else
        -- Check macro limits
        local numGlobal, numPerChar = GetNumMacros()
        if numGlobal < MAX_ACCOUNT_MACROS then
            CreateMacro(macroName, macroIcon, macroBody)
            ETB:Print("|cff00ff00Macro created:|r " .. macroName)
        else
            ETB:Print("|cffff6600Macro limit reached!|r Delete a macro first.")
            return
        end
    end
    ETB:Print("Open your macro window (|cffffd100/macro|r) and drag 'ETB' to your action bar!")
end

ETB:Debug("Commands", "Commands.lua loaded")

-- Version marker: if you see this in /etb debug output, the March 18 build is loaded

-- =============================================================================
-- REGISTRY-TABLE DISPATCH (v12.40.0)
-- =============================================================================
-- Simple, single-action commands migrated from the if/elseif chain above.
-- The dispatcher at the top of SlashCmdList["ETB"] checks this registry first,
-- short-circuits on hit, and falls through to the legacy chain on miss.
--
-- Migration is incremental: leave complex handlers (stats, audit, sources,
-- pipelineaudit, etc.) in the legacy chain until each can be individually
-- extracted and tested. This file will shrink as more commands migrate.
-- =============================================================================

if ETB.CommandRegistry then
    local CR = ETB.CommandRegistry

    CR:Register{
        name = "show", aliases = {"open", ""},
        handler = function() ETB.MainFrame:Show() end,
        help = "Open the main browser window",
        user = true,
    }

    CR:Register{
        name = "hide", aliases = {"close"},
        handler = function() ETB.MainFrame:Hide() end,
        help = "Close the browser window",
        user = true,
    }

    CR:Register{
        name = "toggle",
        handler = function() ETB.MainFrame:Toggle() end,
        help = "Toggle the browser window on/off",
        user = true,
    }

    CR:Register{
        name = "version", aliases = {"ver"},
        handler = function()
            ETB:Print("Build: v15.8.19 — SoO loot data corrected")
            ETB:Print("  |cffffd100Removed 999 Flex/Celestial items|r that were wrongly")
            ETB:Print("  tagged to SoO raid encounters. MoP Classic SoO has no Flex")
            ETB:Print("  and no LFR (per Blizzard blue post); Celestial-tier gear")
            ETB:Print("  comes from Celestial Dungeons, not the raid.")
            ETB:Print("  |cffffd100561 SoO items|r now correctly tagged Normal (1) or")
            ETB:Print("  Heroic (2) — 555 bitmask corrections + 6 new items.")
            ETB:Print("  Garrosh: 73 items (was inflated to 170 by the cruft).")
            ETB:Print("  Source: AtlasLootClassic v5.2.1. Verify: |cffffd100/etb encloot 869|r")
        end,
        help = "Print build version",
        user = true,
    }

    -- v12.42.0 validation: sample memory before and after forcing the Compact
    -- lazy-load, so the user can confirm deferral is actually working in-game.
    -- Safe to run at any time — if Compact data is already materialized (user
    -- already hovered a boss or opened the browser), the delta will be ~0 KB
    -- and the report will say so explicitly.
    CR:Register{
        name = "memtest",
        handler = function()
            if not ETB.SourceData_ATT then
                ETB:Print("|cffff6666SourceData_ATT not available — cannot run memtest.|r")
                return
            end

            -- Snapshot 1: current state. If the user hasn't triggered anything
            -- yet, Compact data is still deferred and memory is near the
            -- login baseline. If they have, memory is already elevated.
            UpdateAddOnMemoryUsage()
            local memBefore = GetAddOnMemoryUsage("EnhancedTransmogBrowser")

            -- Check whether any expansion loader is still present. If yes,
            -- at least one expansion is still deferred. If all are gone,
            -- everything's already loaded and we'll report zero delta.
            local compact = NS.Compact
            local deferredCount = 0
            local loaderNames = { "_ClassicLoader", "_TBCLoader", "_WrathLoader", "_CataLoader", "_MoPLoader" }
            if compact then
                for _, name in ipairs(loaderNames) do
                    if compact[name] then deferredCount = deferredCount + 1 end
                end
            end

            ETB:Print("|cff00ffff=== v12.42.0 Memory Deferral Test ===|r")
            ETB:Print(string.format("Current memory: |cffffd100%s|r", ETB:FormatMemory(memBefore)))
            ETB:Print(string.format("Deferred expansions: |cffffd100%d of 5|r", deferredCount))

            if deferredCount == 0 then
                ETB:Print("|cffffd700All Compact data is already loaded.|r")
                ETB:Print("If you used any ETB feature this session (hovered a boss, opened")
                ETB:Print("the browser, etc.), the lazy-load already fired — that's expected.")
                ETB:Print("For a clean measurement, |cffffd100/reload|r then run this command")
                ETB:Print("immediately as your first action. Expected delta: ~75-90 MB.")
                return
            end

            -- Force the lazy-load.
            ETB:Print("|cffffa500Forcing lazy-load via SDA:EnsureLoaded()...|r")
            ETB.SourceData_ATT:EnsureLoaded()

            -- Snapshot 2: after load.
            UpdateAddOnMemoryUsage()
            local memAfter = GetAddOnMemoryUsage("EnhancedTransmogBrowser")
            local delta = memAfter - memBefore

            ETB:Print(string.format("Memory after load: |cffffd100%s|r", ETB:FormatMemory(memAfter)))
            ETB:Print(string.format("Delta materialized: |cff00ff00+%s|r", ETB:FormatMemory(delta)))

            if delta < 10240 then  -- less than 10 MB means something's off
                ETB:Print("|cffff6666Warning:|r delta < 10 MB. Expected ~75-90 MB on fresh login.")
                ETB:Print("Possible causes: (a) another module already triggered part of the load,")
                ETB:Print("(b) the lazy-load pattern isn't working as designed. Try /reload then")
                ETB:Print("rerun this command as your very first action.")
            else
                ETB:Print(string.format("|cff00ff00✓ Lazy-load is working.|r Without deferral, this %s",
                    ETB:FormatMemory(delta)))
                ETB:Print("  would have been resident from login. Deferral is paying off.")
            end
        end,
        help = "Verify v12.42.0 lazy-load is working (run after /reload for accurate numbers)",
        user = true,
    }

    CR:Register{
        name = "transmogui", aliases = {"tui", "apply"},
        handler = function()
            if ETB.TransmogUI then
                ETB.TransmogUI:Toggle()
            else
                ETB:Print("TransmogUI not loaded!")
            end
        end,
        help = "Open the transmog application UI",
        user = true,
    }

    CR:Register{
        name = "erb", aliases = {"bis", "raidbis"},
        handler = function()
            local ERB = ETB:GetERB()
            if ERB and ERB.MainFrame then
                ERB.MainFrame:Toggle()
            else
                ETB:Print("|cffff6666ERB module failed to load. Try /reload.|r")
            end
        end,
        help = "Open Raid BiS Browser",
        user = true,
    }

    -- v15.7.0: Custom user-defined BiS lists.
    -- Subcommand dispatch: /etb cbis <verb> [args...]
    --   list
    --   create <name>
    --   delete <name>
    --   rename <old> <new>
    --   add <listName> <slot> <itemID> [phase]
    --   remove <listName> <slot> <rank> [phase]
    --   activate <name>
    --   show <name>
    --
    -- All operations scope to the player's current class+spec via DataBridge.
    CR:Register{
        name = "cbis", aliases = {"custombis"},
        handler = function(args)
            local ERB = ETB:GetERB()
            local CB = ERB and ERB.CustomBiS
            if not CB then
                ETB:Print("|cffff6666CustomBiS module not loaded.|r")
                return
            end
            local verb = args[2]
            if not verb or verb == "help" then
                ETB:Print("|cffffd100Custom BiS lists|r — usage:")
                ETB:Print("  /etb cbis list                     - list all your custom BiS lists")
                ETB:Print("  /etb cbis create <name>            - create a new empty list")
                ETB:Print("  /etb cbis delete <name>            - delete a list")
                ETB:Print("  /etb cbis rename <old> <new>       - rename a list")
                ETB:Print("  /etb cbis add <list> <slot> <itemID> [phase]   - add item to slot")
                ETB:Print("  /etb cbis remove <list> <slot> <rank> [phase]  - remove by rank")
                ETB:Print("  /etb cbis activate <name>          - switch ERB to this list")
                ETB:Print("  /etb cbis show <name>              - print contents")
                ETB:Print("  Slot IDs: 1=Head 2=Neck 3=Shoulder 4=Back 5=Chest 6=Wrist")
                ETB:Print("            7=Hands 8=Waist 9=Legs 10=Feet 11=Finger 12=Trinket 13=Weapon")
                return
            end

            if verb == "list" then
                local names = CB:ListAll()
                if #names == 0 then
                    ETB:Print("|cffaaaaaa(no custom lists yet — use /etb cbis create <name>)|r")
                    return
                end
                ETB:Print("|cffffd100Custom BiS lists for this class/spec:|r")
                for _, n in ipairs(names) do
                    local entry = CB:GetList(n)
                    local count = 0
                    if entry and entry.items then
                        for _, slots in pairs(entry.items) do
                            for _, ids in pairs(slots) do count = count + #ids end
                        end
                    end
                    ETB:Print(string.format("  %s |cffaaaaaa(%d items)|r", n, count))
                end
                return
            end

            if verb == "create" then
                local name = args[3]
                if not name then ETB:Print("Usage: /etb cbis create <name>"); return end
                local ok, err = CB:Create(name)
                if ok then ETB:Print("|cff00ff00Created|r custom BiS list: " .. name)
                else      ETB:Print("|cffff6666Error:|r " .. tostring(err)) end
                return
            end

            if verb == "delete" then
                local name = args[3]
                if not name then ETB:Print("Usage: /etb cbis delete <name>"); return end
                local ok, err = CB:Delete(name)
                if ok then ETB:Print("|cff00ff00Deleted|r custom BiS list: " .. name)
                else      ETB:Print("|cffff6666Error:|r " .. tostring(err)) end
                return
            end

            if verb == "rename" then
                local oldName, newName = args[3], args[4]
                if not oldName or not newName then
                    ETB:Print("Usage: /etb cbis rename <old> <new>"); return
                end
                local ok, err = CB:Rename(oldName, newName)
                if ok then ETB:Print("|cff00ff00Renamed|r " .. oldName .. " to " .. newName)
                else      ETB:Print("|cffff6666Error:|r " .. tostring(err)) end
                return
            end

            if verb == "add" then
                local name = args[3]
                local slot = tonumber(args[4])
                local itemID = tonumber(args[5])
                local phase = args[6]  -- optional
                if not name or not slot or not itemID then
                    ETB:Print("Usage: /etb cbis add <list> <slot 1-13> <itemID> [phase]"); return
                end
                local ok, errOrRank = CB:AddItem(name, slot, itemID, phase)
                if ok then
                    ETB:Print(string.format("|cff00ff00Added|r itemID %d to %s slot %d at rank %d",
                                            itemID, name, slot, errOrRank))
                else
                    ETB:Print("|cffff6666Error:|r " .. tostring(errOrRank))
                end
                return
            end

            if verb == "remove" then
                local name = args[3]
                local slot = tonumber(args[4])
                local rank = tonumber(args[5])
                local phase = args[6]
                if not name or not slot or not rank then
                    ETB:Print("Usage: /etb cbis remove <list> <slot> <rank> [phase]"); return
                end
                local ok, errOrItem = CB:RemoveItem(name, slot, rank, phase)
                if ok then
                    ETB:Print(string.format("|cff00ff00Removed|r itemID %d from %s slot %d",
                                            errOrItem, name, slot))
                else
                    ETB:Print("|cffff6666Error:|r " .. tostring(errOrItem))
                end
                return
            end

            if verb == "activate" then
                local name = args[3]
                if not name then ETB:Print("Usage: /etb cbis activate <name>"); return end
                if not CB:GetList(name) then
                    ETB:Print("|cffff6666Error:|r list does not exist")
                    return
                end
                if ERB.DataBridge then
                    if ERB.DataBridge:SwitchSource("cbis:" .. name) then
                        ETB:Print("|cff00ff00Activated|r ERB source: |cffffd100★ " .. name .. "|r")
                    else
                        ETB:Print("|cffff6666Error:|r SwitchSource failed (no items in list?)")
                    end
                end
                return
            end

            if verb == "show" then
                local name = args[3]
                if not name then ETB:Print("Usage: /etb cbis show <name>"); return end
                local entry = CB:GetList(name)
                if not entry then ETB:Print("|cffff6666Error:|r list does not exist"); return end
                ETB:Print("|cffffd100" .. name .. "|r:")
                local slotNames = ERB.SlotNames or {}
                local anyItems = false
                for phase, slotData in pairs(entry.items or {}) do
                    for slotIdx, ids in pairs(slotData) do
                        if #ids > 0 then
                            anyItems = true
                            local sName = slotNames[slotIdx] or ("Slot" .. slotIdx)
                            local idStr = table.concat(ids, ", ")
                            ETB:Print(string.format("  |cffaaaaaa[%s]|r %s: %s", phase, sName, idStr))
                        end
                    end
                end
                if not anyItems then
                    ETB:Print("  |cffaaaaaa(empty — use /etb cbis add to populate)|r")
                end
                return
            end

            ETB:Print("|cffff6666Unknown subcommand:|r " .. tostring(verb) .. " — try /etb cbis help")
        end,
        help = "Manage custom user-defined BiS lists (/etb cbis help for usage)",
        user = true,
    }

    CR:Register{
        name = "npcnames",
        handler = function()
            if ETB.RuntimeNPCNames then
                ETB.RuntimeNPCNames:PrintStats()
            else
                ETB:Print("RuntimeNPCNames module not loaded.")
            end
        end,
        help = "Show RuntimeNPCNames cache stats",
        user = true,
    }

    -- v15.8.13: in-game audit tool for Community Sets data file. Walks
    -- every loaded set and reports per-slot armor-type mismatches —
    -- entries where the itemID is for a different armor type than the
    -- set's declared armorType. Use the report to drive corrections to
    -- the data file.
    CR:Register{
        name = "auditcs", aliases = {"setaudit"},
        handler = function()
            if not ETB.CommunitySets or not ETB.CommunitySets.Audit then
                ETB:Print("CommunitySets module not loaded.")
                return
            end
            local report, err = ETB.CommunitySets:Audit()
            if err then
                ETB:Print("|cffff6666Audit error: " .. err .. "|r")
                return
            end
            ETB.CommunitySets:PrintAuditReport(report)
        end,
        help = "Audit community sets for wrong-armor-type itemIDs",
        user = true,
    }

    -- v12.43.0 migrations: simple single-action commands pulled from the
    -- legacy elseif chain. Each is a 1-3 line handler with no argument
    -- parsing, matching the v12.40.0 migration criteria.

    CR:Register{
        name = "favorites", aliases = {"fav"},
        handler = function()
            if ETB.FavoritesManager then
                ETB.FavoritesManager:PrintStats()
            else
                ETB:Print("FavoritesManager not loaded.")
            end
        end,
        help = "Print favorites stats",
        user = true,
    }

    CR:Register{
        name = "minimap",
        handler = function()
            if ETB.MinimapButton then
                ETB.MinimapButton:Toggle()
            else
                ETB:Print("MinimapButton not loaded.")
            end
        end,
        help = "Toggle the minimap button",
        user = true,
    }

    CR:Register{
        name = "macro",
        handler = function() ETB:CreateMacro() end,
        help = "Create a /etb macro on your action bar",
        user = true,
    }

    -- v15.8.0: tooltips subcommand router
    -- Usage:
    --   /etb tooltips                       - toggle master enhancer
    --   /etb tooltips itemids               - toggle showing item IDs in tooltips
    --   /etb tooltips source                - toggle showing source/instance info
    --   /etb tooltips collected             - toggle showing collected-status line
    --   /etb tooltips cantlearn             - toggle "can't learn" warning line
    --   /etb tooltips status                - show all tooltip toggle states
    local function tooltipsToggleKey(key, label)
        local cur = ETB.Settings:Get(key)
        ETB.Settings:Set(key, not cur)
        ETB:Print(string.format("%s: %s", label, (not cur) and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
    end
    CR:Register{
        name = "tooltips",
        handler = function(args)
            local sub = args[2]
            if not sub then
                if ETB.TooltipEnhancer then ETB.TooltipEnhancer:Toggle()
                else ETB:Print("TooltipEnhancer not loaded.") end
                return
            end
            sub = sub:lower()
            if sub == "itemids" or sub == "itemid" then
                tooltipsToggleKey("tooltips.showItemIDs", "Tooltip: show item IDs")
            elseif sub == "source" then
                tooltipsToggleKey("tooltips.showSource", "Tooltip: show source/instance")
            elseif sub == "collected" then
                tooltipsToggleKey("tooltips.showCollected", "Tooltip: show collected status")
            elseif sub == "cantlearn" or sub == "canlearn" then
                tooltipsToggleKey("tooltips.showCantLearn", "Tooltip: show 'can't learn' warning")
            elseif sub == "status" or sub == "show" then
                ETB:Print("|cffffd100Tooltip toggles:|r")
                ETB:Print(string.format("  master (enhance):  %s", ETB.Settings:Get("tooltips.enhance") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
                ETB:Print(string.format("  itemids:           %s", ETB.Settings:Get("tooltips.showItemIDs") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
                ETB:Print(string.format("  source:            %s", ETB.Settings:Get("tooltips.showSource") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
                ETB:Print(string.format("  collected:         %s", ETB.Settings:Get("tooltips.showCollected") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
                ETB:Print(string.format("  cantlearn:         %s", ETB.Settings:Get("tooltips.showCantLearn") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
            elseif sub == "help" then
                ETB:Print("|cffffd100/etb tooltips|r — usage:")
                ETB:Print("  /etb tooltips           - toggle master enhancer")
                ETB:Print("  /etb tooltips itemids   - toggle item ID display")
                ETB:Print("  /etb tooltips source    - toggle source/instance display")
                ETB:Print("  /etb tooltips collected - toggle collected-status line")
                ETB:Print("  /etb tooltips cantlearn - toggle 'can't learn' warning")
                ETB:Print("  /etb tooltips status    - print all toggle states")
            else
                ETB:Print("Unknown subcommand: " .. tostring(sub) .. " — try /etb tooltips help")
            end
        end,
        help = "Toggle appearance tooltip features (/etb tooltips help for subcommands)",
        user = true,
    }

    -- v15.6.0 / v15.8.0: bag overlay subcommands
    -- Usage:
    --   /etb bagoverlay                  - toggle master enable
    --   /etb bagoverlay unlearnable      - toggle gray markers on items you can't equip
    --   /etb bagoverlay status           - show all bag overlay toggle states
    CR:Register{
        name = "bagoverlay", aliases = {"bag"},
        handler = function(args)
            if not ETB.BagOverlay then ETB:Print("BagOverlay not loaded."); return end
            local sub = args[2]
            if not sub then
                ETB.BagOverlay:Toggle()
                return
            end
            sub = sub:lower()
            if sub == "unlearnable" or sub == "gray" or sub == "grey" then
                local cur = ETB.Settings:Get("bagOverlay.showUnlearnable")
                ETB.Settings:Set("bagOverlay.showUnlearnable", not cur)
                ETB:Print(string.format("Bag overlay: show unlearnable markers: %s",
                    (not cur) and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
                if ETB.BagOverlay.RepaintAll then ETB.BagOverlay:RepaintAll() end
            elseif sub == "status" or sub == "show" then
                ETB:Print("|cffffd100Bag overlay toggles:|r")
                ETB:Print(string.format("  master:        %s", ETB.Settings:Get("bagOverlay.enabled") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
                ETB:Print(string.format("  unlearnable:   %s", ETB.Settings:Get("bagOverlay.showUnlearnable") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
            elseif sub == "help" then
                ETB:Print("|cffffd100/etb bagoverlay|r — usage:")
                ETB:Print("  /etb bagoverlay              - toggle yellow markers")
                ETB:Print("  /etb bagoverlay unlearnable  - toggle gray markers (items you can't equip)")
                ETB:Print("  /etb bagoverlay status       - print toggle states")
            else
                ETB:Print("Unknown subcommand: " .. tostring(sub) .. " — try /etb bagoverlay help")
            end
        end,
        help = "Toggle bag overlay markers (/etb bagoverlay help for subcommands)",
        user = true,
    }

    -- v15.8.0: collection toast toggle
    CR:Register{
        name = "toast", aliases = {"notifications", "notif"},
        handler = function()
            local cur = ETB.Settings:Get("notifications.collectionToast")
            ETB.Settings:Set("notifications.collectionToast", not cur)
            ETB:Print(string.format("Collection toast: %s",
                (not cur) and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
        end,
        help = "Toggle the 'You collected an appearance!' popup",
        user = true,
    }

    -- v15.8.16: separate toggle for set-completion toasts.
    CR:Register{
        name = "settoast", aliases = {"setnotif"},
        handler = function()
            local cur = ETB.Settings:Get("notifications.setCompletionToast")
            ETB.Settings:Set("notifications.setCompletionToast", not cur)
            ETB:Print(string.format("Set completion toast: %s",
                (not cur) and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
        end,
        help = "Toggle the 'Set complete!' popup",
        user = true,
    }

    -- v15.8.17: performance instrumentation — print Lua heap, ETB addon
    -- memory, dataset materialization status, set-completion stats.
    -- Useful for diagnosing memory growth / load time issues.
    CR:Register{
        name = "perf", aliases = {"perfmem"},
        handler = function()
            -- Force GC to get an accurate heap snapshot
            collectgarbage("collect")
            local heapKB = collectgarbage("count")

            -- ETB-specific memory via UpdateAddOnMemoryUsage
            UpdateAddOnMemoryUsage()
            local etbMemKB = 0
            local getMem = GetAddOnMemoryUsage
            local getNum = (C_AddOns and C_AddOns.GetNumAddOns) or GetNumAddOns
            local getInfo = (C_AddOns and C_AddOns.GetAddOnInfo) or GetAddOnInfo
            for i = 1, getNum() do
                local name = getInfo(i)
                if name == "EnhancedTransmogBrowser" then
                    etbMemKB = getMem(i) or 0
                    break
                end
            end

            ETB:Print("|cffffd100Performance snapshot:|r")
            ETB:Print(string.format("  Total Lua heap:     |cffffffff%.1f MB|r",
                heapKB / 1024))
            ETB:Print(string.format("  ETB addon memory:   |cffffffff%.1f MB|r",
                etbMemKB / 1024))

            -- Materialization status — which lazy datasets are loaded?
            local materialized = {}
            local function check(name, marker)
                if marker then table.insert(materialized, name) end
            end
            check("BossLoot",      ETB._BossLootLoader == nil)
            check("ItemMetadata",  ETB._ItemMetadataLoader == nil)
            check("ReagentData",   ETB._ReagentDataLoader == nil)
            check("TierTokenMap",  ETB._TierTokenMapLoader == nil)
            check("ColorMap",      NS.ColorMap ~= nil and NS.ColorMap.HasLoaded and NS.ColorMap:HasLoaded())
            check("Compact.MoP",   NS.Compact and NS.Compact._MoPLoader == nil)
            check("Compact.Cata",  NS.Compact and NS.Compact._CataLoader == nil)
            check("Compact.Wrath", NS.Compact and NS.Compact._WrathLoader == nil)
            check("Compact.TBC",   NS.Compact and NS.Compact._TBCLoader == nil)
            check("Compact.Classic", NS.Compact and NS.Compact._ClassicLoader == nil)
            if #materialized == 0 then
                ETB:Print("  Datasets loaded:    |cff888888(none materialized yet)|r")
            else
                ETB:Print(string.format("  Datasets loaded:    |cffffffff%s|r",
                    table.concat(materialized, ", ")))
            end

            -- SetCompletion status
            if ETB.SetCompletion and ETB.SetCompletion.GetStats then
                local scStats = ETB.SetCompletion:GetStats()
                ETB:Print(string.format(
                    "  Set tracking:       %d sets (%d Blz + %d community), %d celebrated",
                    scStats.totalSets, scStats.blzSets, scStats.communitySets, scStats.celebratedSets))
            end

            -- RuntimeNPCNames size
            if ETB_RuntimeNPCNames then
                local count = 0
                for _ in pairs(ETB_RuntimeNPCNames) do count = count + 1 end
                ETB:Print(string.format("  RuntimeNPCNames:    %d entries", count))
            end

            -- Quick guidance based on numbers
            if heapKB > 800 * 1024 then
                ETB:Print("|cffff6666  Warning:|r Lua heap > 800 MB — close some addons before extended play.")
            end
        end,
        help = "Print memory / dataset / performance snapshot",
        user = true,
    }

    -- v15.8.17: toggle the dev-tool warning at login.
    CR:Register{
        name = "devwarn",
        handler = function(args)
            local sub = args[2]
            if sub == "off" or sub == "false" or sub == "no" then
                ETB.Settings:Set("dev.warnOnDevTools", false)
                ETB:Print("Dev-tool warning: |cffff6666OFF|r")
            elseif sub == "on" or sub == "true" or sub == "yes" then
                ETB.Settings:Set("dev.warnOnDevTools", true)
                ETB:Print("Dev-tool warning: |cff00ff00ON|r")
            else
                local cur = ETB.Settings:Get("dev.warnOnDevTools")
                ETB.Settings:Set("dev.warnOnDevTools", not cur)
                ETB:Print(string.format("Dev-tool warning: %s",
                    (not cur) and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
            end
        end,
        help = "Toggle the 'dev tools loaded' login warning",
        user = true,
    }

    -- v15.8.17: inspect JLoot data for a given encounter or NPC ID.
    -- Diagnostic tool to verify boss loot data without needing to be in
    -- the encounter — useful for pre-launch verification of new raids
    -- like SoO before June 4 launch.
    --
    -- Usage:
    --   /etb encloot 869       — list items for encounter 869 (Garrosh)
    --   /etb encloot npc 71865 — list items for NPC 71865 (resolves to encounter)
    --   /etb encloot all       — summary of all encounters
    CR:Register{
        name = "encloot", aliases = {"encounterloot", "jloot"},
        handler = function(args)
            -- Ensure JLoot is materialized
            if ETB._BossLootLoader then ETB._BossLootLoader() end
            if not ETB.JLoot then
                ETB:Print("|cffff6666JLoot data not available|r")
                return
            end

            local sub = args[2]
            if not sub or sub == "help" then
                ETB:Print("|cffffd100/etb encloot|r usage:")
                ETB:Print("  |cffffffff/etb encloot <encID>|r       — list items for an encounter")
                ETB:Print("  |cffffffff/etb encloot npc <npcID>|r   — list items for an NPC")
                ETB:Print("  |cffffffff/etb encloot all|r           — summary of all encounters")
                return
            end

            -- Resolve NPC → encounter if "npc" prefix
            local encID
            if sub == "npc" then
                local npcID = tonumber(args[3])
                if not npcID then
                    ETB:Print("|cffff6666Need an NPC ID after 'npc'|r")
                    return
                end
                encID = ETB.JNpcToEnc and ETB.JNpcToEnc[npcID]
                if not encID then
                    ETB:Print(string.format("|cffff8866No encounter mapping for NPC %d|r", npcID))
                    return
                end
                -- JNpcToEnc can return a table for multi-NPC encounters
                if type(encID) == "table" then
                    encID = encID[1]
                end
                ETB:Print(string.format("|cff888888NPC %d → encounter %d|r", npcID, encID))
            elseif sub == "all" then
                -- Summary mode: count items per encounter
                local counts = {}
                for _, info in pairs(ETB.JLoot) do
                    -- info is {encID, diffBitmask} OR list of {encID, diffBitmask}
                    if type(info[1]) == "number" then
                        counts[info[1]] = (counts[info[1]] or 0) + 1
                    else
                        for _, entry in ipairs(info) do
                            counts[entry[1]] = (counts[entry[1]] or 0) + 1
                        end
                    end
                end
                -- Sort by encID
                local sorted = {}
                for eid in pairs(counts) do table.insert(sorted, eid) end
                table.sort(sorted)
                ETB:Print(string.format("|cffffd100JLoot encounter summary:|r |cff888888(%d encounters)|r", #sorted))
                for _, eid in ipairs(sorted) do
                    local encInfo = ETB.JEncounters and ETB.JEncounters[eid]
                    local name = (encInfo and encInfo[1]) or "?"
                    ETB:Print(string.format("  enc |cffffffff%d|r %s: %d items", eid, name, counts[eid]))
                end
                return
            else
                encID = tonumber(sub)
            end

            if not encID then
                ETB:Print("|cffff6666Invalid encounter ID|r")
                return
            end

            -- Look up encounter name
            local encInfo = ETB.JEncounters and ETB.JEncounters[encID]
            local encName = (encInfo and encInfo[1]) or "?"

            -- Walk all of JLoot looking for items matching this encounter
            local items = {}
            local diffMap = {}  -- itemID → bitmask
            for itemID, info in pairs(ETB.JLoot) do
                if type(info[1]) == "number" then
                    if info[1] == encID then
                        items[#items + 1] = itemID
                        diffMap[itemID] = info[2]
                    end
                else
                    for _, entry in ipairs(info) do
                        if entry[1] == encID then
                            items[#items + 1] = itemID
                            diffMap[itemID] = entry[2]
                            break
                        end
                    end
                end
            end
            table.sort(items)

            -- v15.8.19: tally difficulty distribution for the header so the
            -- Normal/Heroic balance is visible at a glance (the per-item
            -- list is sorted by itemID, which clusters difficulties and can
            -- look lopsided in the first 30 lines).
            local nNormal, nHeroic, nLFR, nOther = 0, 0, 0, 0
            for _, itemID in ipairs(items) do
                local bm = diffMap[itemID] or 0
                if bm % 2 >= 1 then nNormal = nNormal + 1 end
                if math.floor(bm / 2) % 2 >= 1 then nHeroic = nHeroic + 1 end
                if math.floor(bm / 4) % 2 >= 1 then nLFR = nLFR + 1 end
                if math.floor(bm / 8) % 2 >= 1 then nOther = nOther + 1 end
            end

            ETB:Print(string.format("|cffffd100Encounter %d %s:|r %d items",
                encID, encName, #items))

            if #items == 0 then
                ETB:Print("|cffff8866  No loot data for this encounter.|r")
                return
            end

            -- Difficulty breakdown line
            local breakdown = {}
            if nNormal > 0 then table.insert(breakdown, string.format("|cff66ff66Normal: %d|r", nNormal)) end
            if nHeroic > 0 then table.insert(breakdown, string.format("|cffff8800Heroic: %d|r", nHeroic)) end
            if nLFR > 0 then table.insert(breakdown, string.format("|cff8888ffLFR: %d|r", nLFR)) end
            if nOther > 0 then table.insert(breakdown, string.format("|cffaaaaaaHeroicOnly: %d|r", nOther)) end
            ETB:Print("  " .. table.concat(breakdown, "  |cff444444|||r  "))

            -- Print first 30 items with name + difficulty bitmask
            local function bitmaskName(bm)
                if not bm then return "?" end
                local names = {}
                if bm % 2 >= 1 then table.insert(names, "Normal") end
                if math.floor(bm / 2) % 2 >= 1 then table.insert(names, "Heroic") end
                if math.floor(bm / 4) % 2 >= 1 then table.insert(names, "LFR") end
                if math.floor(bm / 8) % 2 >= 1 then table.insert(names, "HeroicOnly") end
                return table.concat(names, "+")
            end

            local shown = math.min(30, #items)
            for i = 1, shown do
                local itemID = items[i]
                local name = GetItemInfo(itemID) or ("item:" .. itemID)
                ETB:Print(string.format("  |cffaaaaaa%d|r %s |cff888888[%s]|r",
                    itemID, name, bitmaskName(diffMap[itemID])))
            end
            if #items > shown then
                ETB:Print(string.format("|cff888888  ... and %d more|r", #items - shown))
            end
        end,
        help = "Inspect JLoot data for an encounter (use 'all' for summary)",
        user = true,
    }

    -- v15.8.0: preview toggles (model preview on hover, background)
    CR:Register{
        name = "preview",
        handler = function(args)
            local sub = args[2]
            if not sub or sub == "status" or sub == "show" then
                ETB:Print("|cffffd100Preview toggles:|r")
                ETB:Print(string.format("  showOnHover:   %s", ETB.Settings:Get("preview.showOnHover") and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
                ETB:Print(string.format("  backgroundID:  %s", tostring(ETB.Settings:Get("preview.backgroundID"))))
                return
            end
            sub = sub:lower()
            if sub == "hover" or sub == "showonhover" then
                local cur = ETB.Settings:Get("preview.showOnHover")
                ETB.Settings:Set("preview.showOnHover", not cur)
                ETB:Print(string.format("Preview on hover: %s",
                    (not cur) and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
            elseif sub == "background" or sub == "bg" then
                local bgID = args[3]
                if not bgID then
                    ETB:Print("Usage: /etb preview background <backgroundID>")
                    ETB:Print("  Current: " .. tostring(ETB.Settings:Get("preview.backgroundID")))
                    return
                end
                ETB.Settings:Set("preview.backgroundID", bgID)
                ETB:Print("Preview background: " .. bgID)
            else
                ETB:Print("Usage: /etb preview [hover | background <id>]")
            end
        end,
        help = "Toggle preview features (/etb preview status)",
        user = true,
    }

    CR:Register{
        name = "debug",
        handler = function()
            ETB.debug = not ETB.debug
            ETB:Print("Debug: " .. (ETB.debug and "|cff00ff00ON|r" or "|cffff0000OFF|r"))
        end,
        help = "Toggle debug mode (also unlocks developer commands)",
        user = true,
    }

    CR:Register{
        name = "wishlist",
        handler = function()
            if ETB.WishlistPanel then
                ETB.WishlistPanel:Toggle()
            else
                ETB:Print("Wishlist panel not loaded.")
            end
        end,
        help = "Toggle the wishlist panel",
        user = true,
    }

    CR:Register{
        name = "farm",
        handler = function()
            if ETB.WishlistPanel then
                ETB.WishlistPanel:Show()
                ETB.WishlistPanel:ShowTab("farm")
            else
                ETB:Print("Wishlist panel not loaded.")
            end
        end,
        help = "Open wishlist directly to the Farm tab",
        user = true,
    }

    -- v12.44.0 migrations: 7 more simple single-action commands pulled from
    -- the legacy elseif chain. Same pattern as v12.43.0 migrations.

    CR:Register{
        name = "memory", aliases = {"mem"},
        handler = function()
            local mem = ETB:GetMemoryUsage()
            ETB:Print("Memory: " .. ETB:FormatMemory(mem))
            if ETB.BlizzardData and ETB.BlizzardData.PrintStats then
                ETB.BlizzardData:PrintStats()
            end
            if ETB.ItemRow and ETB.ItemRow.GetPoolStats then
                local pool = ETB.ItemRow:GetPoolStats()
                ETB:Print(string.format("Item rows: %d active, %d pooled", pool.active, pool.pool))
            end
        end,
        help = "Show memory usage and data statistics",
        user = true,
    }

    CR:Register{
        name = "cache",
        handler = function()
            if not (ETB.BlizzardData and ETB.BlizzardData.GetCacheStats) then
                ETB:Print("BlizzardData cache stats not available.")
                return
            end
            local cache = ETB.BlizzardData:GetCacheStats()
            ETB:Print("=== CACHE STATS ===")
            if cache.items then
                ETB:Print(string.format("Items: %d/%d (%.1f%% hit rate)",
                    cache.items.size, cache.items.maxSize, cache.items.hitRate))
            end
            if cache.icons then
                ETB:Print(string.format("Icons: %d/%d", cache.icons.size, cache.icons.maxSize))
            end
        end,
        help = "Show item/icon LRU cache statistics",
        user = true,
    }

    CR:Register{
        name = "tutorial", aliases = {"guide"},
        handler = function()
            if ETB.Tutorial then
                ETB.Tutorial:Toggle()
            else
                ETB:Print("Tutorial module not loaded.")
            end
        end,
        help = "Open the getting-started tutorial",
        user = true,
    }

    CR:Register{
        name = "sets",
        handler = function()
            if ETB.SetsData then
                ETB.SetsData:PrintDiscovery()
            else
                ETB:Print("SetsData module not loaded.")
            end
        end,
        help = "Discover/list Blizzard transmog sets",
        user = false,  -- debug-gated (power-user diagnostic)
    }

    CR:Register{
        name = "debugset",
        handler = function(args)
            local setID = tonumber(args[2])
            if setID and ETB.SetsData then
                ETB.SetsData:DebugSet(setID)
            else
                ETB:Print("Usage: /etb debugset <setID>")
                ETB:Print("Example: /etb debugset 630")
            end
        end,
        help = "Debug a specific transmog set by setID",
        user = false,  -- debug-gated (developer diagnostic)
    }

    CR:Register{
        name = "loadreport", aliases = {"lr"},
        handler = function()
            if ETB.ArmorData and ETB.ArmorData.PrintLoadReport then
                ETB.ArmorData:PrintLoadReport()
            else
                ETB:Print("|cffff0000ArmorData module not available|r")
            end
        end,
        help = "Print ArmorData load report",
        user = false,  -- debug-gated
    }

    CR:Register{
        name = "help",
        handler = function()
            ETB:Print("=== Enhanced Transmog Browser ===")
            -- Auto-generated from registered commands (v12.44.0 upgrade):
            -- every registered command with user=true and help text shows here.
            local lines = ETB.CommandRegistry:GetHelpLines()
            for _, line in ipairs(lines) do
                ETB:Print(line)
            end
            ETB:Print("")
            ETB:Print("You can also click the minimap button or create a macro:")
            ETB:Print("|cff00ccff/click ETB_MinimapButton|r or |cff00ccff/etb|r")
        end,
        help = "Show this help message",
        user = true,
    }

    -- v12.46.0 migrations: outfits, audit, sourcetest, instance, api, reset,
    -- purgedata, discover, bossloot/blt. Nine more from the legacy chain.

    CR:Register{
        name = "outfits",
        handler = function()
            if not ETB.ProfileManager then
                ETB:Print("ProfileManager not loaded.")
                return
            end
            local outfits = ETB.ProfileManager:GetAllOutfits()
            local count = 0
            ETB:Print("=== SAVED OUTFITS ===")
            for id, outfit in pairs(outfits) do
                count = count + 1
                local compat = table.concat(outfit.compatibleClasses, ", ")
                ETB:Print(string.format("  %s - %d pieces (Compatible: %s)",
                    outfit.name, ETB.ProfileManager:CountTable(outfit.items), compat))
            end
            if count == 0 then
                ETB:Print("  No saved outfits")
            end
        end,
        help = "List saved outfits",
        user = true,
    }

    -- v15.8.1: setbuilder slash command. The set builder was previously only
    -- reachable from the Builder tab inside the main addon UI. Adding a
    -- direct slash command per user feedback.
    CR:Register{
        name = "setbuilder", aliases = {"builder", "sb"},
        handler = function()
            if not ETB.SetBuilder then
                ETB:Print("|cffff4444SetBuilder module not loaded|r")
                return
            end
            if ETB.SetBuilder.Show then
                ETB.SetBuilder:Show()
            elseif ETB.SetBuilder.Toggle then
                ETB.SetBuilder:Toggle()
            else
                ETB:Print("|cffff4444SetBuilder has no Show or Toggle method|r")
            end
        end,
        help = "Open the Set Builder window",
        user = true,
    }

    CR:Register{
        name = "audit",
        handler = function(args)
            if not ETB.LootAudit then
                ETB:Print("|cffff4444LootAudit module not loaded|r")
                return
            end
            local subcmd = args[2] or "full"
            if subcmd == "full" then ETB.LootAudit:RunFull()
            elseif subcmd == "multi" then ETB.LootAudit:AuditMultiNPC()
            elseif subcmd == "orphans" then ETB.LootAudit:AuditOrphans()
            elseif subcmd == "compare" then ETB.LootAudit:AuditCoverage()
            elseif subcmd == "drops" then ETB.LootAudit:AuditDrops()
            elseif subcmd == "item" then ETB.LootAudit:LookupItem(args[3])
            elseif subcmd == "boss" then ETB.LootAudit:LookupBoss(args[3])
            else
                ETB:Print("Usage: /etb audit [full|multi|orphans|compare|drops|item <id>|boss <npcID>]")
            end
        end,
        help = "Loot data audit: full|multi|orphans|compare|drops|item <id>|boss <npcID>",
        user = true,
    }

    CR:Register{
        name = "reset",
        handler = function(args)
            if args[2] == "confirm" then
                if ETB.Settings then ETB.Settings:Reset() end
                if ETB.FavoritesManager then ETB.FavoritesManager:ClearAll() end
                if ETB.BlizzardData then ETB.BlizzardData:ClearCache() end
                ETB:Print("All data reset!")
            else
                ETB:Print("Type |cffffd100/etb reset confirm|r to reset all data")
            end
        end,
        help = "Reset all settings and cached data (requires 'confirm')",
        user = true,
    }

    CR:Register{
        name = "sourcetest",
        handler = function(args)
            local itemID = tonumber(args[2])
            if not itemID then
                ETB:Print("Usage: |cffffd100/etb sourcetest <itemID>|r")
                ETB:Print("Example: /etb sourcetest 16963  (Helm of Wrath)")
                return
            end
            ETB:Print("|cff00ffff=== SOURCE TEST: Item " .. itemID .. " ===|r")

            -- Test SourceData_ATT first
            if ETB.SourceData_ATT then
                ETB:Print("")
                ETB:Print("|cffffd100[SourceData_ATT] ATT Data (15K+ items):|r")
                if ETB.SourceData_ATT:HasSource(itemID) then
                    local raw = ETB.SourceData_ATT:GetRawSource(itemID)
                    if raw then
                        ETB:Print(string.format("  Raw: encounterID=%s, instanceID=%s, diff=%s, exp=%s, type=%s",
                            tostring(raw[1]), tostring(raw[2]), tostring(raw[3]), tostring(raw[4]), tostring(raw[5])))
                    end
                    local source = ETB.SourceData_ATT:GetPrimarySource(itemID)
                    if source then
                        ETB:Print(string.format("  Type: %s", source.type or "nil"))
                        ETB:Print(string.format("  Name: %s", source.name or "nil"))
                        ETB:Print(string.format("  Location: %s", source.location or "nil"))
                        ETB:Print(string.format("  EncounterID: %s", tostring(source.encounterID)))
                        ETB:Print(string.format("  InstanceID: %s", tostring(source.instanceID)))
                        ETB:Print(string.format("  WorldBoss: %s", tostring(source.isWorldBoss)))
                    end
                else
                    ETB:Print("  No source data found")
                end
            else
                ETB:Print("")
                ETB:Print("|cffff0000[SourceData_ATT] NOT LOADED|r")
            end

            -- Test SourceMapper
            if ETB.SourceMapper then
                ETB:Print("")
                ETB:Print("|cffffd100[SourceMapper] Runtime Data:|r")
                ETB.SourceMapper:TestItem(itemID)
            end

            -- Blizzard API data
            if C_TransmogCollection and C_TransmogCollection.GetItemInfo then
                ETB:Print("")
                ETB:Print("|cffffd100[Blizzard API]:|r")
                local appearanceID, sourceID = C_TransmogCollection.GetItemInfo(itemID)
                ETB:Print(string.format("  AppearanceID: %s", tostring(appearanceID)))
                ETB:Print(string.format("  SourceID: %s", tostring(sourceID)))
                if sourceID and C_TransmogCollection.GetSourceInfo then
                    local sourceInfo = C_TransmogCollection.GetSourceInfo(sourceID)
                    if sourceInfo then
                        ETB:Print(string.format("  SourceType: %s", tostring(sourceInfo.sourceType)))
                        ETB:Print(string.format("  Quality: %s", tostring(sourceInfo.quality)))
                        ETB:Print(string.format("  Collected: %s", tostring(sourceInfo.isCollected)))
                    end
                end
            end
        end,
        help = "Test source mapping for an item by itemID",
        user = false,
    }

    CR:Register{
        name = "instance",
        handler = function(args)
            local instanceID = tonumber(args[2])
            if not instanceID then
                ETB:Print("Usage: |cffffd100/etb instance <instanceID>|r")
                ETB:Print("Example: /etb instance 409  (Molten Core)")
                return
            end
            if not ETB.InstanceData then
                ETB:Print("InstanceData module not loaded.")
                return
            end
            local instance = ETB.InstanceData:GetInstance(instanceID)
            if not instance then
                ETB:Print("|cffff4444Instance " .. instanceID .. " not found|r")
                return
            end
            ETB:Print("|cff00ffff=== INSTANCE " .. instanceID .. " ===|r")
            ETB:Print(string.format("  Name: %s (%s)", instance.name, instance.shortName))
            ETB:Print(string.format("  Type: %s", instance.type == 1 and "Dungeon" or "Raid"))
            ETB:Print(string.format("  Expansion: %d", instance.expansion))
            ETB:Print(string.format("  Max Players: %d", instance.maxPlayers))
            local encounters = ETB.InstanceData:GetInstanceEncounters(instanceID)
            if encounters and #encounters > 0 then
                ETB:Print(string.format("  Encounters (%d):", #encounters))
                for _, enc in ipairs(encounters) do
                    ETB:Print(string.format("    %d. %s (ID: %d)", enc.order or 0, enc.name, enc.encounterID))
                end
            end
        end,
        help = "Look up an instance by instanceID",
        user = false,
    }

    CR:Register{
        name = "api",
        handler = function()
            ETB:Print("=== API TEST ===")
            local tests = {
                { name = "C_TransmogCollection", check = C_TransmogCollection ~= nil },
                { name = "GetCategoryAppearances", check = C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances ~= nil },
                { name = "GetAppearanceSources", check = C_TransmogCollection and C_TransmogCollection.GetAppearanceSources ~= nil },
                { name = "PlayerHasTransmog", check = C_TransmogCollection and C_TransmogCollection.PlayerHasTransmog ~= nil },
                { name = "GetItemInfoInstant", check = GetItemInfoInstant ~= nil },
            }
            for _, t in ipairs(tests) do
                ETB:Print(string.format("  %s %s", t.check and "|cff00ff00✓|r" or "|cffff0000✗|r", t.name))
            end
            if C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances then
                local ok, apps = pcall(C_TransmogCollection.GetCategoryAppearances, 1)
                if ok and apps then
                    ETB:Print(string.format("  |cff00ff00✓|r GetCategoryAppearances(1) = %d appearances", #apps))
                end
            end
        end,
        help = "Test Blizzard transmog API availability",
        user = false,
    }

    CR:Register{
        name = "purgedata",
        handler = function(args)
            if args[2] == "confirm" then
                if ETB.SourceData_ATT and ETB.SourceData_ATT.Reset then
                    ETB.SourceData_ATT:Reset()
                end
                ETB:Print("|cffff4444=== SOURCE DATA CACHE CLEARED ===|r")
                ETB:Print("Cached data cleared. Data reloads from addon files on /reload.")
            else
                ETB:Print("|cffff8800=== CLEAR DATA CACHE ===|r")
                ETB:Print("This clears cached source data from memory.")
                ETB:Print("Type |cffff4444/etb purgedata confirm|r to proceed.")
            end
        end,
        help = "Clear MapperBridge source cache (requires 'confirm')",
        user = false,
    }

    CR:Register{
        name = "discover",
        handler = function(args)
            if not ETB.CustomSets then
                ETB:Print("CustomSets module not loaded!")
                return
            end
            local categoryID = tonumber(args[2]) or (ETB.Filtering and ETB.Filtering.filters and ETB.Filtering.filters.category)
            if not categoryID then
                ETB:Print("Usage: /etb discover [categoryID]")
                return
            end
            ETB:Print(string.format("|cff00ffff=== DISCOVERING POTENTIAL SETS (Category %d) ===|r", categoryID))
            local items = ETB.Filtering and ETB.Filtering:GetItems() or {}
            if #items == 0 then
                ETB:Print("No items loaded. Open a category first!")
                return
            end
            ETB:Print(string.format("Analyzing %d items for naming patterns...", #items))
            local potentialSets = ETB.CustomSets:DiscoverPotentialSets(items, 3)
            if #potentialSets == 0 then
                ETB:Print("No potential sets found (need 3+ pieces with matching names)")
            else
                ETB:Print(string.format("Found %d potential sets:", #potentialSets))
                for i, set in ipairs(potentialSets) do
                    if i <= 10 then
                        ETB:Print(string.format("  |cffffd100%s|r - %d pieces (%s)",
                            set.name, set.pieceCount, set.armorType or "unknown"))
                        for j, item in ipairs(set.items) do
                            if j <= 3 then
                                ETB:Print(string.format("    - %s", item.name or "Unknown"))
                            elseif j == 4 then
                                ETB:Print(string.format("    ... and %d more", #set.items - 3))
                                break
                            end
                        end
                    end
                end
                if #potentialSets > 10 then
                    ETB:Print(string.format("|cff888888... and %d more potential sets|r", #potentialSets - 10))
                end
            end
        end,
        help = "Discover potential custom sets for a category",
        user = false,
    }

    CR:Register{
        name = "bossloot", aliases = {"blt"},
        handler = function(args)
            if not ETB.BossLootTooltip then
                ETB:Print("BossLootTooltip module not loaded")
                return
            end
            local subcmd = args[2]
            if subcmd == "stats" then
                ETB:Print("|cff00ccffBoss Loot Tooltip:|r " .. ETB.BossLootTooltip:GetStats())
                ETB:Print("Enabled: " .. (ETB.BossLootTooltip.enabled and "|cff00ff00Yes|r" or "|cffff0000No|r"))
            elseif subcmd == "build" then
                ETB.BossLootTooltip:BuildIndex()
                ETB:Print("|cff00ccffBoss Loot:|r Index rebuilt - " .. ETB.BossLootTooltip:GetStats())
            else
                ETB.BossLootTooltip:Toggle()
            end
        end,
        help = "Toggle boss loot tooltips (or 'stats'/'build' subcommands)",
        user = true,
    }

    -- Debug-gated commands (user=false): registry still dispatches, but the
    -- gate in SlashCmdList enforces ETB.debug is true before running.
    CR:Register{
        name = "clearcache",
        handler = function()
            if ETB.BlizzardData and ETB.BlizzardData.ClearCache then
                ETB.BlizzardData:ClearCache()
                ETB:Print("Item/icon caches cleared.")
            end
        end,
        user = false,
    }

    -- ========================================================================
    -- v12.49.0 migrations: 8 small/medium commands moved from legacy chain
    -- ========================================================================

    CR:Register{
        name = "stats",
        handler = function()
            local stats = ETB.BlizzardData:GetAllStats()
            ETB:Print("=== COLLECTION STATS ===")
            ETB:Print(string.format("Total: %s appearances", ETB:FormatNumber(stats.total)))
            ETB:Print(string.format("Collected: %s (%.1f%%)", ETB:FormatNumber(stats.collected), stats.percent))
            ETB:Print("")
            for catID, catStats in pairs(stats.byCategory) do
                local info = ETB.CATEGORY_INFO[catID]
                ETB:Print(string.format("  %s: %d/%d (%.0f%%)",
                    info and info.name or catID, catStats.collected, catStats.total, catStats.percent))
            end
        end,
        help = "Show collection statistics by category",
        user = true,
    }

    CR:Register{
        name = "profiles", aliases = {"profile"},
        handler = function()
            if ETB.ProfileManager then
                ETB.ProfileManager:PrintStats()
            else
                ETB:Print("ProfileManager not loaded!")
            end
        end,
        help = "Show character profile statistics",
        user = true,
    }

    CR:Register{
        name = "switch",
        handler = function(args)
            if args[2] and ETB.ProfileManager then
                local profiles = ETB.ProfileManager:GetAllProfiles()
                local searchName = args[2]:lower()
                local found = nil
                for _, p in ipairs(profiles) do
                    if p.name:lower() == searchName or p.key:lower():find(searchName) then
                        found = p.key
                        break
                    end
                end
                if found then
                    ETB.ProfileManager:SetActiveProfile(found)
                    local profile = ETB.ProfileManager:GetActiveProfile()
                    ETB:Print("Switched to: |cffffd100" .. profile.name .. "|r (" .. profile.classID .. ")")
                    if ETB.MainFrame and ETB.MainFrame.RefreshItems then
                        ETB.MainFrame:RefreshItems()
                    end
                else
                    ETB:Print("Profile not found: " .. args[2])
                    ETB:Print("Available profiles:")
                    for _, p in ipairs(profiles) do
                        ETB:Print("  - " .. p.name .. " (" .. p.classID .. ")")
                    end
                end
            else
                ETB:Print("Usage: |cffffd100/etb switch <CharacterName>|r")
            end
        end,
        help = "Switch active character profile",
        user = false,
    }

    CR:Register{
        name = "sources",
        handler = function()
            ETB:Print("|cff00ffff=== SOURCE DATA (MapperBridge) ===|r")
            if ETB.SourceData_ATT then
                local info = ETB.SourceData_ATT:GetDataInfo()
                ETB:Print(string.format("|cffffd100Data source:|r %s", info.source))
                ETB:Print(string.format("|cffffd100Total items:|r %d", info.itemCount))
                if info.expansions then
                    for exp, meta in pairs(info.expansions) do
                        ETB:Print(string.format("  %s: %d items (v%s, %s)",
                            exp, meta.itemCount or 0, meta.version or "?", meta.generated or "?"))
                    end
                end
                local wbCount = 0
                if ETB.SourceData_ATT.WORLD_BOSSES then
                    for _ in pairs(ETB.SourceData_ATT.WORLD_BOSSES) do wbCount = wbCount + 1 end
                end
                ETB:Print(string.format("  World bosses: %d", wbCount))
            else
                ETB:Print("|cffff0000MapperBridge: NOT LOADED|r")
            end
            if ETB.SourceMapper then
                ETB:Print("")
                ETB:Print("|cffffd100SourceMapper (Runtime):|r")
                ETB.SourceMapper:PrintStats()
            end
            if ETB.InstanceData then
                ETB:Print("")
                ETB.InstanceData:PrintStats()
            end
        end,
        help = "Show item source data statistics",
        user = false,
    }

    CR:Register{
        name = "sourcepanel", aliases = {"sp"},
        handler = function()
            if ETB.SourcesSummaryPanel then
                local MF = ETB.MainFrame
                local slots = (MF and MF.livePreviewSlots) or {}
                ETB.SourcesSummaryPanel:Toggle(slots)
            else
                ETB:Print("SourcesSummaryPanel not loaded!")
            end
        end,
        help = "Open Sources Summary Panel for live preview items",
        user = false,
    }

    CR:Register{
        name = "diagnose", aliases = {"diag"},
        handler = function()
            ETB:Print("|cff00ffff=== ETB DIAGNOSTIC ===|r")
            ETB:Print("Version: " .. (ETB.version or "?"))
            ETB:Print("Initialized: " .. tostring(ETB.initialized))
            if ETB.SourceData_ATT then
                ETB:Print("SourceData_ATT: loaded (" .. (ETB.SourceData_ATT._itemCount or "?") .. " items)")
                ETB:Print("Data source: " .. (ETB.SourceData_ATT._dataSource or "?"))
            else
                ETB:Print("|cffff4444SourceData_ATT not loaded|r")
            end
            if ETB.JLoot then
                local jlCount = 0
                for _ in pairs(ETB.JLoot) do jlCount = jlCount + 1 end
                ETB:Print("JLoot: " .. jlCount .. " items")
            end
            local ERB = ETB.ERB
            if ERB and ERB.DataBridge then
                ETB:Print("|cff00ccffERB:|r " .. (ERB.DataBridge.playerSpecName or "?") ..
                    " — " .. ERB.DataBridge.itemCount .. " BiS items" ..
                    " — source: " .. (ERB.DataBridge.activeSource or "?"))
            end
        end,
        help = "Print ETB diagnostic information",
        user = false,
    }

    CR:Register{
        name = "customsets", aliases = {"csets"},
        handler = function(args)
            if not ETB.CustomSets then
                ETB:Print("CustomSets module not loaded!")
                return
            end
            local subCmd = args[2] or "list"

            if subCmd == "list" then
                ETB:Print("|cff00ffff=== CUSTOM SETS ===|r")
                local allSets = ETB.CustomSets:GetAllSets()
                local count = 0
                for setID, set in pairs(allSets) do
                    count = count + 1
                    local collected, total = ETB.CustomSets:GetSetCompletion(setID)
                    local statusColor = (collected == total) and "|cff00ff00" or "|cffff6600"
                    local customMark = set.isUserDefined and "|cffffd100[USER]|r " or ""
                    ETB:Print(string.format("  %s%s - %s%d/%d|r (%s)",
                        customMark, set.name, statusColor, collected, total, set.source or "Unknown"))
                end
                ETB:Print(string.format("|cff888888Total: %d custom sets|r", count))

            elseif subCmd == "stats" then
                ETB.CustomSets:PrintStats()

            elseif subCmd == "usable" then
                ETB:Print("|cff00ffff=== USABLE CUSTOM SETS ===|r")
                local sets = ETB.CustomSets:GetSetsForPlayer()
                for setID, set in pairs(sets) do
                    local collected, total = ETB.CustomSets:GetSetCompletion(setID)
                    ETB:Print(string.format("  %s - %d/%d", set.name, collected, total))
                end
            else
                ETB:Print("Custom Sets Commands:")
                ETB:Print("  |cffffd100/etb csets list|r - List all custom sets")
                ETB:Print("  |cffffd100/etb csets stats|r - Custom sets statistics")
                ETB:Print("  |cffffd100/etb csets usable|r - Sets usable by current class")
            end
        end,
        help = "Manage custom sets (subcommands: list, stats, usable)",
        user = false,
    }

    CR:Register{
        name = "inspect",
        handler = function(args)
            local itemID = tonumber(args[2])
            if not itemID then
                ETB:Print("Usage: /etb inspect <itemID>")
                return
            end

            ETB:Print("=== ITEM " .. itemID .. " ===")
            local name, link, quality = GetItemInfo(itemID)
            if name then
                ETB:Print("Name: " .. (link or name))
                ETB:Print("Quality: " .. ETB:GetQualityName(quality))
            else
                ETB:Print("Item not cached")
            end

            local src, sType, zone = ETB.SourceData:GetSourceFormatted(itemID)
            if src then
                ETB:Print("Source: |cff00ff00" .. src .. "|r")
                ETB:Print("Type: " .. (sType or "?"))
            else
                ETB:Print("Source: |cff888888Unknown|r")
            end

            if ETB.FavoritesManager:IsFavorite(itemID) then
                ETB:Print("|cffffd100★ Favorited|r")
            end
        end,
        help = "Inspect a specific item ID — name, source, favorite status",
        user = false,
    }

    -- ========================================================================
    -- v12.50.0 migrations: 7 medium commands moved from legacy chain
    -- ========================================================================

    CR:Register{
        name = "weapontest",
        handler = function(args)
            local itemID = tonumber(args[2])
            if itemID then
                ETB:Print("=== WEAPON TYPE TEST: Item " .. itemID .. " ===")

                if GetItemInfoInstant then
                    local iid, itype, isubtype, iloc, icon, classID, subclassID = GetItemInfoInstant(itemID)
                    ETB:Print(string.format("GetItemInfoInstant: classID=%s, subclassID=%s",
                        tostring(classID), tostring(subclassID)))
                    ETB:Print(string.format("  itemType=%s, itemSubType=%s, equipLoc=%s",
                        tostring(itype), tostring(isubtype), tostring(iloc)))
                else
                    ETB:Print("GetItemInfoInstant: NOT AVAILABLE")
                end

                local name, link, quality, _, _, itemType, itemSubType = GetItemInfo(itemID)
                ETB:Print(string.format("GetItemInfo: name=%s, itemType=%s, itemSubType=%s",
                    tostring(name), tostring(itemType), tostring(itemSubType)))

                local wt, is2h = ETB.BlizzardData:GetWeaponType(itemID)
                ETB:Print(string.format("GetWeaponType: type=%s, isTwoHanded=%s",
                    tostring(wt), tostring(is2h)))

                if ETB.BlizzardData.itemCache then
                    local cached = ETB.BlizzardData.itemCache:Get(itemID)
                    if cached then
                        ETB:Print(string.format("Cache: weaponType=%s, name=%s",
                            tostring(cached.weaponType), tostring(cached.name)))
                    else
                        ETB:Print("Cache: NOT CACHED")
                    end
                end
            else
                ETB:Print("=== WEAPON TYPE TEST (sample items) ===")
                local testItems = {25200, 49500, 61100, 82100, 102600}
                for _, testID in ipairs(testItems) do
                    local wt, is2h = ETB.BlizzardData:GetWeaponType(testID)
                    local name = GetItemInfo(testID) or "Unknown"
                    ETB:Print(string.format("  %d (%s): type=%s", testID, name, tostring(wt)))
                end
                ETB:Print("")
                ETB:Print("Usage: |cffffd100/etb weapontest <itemID>|r - Test specific item")
            end
        end,
        help = "Test weapon type detection (with optional item ID)",
        user = false,
    }

    CR:Register{
        name = "categories", aliases = {"cats"},
        handler = function()
            ETB:Print("=== DISCOVERING TRANSMOG CATEGORIES ===")

            if not C_TransmogCollection or not C_TransmogCollection.GetCategoryAppearances then
                ETB:Print("|cffff0000C_TransmogCollection not available!|r")
                return
            end

            for catID = 1, 35 do
                local success, appearances = pcall(C_TransmogCollection.GetCategoryAppearances, catID)
                if success and appearances and #appearances > 0 then
                    local firstApp = appearances[1]
                    local sources = {}
                    if firstApp and firstApp.visualID then
                        local srcSuccess, srcData = pcall(C_TransmogCollection.GetAppearanceSources, firstApp.visualID)
                        if srcSuccess and srcData then
                            sources = srcData
                        end
                    end

                    local sampleInfo = "?"
                    if sources[1] and sources[1].itemID then
                        local iid, itype, isubtype, iloc = GetItemInfoInstant(sources[1].itemID)
                        if itype then
                            sampleInfo = string.format("%s/%s (%s)", itype or "?", isubtype or "?", iloc or "?")
                        end
                    end

                    local ourName = "NOT MAPPED"
                    if ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[catID] then
                        ourName = ETB.CATEGORY_INFO[catID].name
                    end

                    ETB:Print(string.format("  Cat %d: %d appearances - %s [We call it: %s]",
                        catID, #appearances, sampleInfo, ourName))
                end
            end

            ETB:Print("")
            ETB:Print("This shows what MoP Classic ACTUALLY has in each category.")
            ETB:Print("Compare to what we expect in CATEGORY_INFO!")
        end,
        help = "Discover MoP Classic transmog categories (1-35)",
        user = false,
    }

    CR:Register{
        name = "shipinfo",
        handler = function()
            ETB:Print("|cff00ffff============================================|r")
            ETB:Print("|cff00ffff  HOW TO UPDATE SHIPPED DATA|r")
            ETB:Print("|cff00ffff============================================|r")
            ETB:Print("")
            ETB:Print("|cffff8000Step 1 (in-game with ETBDataMapper):|r")
            ETB:Print("  /etbmap scan")
            ETB:Print("  /etbmap export production")
            ETB:Print("  Log out of WoW")
            ETB:Print("")
            ETB:Print("|cffff8000Step 2 (extract data from SavedVariables):|r")
            ETB:Print("  Open: WTF/Account/<YOU>/SavedVariables/|cff00aaffETBDataMapper.lua|r")
            ETB:Print("  Find the line: |cff00aaffETB_Data_Classic = {|r")
            ETB:Print("  Copy from that line to end of file")
            ETB:Print("  Replace contents of: Data/|cff00aaffData_Shipped.lua|r")
            ETB:Print("")
            ETB:Print("|cffff8000Step 3 (add guards):|r")
            ETB:Print("  Find/replace all: |cff00aaffETB_Data_X = {|r")
            ETB:Print("  With:             |cff00aaffETB_Data_X = ETB_Data_X or {|r")
            ETB:Print("")
            ETB:Print("|cffff8000Step 4:|r Zip and share!")
            ETB:Print("")
            ETB:Print("|cff888888Guards ensure mapper's fresh data wins over shipped data.|r")
            ETB:Print("|cff888888Players without mapper get shipped data automatically.|r")

            if ETB.SourceData_ATT then
                local info = ETB.SourceData_ATT:GetDataInfo()
                ETB:Print("")
                ETB:Print(string.format("|cffffd100Current data:|r %d items (%s)", info.itemCount, info.source))
            end
        end,
        help = "Show instructions for updating shipped data",
        user = false,
    }

    CR:Register{
        name = "armorcount", aliases = {"armorstats"},
        handler = function()
            local total, collected = 0, 0
            local byType = { CLOTH = 0, LEATHER = 0, MAIL = 0, PLATE = 0, OTHER = 0 }

            ETB:Print("|cff00ffff=== ARMOR APPEARANCES (Direct from Blizzard API) ===|r")

            for cat = 1, 11 do
                local catInfo = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[cat]
                local catName = catInfo and catInfo.name or ("Cat " .. cat)
                local apps = C_TransmogCollection.GetCategoryAppearances(cat)
                if apps then
                    local catTotal = #apps
                    local catCollected = 0
                    for _, a in ipairs(apps) do
                        if a.isCollected then catCollected = catCollected + 1 end
                    end
                    total = total + catTotal
                    collected = collected + catCollected

                    local catByType = { CLOTH = 0, LEATHER = 0, MAIL = 0, PLATE = 0, OTHER = 0 }
                    local totalSources = 0
                    for _, a in ipairs(apps) do
                        local sources = C_TransmogCollection.GetAppearanceSources(a.visualID)
                        if sources then
                            for _, src in ipairs(sources) do
                                totalSources = totalSources + 1
                                if src.itemID then
                                    local _, _, _, _, _, classID, subclassID = GetItemInfoInstant(src.itemID)
                                    if classID == 4 then
                                        if subclassID == 1 then catByType.CLOTH = catByType.CLOTH + 1
                                        elseif subclassID == 2 then catByType.LEATHER = catByType.LEATHER + 1
                                        elseif subclassID == 3 then catByType.MAIL = catByType.MAIL + 1
                                        elseif subclassID == 4 then catByType.PLATE = catByType.PLATE + 1
                                        else catByType.OTHER = catByType.OTHER + 1
                                        end
                                    else
                                        catByType.OTHER = catByType.OTHER + 1
                                    end
                                end
                            end
                        end
                    end

                    ETB:Print(string.format("  %s: %d appearances (%d collected) | %d sources (C:%d L:%d M:%d P:%d O:%d)",
                        catName, catTotal, catCollected, totalSources,
                        catByType.CLOTH, catByType.LEATHER, catByType.MAIL, catByType.PLATE, catByType.OTHER))

                    byType.CLOTH = byType.CLOTH + catByType.CLOTH
                    byType.LEATHER = byType.LEATHER + catByType.LEATHER
                    byType.MAIL = byType.MAIL + catByType.MAIL
                    byType.PLATE = byType.PLATE + catByType.PLATE
                    byType.OTHER = byType.OTHER + catByType.OTHER
                end
            end

            ETB:Print("")
            ETB:Print(string.format("|cffffd100TOTAL:|r %d unique appearances (%d collected)", total, collected))
            ETB:Print(string.format("|cffffd100TOTAL SOURCES:|r Cloth:%d Leather:%d Mail:%d Plate:%d Other:%d",
                byType.CLOTH, byType.LEATHER, byType.MAIL, byType.PLATE, byType.OTHER))
            ETB:Print("")
            ETB:Print("|cff888888NOTE: 'Appearances' = unique visuals. 'Sources' = individual items.|r")
            ETB:Print("|cff888888Your addon shows ONE item per appearance, not all sources.|r")
        end,
        help = "Direct Blizzard API armor appearance count by category",
        user = false,
    }

    CR:Register{
        name = "filtertest",
        handler = function()
            local categoryID = ETB.Filtering.filters.category or 1
            ETB:Print(string.format("|cff00ffff=== FILTER TEST for Category %d ===|r", categoryID))

            local apps = C_TransmogCollection.GetCategoryAppearances(categoryID)
            if not apps then
                ETB:Print("|cffff0000No appearances found!|r")
                return
            end

            ETB:Print(string.format("Total appearances from API: %d", #apps))

            local armorTypeCounts = { CLOTH = 0, LEATHER = 0, MAIL = 0, PLATE = 0, ["nil"] = 0 }
            local sample = math.min(50, #apps)

            for i = 1, sample do
                local app = apps[i]
                local sources = C_TransmogCollection.GetAppearanceSources(app.visualID)
                if sources and #sources > 0 then
                    local src = sources[1]
                    local armorType = ETB.BlizzardData:GetArmorType(src.itemID)
                    if armorType then
                        armorTypeCounts[armorType] = (armorTypeCounts[armorType] or 0) + 1
                    else
                        armorTypeCounts["nil"] = armorTypeCounts["nil"] + 1
                    end
                end
            end

            ETB:Print(string.format("Sample of %d items:", sample))
            ETB:Print(string.format("  Cloth: %d | Leather: %d | Mail: %d | Plate: %d | NIL: %d",
                armorTypeCounts.CLOTH, armorTypeCounts.LEATHER, armorTypeCounts.MAIL,
                armorTypeCounts.PLATE, armorTypeCounts["nil"]))

            if armorTypeCounts["nil"] > sample * 0.5 then
                ETB:Print("|cffff0000WARNING: >50% of items have nil armorType!|r")
                ETB:Print("|cffff0000This is why armor filter shows few items.|r")
                ETB:Print("|cffffd100Fix: Items need to be cached via GetItemInfo first.|r")
            end
        end,
        help = "Test armor type filter for current category",
        user = false,
    }

    CR:Register{
        name = "verify",
        handler = function()
            ETB:Print("|cff00ffff=== ITEM COUNT VERIFICATION ===|r")

            local catNames = {
                [1] = "Head", [2] = "Shoulder", [3] = "Back", [4] = "Chest", [5] = "Shirt",
                [6] = "Tabard", [7] = "Wrist", [8] = "Hands", [9] = "Waist", [10] = "Legs", [11] = "Feet"
            }

            local totalExpected = 0
            local totalLoaded = 0

            for catID = 1, 11 do
                local apps = C_TransmogCollection.GetCategoryAppearances(catID)
                local expectedSources = 0
                if apps then
                    for _, a in ipairs(apps) do
                        local sources = C_TransmogCollection.GetAppearanceSources(a.visualID)
                        if sources then
                            expectedSources = expectedSources + #sources
                        end
                    end
                end

                local loadedCount = 0
                local stats = ETB.ArmorData and ETB.ArmorData.loadStats and ETB.ArmorData.loadStats[catID]
                if stats then
                    loadedCount = stats.items or 0
                end

                local status = ""
                if loadedCount == 0 then
                    status = " |cff888888(not loaded yet)|r"
                elseif loadedCount == expectedSources then
                    status = " |cff00ff00✓|r"
                else
                    status = string.format(" |cffff0000MISMATCH! Got %d|r", loadedCount)
                end

                ETB:Print(string.format("  %s: Expected %d sources%s", catNames[catID], expectedSources, status))
                totalExpected = totalExpected + expectedSources
            end

            ETB:Print("")
            ETB:Print(string.format("|cffffd100Total Expected: %d sources|r", totalExpected))
            ETB:Print("|cff888888Run /etb clearcache then browse a category to force fresh load|r")
        end,
        help = "Verify loaded items match Blizzard API expected count",
        user = false,
    }

    CR:Register{
        name = "classtest",
        handler = function()
            ETB:Print("|cff00ffff=== CLASS FILTERING TEST (MoP Classic) ===|r")
            ETB:Print("|cff888888Note: Thrown removed, ranged slot gone, wands are main-hand|r")
            ETB:Print("")

            -- v14.8.0: tables now sourced from canonical Data/Categories/ClassData.lua.
            -- Note WEAPONS is in SET form ({AXE=true, ...}) here, so we iterate
            -- pairs() and sort the keys for display rather than table.concat'ing
            -- an array.
            local CD = ETB.ClassData
            if not CD then
                ETB:Print("|cffff5555ClassData module not loaded — cannot run classtest.|r")
                return
            end

            local classOrder = CD:GetDisplayOrder()

            for _, classID in ipairs(classOrder) do
                local armor = CD:GetPrimaryArmor(classID)
                local weaponSet = CD:GetWeapons(classID)

                local count = 0
                local weaponList = {}
                for wt in pairs(weaponSet) do
                    count = count + 1
                    weaponList[#weaponList + 1] = wt
                end
                table.sort(weaponList)

                ETB:Print(string.format("|cffffd100%s|r: %s armor, %d weapon types", classID, armor, count))
                ETB:Print(string.format("  Weapons: %s", table.concat(weaponList, ", ")))
            end

            ETB:Print("")
            ETB:Print("|cff888888Use /etb switch <name> to test filtering with different profiles|r")
        end,
        help = "Show class armor/weapon filtering matrix",
        user = false,
    }

    -- ========================================================================
    -- v12.51.0 migrations: 5 heavy fix scripts moved from legacy chain
    -- These are the largest commands but also the most self-contained — they
    -- read CommunitySetData / Blizzard APIs and write to SavedVariables. Pure
    -- transcription with no behavior changes.
    -- ========================================================================

    CR:Register{
        name = "rawapi",
        handler = function(args)
            local setID = tonumber(args[2])
            if not setID then
                ETB:Print("Usage: /etb rawapi <setID>")
                return
            end

            ETB:Print("=== RAW API TEST for set " .. setID .. " ===")

            ETB:Print("")
            ETB:Print("C_TransmogSets.GetSetSources:")
            if C_TransmogSets and C_TransmogSets.GetSetSources then
                local success, result = pcall(C_TransmogSets.GetSetSources, setID)
                ETB:Print("  pcall success: " .. tostring(success))
                ETB:Print("  result type: " .. type(result))
                if type(result) == "table" then
                    local count = 0
                    for k, v in pairs(result) do
                        count = count + 1
                        if count <= 3 then
                            ETB:Print(string.format("  [%s (%s)] = %s (%s)", tostring(k), type(k), tostring(v), type(v)))
                        end
                    end
                    ETB:Print("  total entries: " .. count)
                end
            else
                ETB:Print("  API not available")
            end

            ETB:Print("")
            ETB:Print("C_TransmogSets.GetAllSourceIDs:")
            if C_TransmogSets and C_TransmogSets.GetAllSourceIDs then
                local success, result = pcall(C_TransmogSets.GetAllSourceIDs, setID)
                ETB:Print("  pcall success: " .. tostring(success))
                ETB:Print("  result type: " .. type(result))
                if type(result) == "table" then
                    ETB:Print("  count: " .. #result)
                    for i = 1, math.min(3, #result) do
                        ETB:Print(string.format("  [%d] = %s (%s)", i, tostring(result[i]), type(result[i])))
                    end

                    if #result > 0 then
                        local testSourceID = result[1]
                        ETB:Print("")
                        ETB:Print("Testing C_TransmogCollection.GetSourceInfo(" .. testSourceID .. "):")
                        if C_TransmogCollection and C_TransmogCollection.GetSourceInfo then
                            local success2, info = pcall(C_TransmogCollection.GetSourceInfo, testSourceID)
                            ETB:Print("  pcall success: " .. tostring(success2))
                            ETB:Print("  result type: " .. type(info))
                            if type(info) == "table" then
                                for k, v in pairs(info) do
                                    ETB:Print(string.format("    %s = %s", tostring(k), tostring(v)))
                                end
                            elseif info then
                                ETB:Print("  value: " .. tostring(info))
                            end
                        else
                            ETB:Print("  GetSourceInfo not available")
                        end
                    end
                end
            else
                ETB:Print("  API not available")
            end

            ETB:Print("")
            ETB:Print("C_TransmogSets.GetSetPrimaryAppearances:")
            if C_TransmogSets and C_TransmogSets.GetSetPrimaryAppearances then
                local success, result = pcall(C_TransmogSets.GetSetPrimaryAppearances, setID)
                ETB:Print("  pcall success: " .. tostring(success))
                ETB:Print("  result type: " .. type(result))
                if type(result) == "table" then
                    ETB:Print("  count: " .. #result)
                    for i = 1, math.min(3, #result) do
                        local v = result[i]
                        if type(v) == "table" then
                            ETB:Print(string.format("  [%d] = table with keys:", i))
                            for k2, v2 in pairs(v) do
                                ETB:Print(string.format("    %s = %s", tostring(k2), tostring(v2)))
                            end
                        else
                            ETB:Print(string.format("  [%d] = %s (%s)", i, tostring(v), type(v)))
                        end
                    end
                end
            else
                ETB:Print("  API not available")
            end
        end,
        help = "Raw C_TransmogSets API test for a specific set ID",
        user = false,
    }

    CR:Register{
        name = "pipelineaudit",
        handler = function()
            ETB:Print("|cff00ffff=== ETB DATA PIPELINE AUDIT ===|r")
            ETB:Print("")

            ETB:Print("|cffffd100[1] PROFILE SYSTEM|r")
            if ETB.ProfileManager then
                local profile = ETB.ProfileManager:GetActiveProfile()
                local currentProfile = ETB.ProfileManager:GetCurrentProfile()
                ETB:Print(string.format("  Current Character: %s (%s)",
                    currentProfile and currentProfile.name or "?",
                    currentProfile and currentProfile.classID or "?"))
                ETB:Print(string.format("  Active Profile: %s (%s)",
                    profile and profile.name or "?",
                    profile and profile.classID or "?"))
                ETB:Print(string.format("  Total Profiles: %d", ETB.ProfileManager:GetProfileCount()))

                local allProfiles = ETB.ProfileManager:GetAllProfiles()
                for _, p in ipairs(allProfiles) do
                    local marker = p.isActive and "|cff00ff00►|r" or " "
                    local current = p.isCurrent and " (current)" or ""
                    ETB:Print(string.format("    %s %s - %s%s", marker, p.name, p.classID, current))
                end
            else
                ETB:Print("  |cffff0000ProfileManager not loaded!|r")
            end
            ETB:Print("")

            ETB:Print("|cffffd100[2] CLASS FILTERING|r")
            if ETB.BlizzardData then
                local activeClass, activeName = ETB.BlizzardData:GetActiveClassName()
                local primaryArmor = ETB.BlizzardData:GetPlayerPrimaryArmor()
                local weaponTypes = ETB.BlizzardData:GetPlayerWeaponTypes()

                ETB:Print(string.format("  Active Class: %s (%s)", tostring(activeClass), tostring(activeName)))
                ETB:Print(string.format("  Primary Armor: %s", tostring(primaryArmor)))

                local weaponCount = 0
                local weaponList = {}
                for wt, _ in pairs(weaponTypes) do
                    weaponCount = weaponCount + 1
                    table.insert(weaponList, wt)
                end
                table.sort(weaponList)
                ETB:Print(string.format("  Usable Weapons (%d): %s", weaponCount, table.concat(weaponList, ", ")))
            else
                ETB:Print("  |cffff0000BlizzardData not loaded!|r")
            end
            ETB:Print("")

            ETB:Print("|cffffd100[3] CATEGORY DATA|r")
            local armorCats, weaponCats = 0, 0
            local totalAppearances, totalCollected = 0, 0
            for catID, catInfo in pairs(ETB.CATEGORY_INFO) do
                if catInfo.isArmor then armorCats = armorCats + 1
                elseif catInfo.isWeapon then weaponCats = weaponCats + 1 end

                if C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances then
                    local success, apps = pcall(C_TransmogCollection.GetCategoryAppearances, catID)
                    if success and apps then
                        totalAppearances = totalAppearances + #apps
                        for _, app in ipairs(apps) do
                            if app.isCollected then totalCollected = totalCollected + 1 end
                        end
                    end
                end
            end
            ETB:Print(string.format("  Armor Categories: %d", armorCats))
            ETB:Print(string.format("  Weapon Categories: %d", weaponCats))
            ETB:Print(string.format("  Total Appearances: %s", ETB:FormatNumber(totalAppearances)))
            ETB:Print(string.format("  Total Collected: %s (%.1f%%)",
                ETB:FormatNumber(totalCollected),
                totalAppearances > 0 and (totalCollected / totalAppearances * 100) or 0))
            ETB:Print("")

            ETB:Print("|cffffd100[4] SAMPLE DATA CHECK|r")
            if ETB.BlizzardData then
                local headItems = ETB.BlizzardData:GetItemsForCategory(1, { limit = 10, onlyUsable = true })
                local primaryArmor = ETB.BlizzardData:GetPlayerPrimaryArmor()
                local headValid = 0
                for _, item in ipairs(headItems) do
                    if item.armorType == primaryArmor or item.armorType == nil then
                        headValid = headValid + 1
                    end
                end
                ETB:Print(string.format("  Head (cat 1): %d items, %d match %s", #headItems, headValid, primaryArmor))

                local swordItems = ETB.BlizzardData:GetItemsForCategory(14, { limit = 10 })
                local swordValid = 0
                local swordTypes = {}
                for _, item in ipairs(swordItems) do
                    local wt = item.weaponType or "nil"
                    swordTypes[wt] = (swordTypes[wt] or 0) + 1
                    if wt == "SWORD" or wt == nil then swordValid = swordValid + 1 end
                end
                local typeStr = ""
                for wt, count in pairs(swordTypes) do
                    typeStr = typeStr .. wt .. ":" .. count .. " "
                end
                ETB:Print(string.format("  1H Swords (cat 14): %d items, types: %s", #swordItems, typeStr))
            end
            ETB:Print("")

            ETB:Print("|cffffd100[5] FILTER SYSTEM|r")
            if ETB.Filtering then
                ETB:Print(string.format("  Category: %d", ETB.Filtering.filters.category or 0))
                ETB:Print(string.format("  Collected Filter: %s", ETB.Filtering.filters.collected or "all"))
                ETB:Print(string.format("  My Class Only: %s", tostring(ETB.Filtering.filters.myClassOnly)))
                ETB:Print(string.format("  Armor Type: %s", ETB.Filtering.filters.armorType or "all"))
                ETB:Print(string.format("  Weapon Type: %s", ETB.Filtering.filters.weaponType or "all"))
            else
                ETB:Print("  |cffff0000Filtering not loaded!|r")
            end
            ETB:Print("")

            ETB:Print("|cffffd100[6] SETS DATA|r")
            if ETB.SetsData then
                local stats = ETB.SetsData:GetStats()
                ETB:Print(string.format("  Total Sets: %d", stats.total or 0))
                ETB:Print(string.format("  Collected Sets: %d", stats.collected or 0))
                ETB:Print(string.format("  Usable Sets: %d", stats.usable or 0))
            else
                ETB:Print("  |cffff0000SetsData not loaded!|r")
            end

            ETB:Print("")
            ETB:Print("|cff00ff00Audit complete!|r")
        end,
        help = "Comprehensive ETB data pipeline audit (legacy — use /etb audit instead)",
        user = false,
    }

    CR:Register{
        name = "fixitems",
        handler = function()
            print("|cff00ff00[ETB FixItems]|r Scanning all community set items...")

            local rawData = ETB.CommunitySetData
            if not rawData then
                print("|cffff4444[ETB]|r CommunitySetData not loaded")
                return
            end

            local total = 0
            local valid = 0
            local fixed = 0
            local unfixable = 0
            local fixMap = {}
            local unfixList = {}

            for i, raw in ipairs(rawData) do
                local name = raw[1]
                local rawItems = raw[5]
                local setFixes = nil

                for invSlot, itemData in pairs(rawItems) do
                    local itemID = itemData[1]
                    local visualID = itemData[2]
                    total = total + 1

                    -- v14.7.0: was 'iName' but first GetItemInfoInstant return on
                    -- MoP 5.4.8 is the numeric itemID, used here only as a truthy
                    -- "is this item known to the client" check. Renamed for clarity.
                    local itemKnown = GetItemInfoInstant and GetItemInfoInstant(itemID)
                    if itemKnown then
                        valid = valid + 1
                    else
                        local replacement = nil

                        if visualID and visualID > 0 and C_TransmogCollection
                           and C_TransmogCollection.GetAppearanceSources then
                            local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, visualID)
                            if ok and sources then
                                for _, src in ipairs(sources) do
                                    local sourceID = type(src) == "table" and src.sourceID or src
                                    if sourceID then
                                        local srcOK, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
                                        if srcOK and srcInfo and srcInfo.itemID and srcInfo.itemID > 0 then
                                            -- v14.7.0: same first-return fix as elsewhere
                                            local rID, _, _, _, rIcon = GetItemInfoInstant(srcInfo.itemID)
                                            if rID then
                                                local realName = GetItemInfo and GetItemInfo(srcInfo.itemID)
                                                replacement = {
                                                    itemID = srcInfo.itemID,
                                                    visualID = srcInfo.visualID or visualID,
                                                    name = realName or ("Item " .. srcInfo.itemID),
                                                    icon = rIcon,
                                                    sourceID = sourceID,
                                                }
                                                break
                                            end
                                        end
                                    end
                                end
                            end
                        end

                        if replacement then
                            fixed = fixed + 1
                            if not setFixes then setFixes = {} end
                            setFixes[invSlot] = replacement
                        else
                            unfixable = unfixable + 1
                            table.insert(unfixList, {
                                setIndex = i,
                                setName = name,
                                slot = invSlot,
                                itemID = itemID,
                                visualID = visualID,
                            })
                        end
                    end
                end

                if setFixes then
                    fixMap[i] = setFixes
                end
            end

            ETB_FixItemsMap = fixMap
            ETB_FixItemsUnfixable = unfixList

            print("|cff00ff00[ETB FixItems]|r Complete!")
            print(string.format("  Total items scanned: %d", total))
            print(string.format("  |cff00ff00Already valid: %d|r", valid))
            print(string.format("  |cff00ff00Fixed via visualID: %d|r (MoP replacement found)", fixed))
            print(string.format("  |cffff4444Unfixable: %d|r (no MoP item shares this appearance)", unfixable))

            local fixSetCount = 0
            for _ in pairs(fixMap) do fixSetCount = fixSetCount + 1 end
            print(string.format("  Sets with fixes: %d", fixSetCount))

            if #unfixList > 0 then
                print("|cffff8800Unfixable items (first 20):|r")
                for idx, uf in ipairs(unfixList) do
                    if idx > 20 then
                        print("  ... and " .. (#unfixList - 20) .. " more")
                        break
                    end
                    print(string.format("  [%d] %s slot %d: itemID=%d visID=%d",
                        uf.setIndex, uf.setName, uf.slot, uf.itemID, uf.visualID))
                end
            end

            local shown = 0
            if fixSetCount > 0 then
                print("|cff00ff00Example fixes:|r")
                for dataIdx, fixes in pairs(fixMap) do
                    if shown >= 10 then break end
                    local setName = rawData[dataIdx] and rawData[dataIdx][1] or "?"
                    for slot, fix in pairs(fixes) do
                        local origID = rawData[dataIdx][5][slot] and rawData[dataIdx][5][slot][1] or 0
                        print(string.format("  %s [slot %d]: %d -> %d (%s)",
                            setName, slot, origID, fix.itemID, fix.name))
                        shown = shown + 1
                        if shown >= 10 then break end
                    end
                end
            end

            print("|cffffd100Saved to ETB_FixItemsMap and ETB_FixItemsUnfixable|r")
            print("|cffffd100/reload then share WTF/.../EnhancedTransmogBrowser.lua|r")
        end,
        help = "Scan community set items, fix broken items via visualID lookup",
        user = false,
    }

    CR:Register{
        name = "deepfix",
        handler = function()
            print("|cff00ff00[ETB DeepFix v2]|r Phase 1: Indexing ALL Blizzard transmog sets...")

            if not C_TransmogSets or not C_TransmogSets.GetAllSets then
                print("|cffff4444[ETB]|r C_TransmogSets API not available")
                return
            end

            local rawData = ETB.CommunitySetData
            if not rawData then
                print("|cffff4444[ETB]|r CommunitySetData not loaded")
                return
            end

            local INVTYPE_TO_SLOT = {
                INVTYPE_HEAD = 1, INVTYPE_SHOULDER = 3, INVTYPE_CHEST = 5,
                INVTYPE_ROBE = 5, INVTYPE_WAIST = 6, INVTYPE_LEGS = 7,
                INVTYPE_FEET = 8, INVTYPE_WRIST = 9, INVTYPE_HAND = 10,
            }
            local SLOT_NAMES = {[1]="Head",[3]="Shoulder",[5]="Chest",[6]="Waist",
                               [7]="Legs",[8]="Feet",[9]="Wrist",[10]="Hands"}

            local blizzByName = {}
            local blizzSetCount = 0
            local blizzItemCount = 0

            local allSets = C_TransmogSets.GetAllSets()
            for _, setInfo in ipairs(allSets) do
                local setID = setInfo.setID
                local setName = setInfo.name or ""
                local items = {}
                local itemCount = 0

                local ok, appearances = pcall(C_TransmogSets.GetSetPrimaryAppearances, setID)
                if ok and appearances then
                    for _, app in ipairs(appearances) do
                        local appID = type(app) == "table" and app.appearanceID or app
                        if appID then
                            local srcOK, sources = pcall(C_TransmogCollection.GetAppearanceSources, appID)
                            if srcOK and sources then
                                for _, src in ipairs(sources) do
                                    local sourceID = type(src) == "table" and src.sourceID or src
                                    if sourceID then
                                        local infoOK, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
                                        if infoOK and srcInfo and srcInfo.itemID and srcInfo.itemID > 0 then
                                            -- v14.7.0 fix: GetItemInfoInstant on MoP 5.4.8 returns
                                            --   (itemID, itemType, itemSubType, equipLoc, icon, classID, subclassID)
                                            -- = 7 fields, not 9. Earlier code read field 9 as
                                            -- "invTypeStr" (always nil) so this whole block was dead code,
                                            -- and stored the numeric itemID as the "name" field. Use field
                                            -- 4 (equipLoc) for INVTYPE matching, and GetItemInfo for the
                                            -- real string name.
                                            local _, _, _, equipLoc, iIcon = GetItemInfoInstant(srcInfo.itemID)
                                            local iName = GetItemInfo and GetItemInfo(srcInfo.itemID)
                                            if equipLoc then
                                                local slot = INVTYPE_TO_SLOT[equipLoc]
                                                if slot and not items[slot] then
                                                    items[slot] = {
                                                        itemID = srcInfo.itemID,
                                                        visualID = srcInfo.visualID or appID,
                                                        sourceID = sourceID,
                                                        name = iName or ("Item " .. srcInfo.itemID),
                                                        icon = iIcon,
                                                    }
                                                    itemCount = itemCount + 1
                                                end
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end

                if itemCount == 0 then
                    local srcOK2, sourceIDs = pcall(C_TransmogSets.GetAllSourceIDs, setID)
                    if srcOK2 and sourceIDs then
                        for _, sourceID in ipairs(sourceIDs) do
                            local infoOK, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
                            if infoOK and srcInfo and srcInfo.itemID and srcInfo.itemID > 0 then
                                -- v14.7.0 fix: same as above — equipLoc is field 4, not field 9
                                local _, _, _, equipLoc, iIcon = GetItemInfoInstant(srcInfo.itemID)
                                local iName = GetItemInfo and GetItemInfo(srcInfo.itemID)
                                if equipLoc then
                                    local slot = INVTYPE_TO_SLOT[equipLoc]
                                    if slot and not items[slot] then
                                        items[slot] = {
                                            itemID = srcInfo.itemID,
                                            visualID = srcInfo.visualID or 0,
                                            sourceID = sourceID,
                                            name = iName or ("Item " .. srcInfo.itemID),
                                            icon = iIcon,
                                        }
                                        itemCount = itemCount + 1
                                    end
                                end
                            end
                        end
                    end
                end

                if itemCount > 0 then
                    local nameNorm = setName:lower():gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
                    local nameNoRecolor = nameNorm:gsub("%s*%(recolor%)%s*", "")

                    blizzByName[nameNorm] = blizzByName[nameNorm] or {}
                    table.insert(blizzByName[nameNorm], { setID = setID, name = setName, items = items })

                    if nameNoRecolor ~= nameNorm then
                        blizzByName[nameNoRecolor] = blizzByName[nameNoRecolor] or {}
                        table.insert(blizzByName[nameNoRecolor], { setID = setID, name = setName, items = items })
                    end

                    blizzSetCount = blizzSetCount + 1
                    blizzItemCount = blizzItemCount + itemCount
                end
            end

            print(string.format("|cff00ff00[ETB DeepFix v2]|r Indexed %d Blizzard sets (%d items)",
                blizzSetCount, blizzItemCount))

            print("|cff00ff00[ETB DeepFix v2]|r Phase 2: Matching community sets by name...")

            local deepFixMap = {}
            local deepUnfixable = {}
            local totalBroken = 0
            local fixedByName = 0
            local fixedByVisualID = 0
            local stillBroken = 0
            local nameMatched = 0

            for i, raw in ipairs(rawData) do
                local setName = raw[1]
                local rawItems = raw[5]

                local brokenSlots = {}
                local hasBroken = false

                for invSlot, itemData in pairs(rawItems) do
                    local itemID = itemData[1]
                    -- v14.7.0: rename for clarity — was 'iName' but it's actually
                    -- the numeric itemID truthy-check (MoP 5.4.8 GetItemInfoInstant
                    -- shape). Used purely to detect items unknown to the client.
                    local itemKnown = GetItemInfoInstant and GetItemInfoInstant(itemID)
                    if not itemKnown then
                        brokenSlots[invSlot] = { itemID = itemID, visualID = itemData[2] }
                        totalBroken = totalBroken + 1
                        hasBroken = true
                    end
                end

                if hasBroken then
                    local setFixes = {}

                    local nameNorm = setName:lower():gsub("\\.", ""):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
                    local nameNoRecolor = nameNorm:gsub("%s*%(recolor%)%s*", "")

                    local matches = blizzByName[nameNorm] or blizzByName[nameNoRecolor] or {}

                    if #matches > 0 then
                        nameMatched = nameMatched + 1

                        local bestMatch = nil
                        local bestCover = 0

                        for _, match in ipairs(matches) do
                            local cover = 0
                            for brokenSlot in pairs(brokenSlots) do
                                if match.items[brokenSlot] then
                                    cover = cover + 1
                                end
                            end
                            if cover > bestCover then
                                bestCover = cover
                                bestMatch = match
                            end
                        end

                        if bestMatch and bestCover > 0 then
                            for brokenSlot, brokenData in pairs(brokenSlots) do
                                local blizzItem = bestMatch.items[brokenSlot]
                                if blizzItem then
                                    setFixes[brokenSlot] = {
                                        itemID = blizzItem.itemID,
                                        visualID = blizzItem.visualID,
                                        name = blizzItem.name,
                                        method = "name:" .. bestMatch.setID .. ":" .. bestMatch.name,
                                    }
                                    fixedByName = fixedByName + 1
                                end
                            end
                        end
                    end

                    for brokenSlot, brokenData in pairs(brokenSlots) do
                        if not setFixes[brokenSlot] then
                            local visualID = brokenData.visualID
                            if visualID and visualID > 0 and C_TransmogCollection.GetAppearanceSources then
                                local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, visualID)
                                if ok and sources then
                                    for _, src in ipairs(sources) do
                                        local sourceID = type(src) == "table" and src.sourceID or src
                                        if sourceID then
                                            local srcOK, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
                                            if srcOK and srcInfo and srcInfo.itemID and srcInfo.itemID > 0 then
                                                -- v14.7.0: first return is itemID; use truthy check + GetItemInfo for real name
                                                local rID = GetItemInfoInstant(srcInfo.itemID)
                                                if rID then
                                                    local realName = GetItemInfo and GetItemInfo(srcInfo.itemID)
                                                    setFixes[brokenSlot] = {
                                                        itemID = srcInfo.itemID,
                                                        visualID = srcInfo.visualID or visualID,
                                                        name = realName or ("Item " .. srcInfo.itemID),
                                                        method = "visualID:" .. visualID,
                                                    }
                                                    fixedByVisualID = fixedByVisualID + 1
                                                    break
                                                end
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end

                    for brokenSlot, brokenData in pairs(brokenSlots) do
                        if not setFixes[brokenSlot] then
                            stillBroken = stillBroken + 1
                            table.insert(deepUnfixable, {
                                setIndex = i,
                                setName = setName,
                                slot = brokenSlot,
                                slotName = SLOT_NAMES[brokenSlot] or "?",
                                itemID = brokenData.itemID,
                                visualID = brokenData.visualID,
                            })
                        end
                    end

                    if next(setFixes) then
                        deepFixMap[i] = {
                            setName = setName,
                            fixes = setFixes,
                        }
                    end
                end
            end

            ETB_DeepFixMap = deepFixMap
            ETB_DeepFixUnfixable = deepUnfixable

            print("|cff00ff00[ETB DeepFix v2]|r === RESULTS ===")
            print(string.format("  Total broken items: %d", totalBroken))
            print(string.format("  Community sets matched by name: %d", nameMatched))
            print(string.format("  |cff00ff00Fixed by Blizzard name match: %d|r", fixedByName))
            print(string.format("  |cff00ff00Fixed by visualID lookup: %d|r", fixedByVisualID))
            print(string.format("  |cff00ff00Total fixed: %d|r", fixedByName + fixedByVisualID))
            print(string.format("  |cffff4444Still unfixable: %d|r", stillBroken))

            local shown = 0
            if fixedByName > 0 then
                print("|cff00ff00Name-match fixes (first 20):|r")
                for dataIdx, entry in pairs(deepFixMap) do
                    if shown >= 20 then break end
                    for slot, fix in pairs(entry.fixes) do
                        if fix.method and fix.method:find("^name:") then
                            local origID = rawData[dataIdx][5][slot] and rawData[dataIdx][5][slot][1] or 0
                            print(string.format("  [%d] %s slot %d (%s): %d -> %d (%s)",
                                dataIdx, entry.setName, slot, SLOT_NAMES[slot] or "?",
                                origID, fix.itemID, fix.name or "?"))
                            shown = shown + 1
                            if shown >= 20 then break end
                        end
                    end
                end
            end

            if #deepUnfixable > 0 then
                print(string.format("|cffff8800Still unfixable (%d, first 15):|r", #deepUnfixable))
                for idx, uf in ipairs(deepUnfixable) do
                    if idx > 15 then
                        print("  ... and " .. (#deepUnfixable - 15) .. " more")
                        break
                    end
                    print(string.format("  [%d] %s %s: item=%d vis=%d",
                        uf.setIndex, uf.setName, uf.slotName, uf.itemID, uf.visualID))
                end
            end

            print("|cffffd100Saved to ETB_DeepFixMap and ETB_DeepFixUnfixable|r")
            print("|cffffd100/reload then share WTF/.../EnhancedTransmogBrowser.lua|r")
        end,
        help = "Deep fix: name-match community sets to Blizzard transmog sets",
        user = false,
    }

    CR:Register{
        name = "fixsets", aliases = {"scansets"},
        handler = function()
            if not C_TransmogSets or not C_TransmogSets.GetAllSets then
                print("|cffff4444[ETB ScanSets]|r C_TransmogSets.GetAllSets not available")
                return
            end

            print("|cff00ff00[ETB ScanSets]|r Scanning Blizzard transmog sets...")

            local allSetData = C_TransmogSets.GetAllSets() or {}
            if #allSetData == 0 then
                print("|cffff4444[ETB ScanSets]|r GetAllSets returned empty")
                return
            end

            print(string.format("|cff00ff00[ETB]|r GetAllSets returned %d sets", #allSetData))

            local blizzByName = {}
            local blizzSetCount = 0
            local resolveErrors = 0

            for _, setInfo in ipairs(allSetData) do
                local setID = setInfo.setID
                local setName = setInfo.name
                if setID and setName then
                    local items = {}
                    local itemCount = 0

                    local appearances = nil
                    if C_TransmogSets.GetSetPrimaryAppearances then
                        local ok, result = pcall(C_TransmogSets.GetSetPrimaryAppearances, setID)
                        if ok and result then appearances = result end
                    end

                    if not appearances or #appearances == 0 then
                        if C_TransmogSets.GetAllSourceIDs then
                            local ok, sourceIDs = pcall(C_TransmogSets.GetAllSourceIDs, setID)
                            if ok and sourceIDs then
                                appearances = {}
                                for _, sid in ipairs(sourceIDs) do
                                    table.insert(appearances, { sourceID = sid })
                                end
                            end
                        end
                    end

                    if appearances then
                        for _, appInfo in ipairs(appearances) do
                            local sourceID = appInfo.sourceID or appInfo.appearanceID
                            if sourceID and sourceID > 0 then
                                local srcOK, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
                                if srcOK and srcInfo and srcInfo.itemID and srcInfo.itemID > 0 then
                                    local invSlot = nil
                                    if srcInfo.invType then
                                        local invTypeMap = {
                                            INVTYPE_HEAD=1, INVTYPE_SHOULDER=3, INVTYPE_CHEST=5,
                                            INVTYPE_ROBE=5, INVTYPE_WAIST=6, INVTYPE_LEGS=7,
                                            INVTYPE_FEET=8, INVTYPE_WRIST=9, INVTYPE_HAND=10,
                                            INVTYPE_CLOAK=15, INVTYPE_BODY=4, INVTYPE_TABARD=19,
                                        }
                                        invSlot = invTypeMap[srcInfo.invType]
                                    end
                                    if not invSlot and srcInfo.categoryID then
                                        local catToSlot = {
                                            [1]=1, [2]=3, [3]=15, [4]=5, [5]=4, [6]=19,
                                            [7]=9, [8]=10, [9]=6, [10]=7, [11]=8,
                                        }
                                        invSlot = catToSlot[srcInfo.categoryID]
                                    end
                                    if not invSlot then
                                        local _, _, _, invTypeStr = GetItemInfoInstant(srcInfo.itemID)
                                        if invTypeStr then
                                            local fallbackMap = {
                                                INVTYPE_HEAD=1, INVTYPE_SHOULDER=3, INVTYPE_CHEST=5,
                                                INVTYPE_ROBE=5, INVTYPE_WAIST=6, INVTYPE_LEGS=7,
                                                INVTYPE_FEET=8, INVTYPE_WRIST=9, INVTYPE_HAND=10,
                                                INVTYPE_CLOAK=15, INVTYPE_BODY=4, INVTYPE_TABARD=19,
                                            }
                                            invSlot = fallbackMap[invTypeStr]
                                        end
                                    end
                                    if invSlot then
                                        items[invSlot] = {
                                            itemID = srcInfo.itemID,
                                            sourceID = sourceID,
                                            visualID = srcInfo.visualID or 0,
                                        }
                                        itemCount = itemCount + 1
                                    end
                                else
                                    resolveErrors = resolveErrors + 1
                                end
                            end
                        end
                    end

                    if itemCount > 0 then
                        if not blizzByName[setName] then
                            blizzByName[setName] = {}
                        end
                        table.insert(blizzByName[setName], {
                            setID = setID,
                            items = items,
                            itemCount = itemCount,
                            label = setInfo.label or "",
                        })
                        blizzSetCount = blizzSetCount + 1
                    end
                end
            end

            local uniqueNames = 0
            for _ in pairs(blizzByName) do uniqueNames = uniqueNames + 1 end
            print(string.format("|cff00ff00[ETB]|r Indexed %d Blizzard sets (%d unique names, %d resolve errors)", blizzSetCount, uniqueNames, resolveErrors))

            local exportData = {}
            local matched = 0
            local unmatched = 0
            local slotsAdded = 0
            local slotsReplaced = 0
            local unmatchedNames = {}

            local rawData = ETB.CommunitySetData
            if not rawData then
                print("|cffff4444[ETB]|r CommunitySetData not found")
                return
            end

            for i, raw in ipairs(rawData) do
                local name = raw[1]
                local armorType = raw[2]
                local expansion = raw[3]
                local filterType = raw[4]
                local rawItems = raw[5]

                local matchName = name:gsub("\\", ""):gsub("\\'", "'")

                local blizzMatches = blizzByName[matchName]

                if not blizzMatches then
                    local baseName = matchName:gsub(" %(Recolor%)$", "")
                    blizzMatches = blizzByName[baseName]
                end

                if blizzMatches then
                    local bestMatch = nil
                    local bestOverlap = -1

                    for _, bm in ipairs(blizzMatches) do
                        local overlap = 0
                        for invSlot, _ in pairs(rawItems) do
                            if bm.items[invSlot] then
                                overlap = overlap + 1
                            end
                        end
                        if overlap > bestOverlap then
                            bestOverlap = overlap
                            bestMatch = bm
                        end
                    end

                    if bestMatch then
                        matched = matched + 1
                        local mergedItems = {}

                        for invSlot, rawItem in pairs(rawItems) do
                            local itemID = rawItem[1]
                            local visualID = rawItem[2]
                            local existsInClient = false
                            if GetItemInfoInstant then
                                local n = GetItemInfoInstant(itemID)
                                if n then existsInClient = true end
                            end

                            if existsInClient then
                                mergedItems[invSlot] = { itemID = itemID, visualID = visualID }
                            elseif bestMatch.items[invSlot] then
                                local bi = bestMatch.items[invSlot]
                                mergedItems[invSlot] = { itemID = bi.itemID, visualID = bi.visualID }
                                slotsReplaced = slotsReplaced + 1
                            end
                        end

                        for invSlot, bi in pairs(bestMatch.items) do
                            if not mergedItems[invSlot] and not rawItems[invSlot] then
                                mergedItems[invSlot] = { itemID = bi.itemID, visualID = bi.visualID }
                                slotsAdded = slotsAdded + 1
                            end
                        end

                        exportData[i] = {
                            name = name,
                            armorType = armorType,
                            expansion = expansion,
                            filterType = filterType,
                            items = mergedItems,
                            blizzSetID = bestMatch.setID,
                        }
                    end
                else
                    unmatched = unmatched + 1
                    local hasBadItems = false
                    for _, rawItem in pairs(rawItems) do
                        if GetItemInfoInstant and not GetItemInfoInstant(rawItem[1]) then
                            hasBadItems = true
                            break
                        end
                    end
                    if hasBadItems then
                        table.insert(unmatchedNames, matchName or name)
                    end
                end
            end

            ETB_SetScanResults = exportData
            ETB_SetScanUnmatched = unmatchedNames

            print("|cff00ff00[ETB ScanSets]|r Complete!")
            print(string.format("  Blizzard sets indexed: %d", blizzSetCount))
            print(string.format("  Community sets matched: |cff00ff00%d|r", matched))
            print(string.format("  Slots replaced (bad item -> correct): |cff00ff00%d|r", slotsReplaced))
            print(string.format("  Slots added from Blizzard data: |cff00ff00%d|r", slotsAdded))
            print(string.format("  Unmatched community sets: %d", unmatched))
            if #unmatchedNames > 0 then
                print("|cffff8800Unmatched sets with bad items:|r")
                for idx, uName in ipairs(unmatchedNames) do
                    if idx <= 20 then
                        print("  - " .. uName)
                    elseif idx == 21 then
                        print("  ... and " .. (#unmatchedNames - 20) .. " more")
                    end
                end
            end
            print("|cffffd100Data saved to ETB_SetScanResults and ETB_SetScanUnmatched|r")
            print("|cffffd100/reload then share WTF/.../EnhancedTransmogBrowser.lua|r")
        end,
        help = "Scan Blizzard sets and match community sets by name",
        user = false,
    }

end

