--[[
============================================================================
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 ""
    
    -- User-facing commands (always available)
    local USER_COMMANDS = {
        [""] = true, show = true, open = true, hide = true, close = true,
        toggle = true, transmogui = true, tui = true, apply = true,
        help = true, favorites = true, fav = true, minimap = true,
        debug = true, reset = true, profiles = true, profile = true,
        outfits = true, macro = true, stats = true, farm = true, wishlist = true, audit = true, version = true, ver = true, npcnames = true,
    }
    
    -- Gate developer/diagnostic commands behind debug mode
    if not USER_COMMANDS[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 cmd == "" or cmd == "show" or cmd == "open" then
        ETB.MainFrame:Show()
    
    elseif cmd == "version" or cmd == "ver" then
        ETB:Print("Build: v12.37.0 — March 21, 2026 source mapping + NPC names")
        
    elseif cmd == "hide" or cmd == "close" then
        ETB.MainFrame:Hide()
        
    elseif cmd == "toggle" then
        ETB.MainFrame:Toggle()
        
    elseif cmd == "transmogui" or cmd == "tui" or cmd == "apply" then
        -- Open the Transmog Application UI (shows only collected items)
        if ETB.TransmogUI then
            ETB.TransmogUI:Toggle()
        else
            ETB:Print("TransmogUI not loaded!")
        end
        
    elseif cmd == "stats" then
        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
        
    elseif cmd == "farm" then
        if ETB.WishlistPanel then
            ETB.WishlistPanel:Show()
            ETB.WishlistPanel:ShowTab("farm")
        else
            ETB:Print("Wishlist panel not loaded.")
        end
        
    elseif cmd == "wishlist" then
        if ETB.WishlistPanel then
            ETB.WishlistPanel:Toggle()
        else
            ETB:Print("Wishlist panel not loaded.")
        end
        
    elseif cmd == "profiles" or cmd == "profile" then
        -- Profile system commands
        if ETB.ProfileManager then
            ETB.ProfileManager:PrintStats()
        else
            ETB:Print("ProfileManager not loaded!")
        end
        
    elseif cmd == "switch" then
        -- Switch active profile: /etb switch CharacterName
        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
        
    elseif cmd == "outfits" then
        -- List saved outfits
        if ETB.ProfileManager then
            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
        
    elseif cmd == "memory" or cmd == "mem" then
        local mem = ETB:GetMemoryUsage()
        ETB:Print("Memory: " .. ETB:FormatMemory(mem))
        ETB.BlizzardData:PrintStats()
        
        local pool = ETB.ItemRow:GetPoolStats()
        ETB:Print(string.format("Item rows: %d active, %d pooled", pool.active, pool.pool))
        
    elseif cmd == "cache" then
        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
        
    elseif cmd == "clearcache" then
        ETB.BlizzardData:ClearCache()
        
    elseif cmd == "sources" then
        ETB:Print("|cff00ffff=== SOURCE DATA (MapperBridge) ===|r")
        
        -- MapperBridge stats
        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))
            
            -- Expansion breakdown
            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
            
            -- World bosses
            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
        
        -- SourceMapper runtime stats
        if ETB.SourceMapper then
            ETB:Print("")
            ETB:Print("|cffffd100SourceMapper (Runtime):|r")
            ETB.SourceMapper:PrintStats()
        end
        
        -- InstanceData (via MapperBridge)
        if ETB.InstanceData then
            ETB:Print("")
            ETB.InstanceData:PrintStats()
        end
        
    elseif cmd == "sourcepanel" or cmd == "sp" then
        -- Open the Sources Summary Panel for Live Preview items
        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
        
    elseif cmd == "sourcetest" then
        -- Test source mapping for an item
        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 (15K+ items from enriched source data)
        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
        
        -- Also show Blizzard API data if available
        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
        
    elseif cmd == "diagnose" or cmd == "diag" then
        -- Basic diagnostic info
        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
        -- ERB status
        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
        
    elseif cmd == "instance" then
        -- Test instance/encounter lookup
        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 ETB.InstanceData then
            local instance = ETB.InstanceData:GetInstance(instanceID)
            if instance then
                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))
                
                -- List encounters
                local encounters = ETB.InstanceData:GetInstanceEncounters(instanceID)
                if #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
            else
                ETB:Print("Instance not found: " .. instanceID)
            end
        else
            ETB:Print("InstanceData not loaded!")
        end
        
    elseif cmd == "favorites" or cmd == "fav" then
        ETB.FavoritesManager:PrintStats()
        
    elseif cmd == "minimap" then
        ETB.MinimapButton:Toggle()
        
    elseif cmd == "macro" then
        ETB:CreateMacro()
        
    elseif cmd == "tooltips" then
        ETB.TooltipEnhancer:Toggle()
        
    elseif cmd == "debug" then
        ETB.debug = not ETB.debug
        ETB:Print("Debug: " .. (ETB.debug and "|cff00ff00ON|r" or "|cffff0000OFF|r"))
        
    elseif cmd == "api" then
        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
        
    elseif cmd == "weapontest" then
        -- Test weapon type detection
        local itemID = tonumber(args[2])
        if itemID then
            -- Test specific item
            ETB:Print("=== WEAPON TYPE TEST: Item " .. itemID .. " ===")
            
            -- Test GetItemInfoInstant
            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
            
            -- Test GetItemInfo
            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)))
            
            -- Test GetWeaponType
            local wt, is2h = ETB.BlizzardData:GetWeaponType(itemID)
            ETB:Print(string.format("GetWeaponType: type=%s, isTwoHanded=%s", 
                tostring(wt), tostring(is2h)))
                
            -- Check cache
            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
            -- Test with known weapon itemIDs
            ETB:Print("=== WEAPON TYPE TEST (sample items) ===")
            local testItems = {25200, 49500, 61100, 82100, 102600}  -- From debug logs
            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
        
    elseif cmd == "categories" or cmd == "cats" then
        -- Discover actual MoP Classic transmog categories
        ETB:Print("=== DISCOVERING TRANSMOG CATEGORIES ===")
        
        if not C_TransmogCollection or not C_TransmogCollection.GetCategoryAppearances then
            ETB:Print("|cffff0000C_TransmogCollection not available!|r")
            return
        end
        
        -- Check categories 1-35 (expanded range to find all weapon types)
        for catID = 1, 35 do
            local success, appearances = pcall(C_TransmogCollection.GetCategoryAppearances, catID)
            if success and appearances and #appearances > 0 then
                -- Get first item to see what type it is
                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
                
                -- Get item info for first source
                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
                
                -- Check our CATEGORY_INFO
                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!")
        
    elseif cmd == "inspect" then
        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
        
    elseif cmd == "reset" then
        if args[2] == "confirm" then
            ETB.Settings:Reset()
            ETB.FavoritesManager:ClearAll()
            ETB.BlizzardData:ClearCache()
            ETB:Print("All data reset!")
        else
            ETB:Print("Type |cffffd100/etb reset confirm|r to reset all data")
        end
        
    elseif cmd == "shipinfo" then
        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")
        
        -- Show current data status
        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
        
    elseif cmd == "purgedata" then
        if args[2] == "confirm" then
            -- Clear any cached data in MapperBridge
            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
        
    elseif cmd == "sets" then
        -- Discover Blizzard transmog sets
        if ETB.SetsData then
            ETB.SetsData:PrintDiscovery()
        else
            ETB:Print("SetsData module not loaded!")
        end
        
    elseif cmd == "debugset" then
        -- Debug a specific set: /etb debugset 630
        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
        
    elseif cmd == "rawapi" then
        -- Raw API test for sets: /etb rawapi 910
        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 .. " ===")
        
        -- Test C_TransmogSets.GetSetSources
        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
        
        -- Test C_TransmogSets.GetAllSourceIDs
        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
                
                -- Test GetSourceInfo on first sourceID
                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
        
        -- Test C_TransmogSets.GetSetPrimaryAppearances
        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
        
    -- =========================================================================
    -- CUSTOM SETS COMMANDS
    -- =========================================================================
    
    elseif cmd == "customsets" or cmd == "csets" then
        -- Custom sets management
        if ETB.CustomSets then
            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
        else
            ETB:Print("CustomSets module not loaded!")
        end
    
    elseif cmd == "discover" then
        -- Discovery tool to find potential custom sets
        if not ETB.CustomSets then
            ETB:Print("CustomSets module not loaded!")
            return
        end
        
        local categoryID = tonumber(args[2]) or ETB.Filtering.filters.category
        ETB:Print(string.format("|cff00ffff=== DISCOVERING POTENTIAL SETS (Category %d) ===|r", categoryID))
        
        -- Get items from current category
        local items = ETB.Filtering:GetItems()
        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  -- Limit output
                    ETB:Print(string.format("  |cffffd100%s|r - %d pieces (%s)", 
                        set.name, set.pieceCount, set.armorType or "unknown"))
                    -- Show first few items
                    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
        
    elseif cmd == "armorcount" or cmd == "armorstats" then
        -- Direct Blizzard API count (authoritative)
        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
                
                -- Count sources by armor type for this category
                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")
        
    elseif cmd == "filtertest" then
        -- Test why armor type filter fails
        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))
        
        -- Sample first 50 appearances
        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
        
    elseif cmd == "loadreport" or cmd == "lr" then
        -- Print ArmorData load report (shows what was actually loaded)
        if ETB.ArmorData and ETB.ArmorData.PrintLoadReport then
            ETB.ArmorData:PrintLoadReport()
        else
            ETB:Print("|cffff0000ArmorData module not available|r")
        end
        
    elseif cmd == "verify" then
        -- Verify loaded items match expected from API
        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
            -- Get expected count from API
            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
            
            -- Get loaded count from ArmorData
            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
            
            -- Compare
            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")
        
    elseif cmd == "audit" then
        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
        
    elseif cmd == "tutorial" or cmd == "guide" then
        if ETB.Tutorial then
            ETB.Tutorial:Toggle()
        end
        
    elseif cmd == "help" then
        ETB:Print("=== Enhanced Transmog Browser ===")
        ETB:Print("|cffffd100/etb|r — Open collection browser")
        ETB:Print("|cffffd100/etb transmogui|r — Open transmog application UI")
        ETB:Print("|cffffd100/etb tutorial|r — Open getting started guide")
        ETB:Print("|cffffd100/etb hide|r — Close browser")
        ETB:Print("|cffffd100/etb toggle|r — Toggle browser on/off")
        ETB:Print("|cffffd100/etb profiles|r — Profile system info")
        ETB:Print("|cffffd100/etb outfits|r — List saved outfits")
        ETB:Print("|cffffd100/etb favorites|r — Favorites info")
        ETB:Print("|cffffd100/etb minimap|r — Toggle minimap button")
        ETB:Print("|cffffd100/etb stats|r — Collection progress by slot")
        ETB:Print("|cffffd100/etb wishlist|r — Open wishlist panel")
        ETB:Print("|cffffd100/etb farm|r — Open farming guide")
        ETB:Print("|cffffd100/etb audit|r — Loot data audit (multi|orphans|compare|drops|item|boss)")
        ETB:Print("|cffffd100/etb reset confirm|r — Reset all saved data")
        ETB:Print("|cffffd100/etb debug|r — Toggle debug mode (enables developer commands)")
        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")
        
    elseif cmd == "pipelineaudit" then
        -- Legacy: Comprehensive data pipeline audit (use /etb audit instead)
        ETB:Print("|cff00ffff=== ETB DATA PIPELINE AUDIT ===|r")
        ETB:Print("")
        
        -- 1. Profile System
        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()))
            
            -- Show all profiles
            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("")
        
        -- 2. Class Filtering Check
        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)))
            
            -- Count usable weapons
            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("")
        
        -- 3. Category Data Check
        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("")
        
        -- 4. Sample Data Check (test one armor and one weapon category)
        ETB:Print("|cffffd100[4] SAMPLE DATA CHECK|r")
        if ETB.BlizzardData then
            -- Test Head (category 1)
            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))
            
            -- Test 1H Swords (category 14)
            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("")
        
        -- 5. Filter System Check
        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("")
        
        -- 6. Sets Data Check
        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")
        
    elseif cmd == "classtest" then
        -- Test class filtering for all classes (MoP Classic)
        ETB:Print("|cff00ffff=== CLASS FILTERING TEST (MoP Classic) ===|r")
        ETB:Print("|cff888888Note: Thrown removed, ranged slot gone, wands are main-hand|r")
        ETB:Print("")
        
        local CLASS_PRIMARY_ARMOR = {
            WARRIOR = "PLATE", PALADIN = "PLATE", DEATHKNIGHT = "PLATE",
            HUNTER = "MAIL", SHAMAN = "MAIL",
            ROGUE = "LEATHER", DRUID = "LEATHER", MONK = "LEATHER",
            MAGE = "CLOTH", WARLOCK = "CLOTH", PRIEST = "CLOTH",
        }
        
        local CLASS_WEAPONS = {
            WARRIOR     = { "AXE", "DAGGER", "FIST", "MACE", "POLEARM", "SHIELD", "STAFF", "SWORD" },
            PALADIN     = { "AXE", "MACE", "POLEARM", "SHIELD", "SWORD" },
            DEATHKNIGHT = { "AXE", "MACE", "POLEARM", "SWORD" },
            HUNTER      = { "AXE", "BOW", "CROSSBOW", "DAGGER", "FIST", "GUN", "POLEARM", "STAFF", "SWORD" },
            SHAMAN      = { "AXE", "DAGGER", "FIST", "MACE", "SHIELD", "STAFF" },
            ROGUE       = { "AXE", "DAGGER", "FIST", "MACE", "SWORD" },
            DRUID       = { "DAGGER", "FIST", "MACE", "POLEARM", "STAFF" },
            MONK        = { "AXE", "FIST", "MACE", "POLEARM", "STAFF", "SWORD" },
            MAGE        = { "DAGGER", "STAFF", "SWORD", "WAND" },
            WARLOCK     = { "DAGGER", "STAFF", "SWORD", "WAND" },
            PRIEST      = { "DAGGER", "MACE", "STAFF", "WAND" },
        }
        
        local classOrder = { "WARRIOR", "PALADIN", "DEATHKNIGHT", "HUNTER", "SHAMAN", "ROGUE", "DRUID", "MONK", "MAGE", "WARLOCK", "PRIEST" }
        
        for _, classID in ipairs(classOrder) do
            local armor = CLASS_PRIMARY_ARMOR[classID]
            local weapons = CLASS_WEAPONS[classID]
            ETB:Print(string.format("|cffffd100%s|r: %s armor, %d weapon types", classID, armor, #weapons))
            ETB:Print(string.format("  Weapons: %s", table.concat(weapons, ", ")))
        end
        
        ETB:Print("")
        ETB:Print("|cff888888Use /etb switch <name> to test filtering with different profiles|r")
        
    elseif cmd == "bossloot" or cmd == "blt" then
        if args[1] == "stats" then
            if ETB.BossLootTooltip then
                ETB:Print("|cff00ccffBoss Loot Tooltip:|r " .. ETB.BossLootTooltip:GetStats())
                ETB:Print("Enabled: " .. (ETB.BossLootTooltip.enabled and "|cff00ff00Yes|r" or "|cffff0000No|r"))
            else
                ETB:Print("BossLootTooltip module not loaded")
            end
        elseif args[1] == "build" then
            if ETB.BossLootTooltip then
                ETB.BossLootTooltip:BuildIndex()
                ETB:Print("|cff00ccffBoss Loot:|r Index rebuilt - " .. ETB.BossLootTooltip:GetStats())
            end
        else
            if ETB.BossLootTooltip then
                ETB.BossLootTooltip:Toggle()
            else
                ETB:Print("BossLootTooltip module not loaded")
            end
        end
        
    elseif cmd == "fixitems" then
        -- For every community set item that doesn't exist in MoP client,
        -- use visualID to find a MoP item sharing the same appearance
        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 = {}       -- { [dataIndex] = { [invSlot] = {itemID=N, visualID=N, name="..."} } }
        local unfixList = {}    -- { {set=name, slot=N, itemID=N, visualID=N} }
        
        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
                
                -- Check if item exists in MoP client
                local iName = GetItemInfoInstant and GetItemInfoInstant(itemID)
                if iName then
                    valid = valid + 1
                else
                    -- Item doesn't exist — try visualID lookup
                    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
                                        local rName, _, _, _, rIcon = GetItemInfoInstant(srcInfo.itemID)
                                        if rName then
                                            replacement = {
                                                itemID = srcInfo.itemID,
                                                visualID = srcInfo.visualID or visualID,
                                                name = rName,
                                                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
        
        -- Save to SavedVariables
        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))
        
        -- Show fix details
        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
        
        -- Show some fix examples
        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")
    
    elseif cmd == "deepfix" then
        -- DEEP FIX v2: Match ALL community sets to Blizzard transmog sets by NAME,
        -- then rebuild every slot from Blizzard's API data.
        -- This fixes the index-shift bug from the scansets approach.
        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"}
        
        -- Build Blizzard set index: blizzByName[normalized_name] = { {setID, items={[slot]={...}}} }
        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
            
            -- Try GetSetPrimaryAppearances first
            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
                                        local iName, _, _, _, iIcon, _, _, _, invTypeStr = GetItemInfoInstant(srcInfo.itemID)
                                        if iName and invTypeStr then
                                            local slot = INVTYPE_TO_SLOT[invTypeStr]
                                            if slot and not items[slot] then
                                                items[slot] = {
                                                    itemID = srcInfo.itemID,
                                                    visualID = srcInfo.visualID or appID,
                                                    sourceID = sourceID,
                                                    name = iName,
                                                    icon = iIcon,
                                                }
                                                itemCount = itemCount + 1
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end
            
            -- Fallback: GetAllSourceIDs  
            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
                            local iName, _, _, _, iIcon, _, _, _, invTypeStr = GetItemInfoInstant(srcInfo.itemID)
                            if iName and invTypeStr then
                                local slot = INVTYPE_TO_SLOT[invTypeStr]
                                if slot and not items[slot] then
                                    items[slot] = {
                                        itemID = srcInfo.itemID,
                                        visualID = srcInfo.visualID or 0,
                                        sourceID = sourceID,
                                        name = iName,
                                        icon = iIcon,
                                    }
                                    itemCount = itemCount + 1
                                end
                            end
                        end
                    end
                end
            end
            
            if itemCount > 0 then
                -- Index by multiple name variants for fuzzy matching
                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))
        
        -- Phase 2: Match community sets by name and fix broken items
        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]
            
            -- Find broken items
            local brokenSlots = {}
            local hasBroken = false
            
            for invSlot, itemData in pairs(rawItems) do
                local itemID = itemData[1]
                local iName = GetItemInfoInstant and GetItemInfoInstant(itemID)
                if not iName then
                    brokenSlots[invSlot] = { itemID = itemID, visualID = itemData[2] }
                    totalBroken = totalBroken + 1
                    hasBroken = true
                end
            end
            
            if not hasBroken then
                -- skip, all valid
            else
                local setFixes = {}
                
                -- Strategy 1: Name match to Blizzard sets
                local nameNorm = setName:lower():gsub("\\.", ""):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
                local nameNoRecolor = nameNorm:gsub("%s*%(recolor%)%s*", "")
                
                -- Try exact match first, then without (Recolor)
                local matches = blizzByName[nameNorm] or blizzByName[nameNoRecolor] or {}
                
                if #matches > 0 then
                    nameMatched = nameMatched + 1
                    
                    -- Find best match: most slots covered for our broken slots
                    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
                
                -- Strategy 2: VisualID lookup for remaining broken slots
                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
                                            local rName = GetItemInfoInstant(srcInfo.itemID)
                                            if rName then
                                                setFixes[brokenSlot] = {
                                                    itemID = srcInfo.itemID,
                                                    visualID = srcInfo.visualID or visualID,
                                                    name = rName,
                                                    method = "visualID:" .. visualID,
                                                }
                                                fixedByVisualID = fixedByVisualID + 1
                                                break
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
                
                -- Track remaining unfixable
                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
                    -- Key by BOTH index and name for safe application
                    deepFixMap[i] = {
                        setName = setName,
                        fixes = setFixes,
                    }
                end
            end
        end
        
        -- Save
        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))
        
        -- Show name-match fix examples
        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")
    
    elseif cmd == "fixsets" or cmd == "scansets" then
        -- Scan Blizzard's C_TransmogSets API, match community sets by name,
        -- and export correct MoP itemIDs for all slots
        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...")
        
        -- Step 1: Get ALL Blizzard sets (MoP Classic returns setInfo objects)
        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))
        
        -- Build: setName -> list of { items, setID, ... }
        local blizzByName = {}
        local blizzSetCount = 0
        local resolveErrors = 0
        
        for _, setInfo in ipairs(allSetData) do
            local setID = setInfo.setID
            local setName = setInfo.name
            if not setID or not setName then
                -- skip
            else
                local items = {}
                local itemCount = 0
                
                -- Try GetSetPrimaryAppearances first (preferred)
                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
                
                -- Fallback: GetAllSourceIDs
                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
                                -- Determine invSlot from invType
                                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
                                -- Fallback: use categoryID
                                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
                                -- Last resort: get invType from GetItemInfoInstant
                                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))
        
        -- Step 2: Match community sets by name and fill in items
        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]
            
            -- Normalize: strip Lua escape backslashes for matching
            local matchName = name:gsub("\\", ""):gsub("\\'", "'")
            
            -- Try exact match
            local blizzMatches = blizzByName[matchName]
            
            -- Try without " (Recolor)"
            if not blizzMatches then
                local baseName = matchName:gsub(" %(Recolor%)$", "")
                blizzMatches = blizzByName[baseName]
            end
            
            if blizzMatches then
                -- Find best match by slot overlap with existing items
                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 = {}
                    
                    -- Keep valid existing items, replace bad ones
                    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
                    
                    -- Add any Blizzard slots we didn't have at all
                    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
        
        -- Save to SavedVariables
        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")
        
    elseif cmd == "npcnames" then
        if ETB.RuntimeNPCNames then
            ETB.RuntimeNPCNames:PrintStats()
        else
            ETB:Print("RuntimeNPCNames module not loaded.")
        end
        
    else
        ETB:Print("Unknown command. Type |cffffd100/etb help|r")
    end
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
