--[[
============================================================================
ETB v11.0 - SetsData.lua
============================================================================
Blizzard Transmog Sets Integration (C_TransmogSets)

MoP Classic has native transmog set support through the C_TransmogSets API.
This module provides access to:
- Base Sets (base appearances)
- Variant Sets (color/difficulty variants)
- Set completion tracking
- Source information for set pieces

API Reference (MoP Classic 5.4.8):
- C_TransmogSets.GetAllSets() - Returns all set IDs
- C_TransmogSets.GetSetInfo(setID) - Returns set name, etc.
- C_TransmogSets.GetSetSources(setID) - Returns sourceIDs for set pieces
- C_TransmogSets.GetSourcesForSlot(setID, slot) - Returns sources for a slot
- C_TransmogSets.GetAllSourceIDs(setID) - Returns all source IDs
- C_TransmogSets.GetBaseSetID(variantSetID) - Returns base set ID
- C_TransmogSets.GetVariantSets(baseSetID) - Returns variant set IDs
- C_TransmogSets.IsSetUsable(setID) - Can current character use this set
- C_TransmogSets.GetSetPrimaryAppearances(setID) - Returns primary appearances
============================================================================
]]--

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

ETB.SetsData = {
    -- Cached set data
    allSets = {},
    baseSets = {},
    variantSets = {},
    
    -- Indexed by set type (use numbers as fallback if Enum doesn't exist)
    setsByType = {},
    
    -- Cached completion data
    setCompletion = {},
    
    -- API availability
    apiAvailable = false,
}

local SD = ETB.SetsData

-- Set type constants (Enum.TransmogSetType doesn't exist in MoP Classic)
-- We'll just use simple numbers
local SET_TYPE_ARMOR = 1
local SET_TYPE_WEAPON = 2

-- Initialize setsByType with our constants
SD.setsByType[SET_TYPE_ARMOR] = {}
SD.setsByType[SET_TYPE_WEAPON] = {}

-- =============================================================================
-- API CHECK
-- =============================================================================

function SD:CheckAPI()
    -- Check if C_TransmogSets is available
    if C_TransmogSets then
        -- Check for specific functions we need
        self.apiAvailable = (
            C_TransmogSets.GetAllSets and
            C_TransmogSets.GetSetInfo and
            type(C_TransmogSets.GetAllSets) == "function"
        )
        
        if self.apiAvailable then
            ETB:Debug("SetsData", "C_TransmogSets API is available")
        else
            ETB:Debug("SetsData", "C_TransmogSets exists but missing required functions")
        end
    else
        self.apiAvailable = false
        ETB:Debug("SetsData", "C_TransmogSets API is NOT available")
    end
    
    return self.apiAvailable
end

-- =============================================================================
-- SET DISCOVERY
-- =============================================================================

function SD:DiscoverSets()
    if not self:CheckAPI() then
        ETB:Print("|cffff0000C_TransmogSets API not available in this game version|r")
        return {}
    end
    
    ETB:Debug("SetsData", "Discovering transmog sets...")
    
    -- Clear cached data
    self.allSets = {}
    self.baseSets = {}
    self.variantSets = {}
    self.setsByType[SET_TYPE_ARMOR] = {}
    self.setsByType[SET_TYPE_WEAPON] = {}
    
    -- Get all sets from Blizzard API
    -- NOTE: In MoP Classic, GetAllSets() returns an array of setInfo OBJECTS, not setIDs!
    local allSetData = C_TransmogSets.GetAllSets() or {}
    
    ETB:Debug("SetsData", string.format("Found %d total sets", #allSetData))
    
    for i, setInfo in ipairs(allSetData) do
        -- setInfo is already a table with set data, not a setID
        -- We need to extract the setID from it
        local setID = setInfo.setID
        
        if setID and setInfo.name then
            -- Store full set info
            self.allSets[setID] = {
                setID = setID,
                name = setInfo.name,
                description = setInfo.description,
                label = setInfo.label,
                expansionID = setInfo.expansionID,
                patchID = setInfo.patchID,
                setType = setInfo.setType,
                classMask = setInfo.classMask,
                requiredFaction = setInfo.requiredFaction,
                collected = setInfo.collected,
                favorited = setInfo.favorited,
                hiddenUntilCollected = setInfo.hiddenUntilCollected,
                
                -- Relationships
                baseSetID = setInfo.baseSetID,
                isBaseSet = (setInfo.baseSetID == nil or setInfo.baseSetID == setID),
            }
            
            -- Categorize as base or variant
            if self.allSets[setID].isBaseSet then
                self.baseSets[setID] = self.allSets[setID]
            else
                self.variantSets[setID] = self.allSets[setID]
            end
            
            -- Index by type
            local setType = setInfo.setType or SET_TYPE_ARMOR
            if not self.setsByType[setType] then
                self.setsByType[setType] = {}
            end
            self.setsByType[setType][setID] = self.allSets[setID]
        end
    end
    
    ETB:Debug("SetsData", string.format("Categorized: %d base sets, %d variants", 
        self:CountTable(self.baseSets), self:CountTable(self.variantSets)))
    
    return self.allSets
end

-- =============================================================================
-- SET INFORMATION
-- =============================================================================

function SD:GetSetInfo(setID)
    if not self.apiAvailable then return nil end
    if not setID or type(setID) ~= "number" then return nil end
    
    -- Return cached if available (preferred since GetAllSets already gave us the data)
    if self.allSets[setID] then
        return self.allSets[setID]
    end
    
    -- If we haven't discovered sets yet, do so now
    if self:CountTable(self.allSets) == 0 then
        self:DiscoverSets()
        if self.allSets[setID] then
            return self.allSets[setID]
        end
    end
    
    -- As a last resort, try the API (may not work in MoP Classic)
    local success, setInfo = pcall(C_TransmogSets.GetSetInfo, setID)
    if success and setInfo then
        self.allSets[setID] = setInfo
        return setInfo
    end
    
    return nil
end

function SD:GetSetSources(setID)
    if not self.apiAvailable then return {} end
    if not setID or type(setID) ~= "number" then return {} end
    
    -- This function doesn't exist in MoP Classic - use GetAllSourceIDs instead
    if not C_TransmogSets.GetSetSources then
        return {}
    end
    
    local success, sources = pcall(C_TransmogSets.GetSetSources, setID)
    if success and sources then
        return sources
    end
    return {}
end

-- Alternative: Get all source IDs for a set
function SD:GetAllSourceIDs(setID)
    if not self.apiAvailable then return {} end
    if not setID or type(setID) ~= "number" then return {} end
    
    if not C_TransmogSets or not C_TransmogSets.GetAllSourceIDs then
        return {}
    end
    
    local success, result = pcall(C_TransmogSets.GetAllSourceIDs, setID)
    if success and result and type(result) == "table" then
        return result
    end
    
    return {}
end

function SD:GetSetPrimaryAppearances(setID)
    if not self.apiAvailable then return {} end
    if not setID or type(setID) ~= "number" then return {} end
    
    if C_TransmogSets.GetSetPrimaryAppearances then
        local success, result = pcall(C_TransmogSets.GetSetPrimaryAppearances, setID)
        if success and result then
            return result
        end
    end
    
    return {}
end

-- Get all items for a set with their item info (for UI display)
-- In MoP Classic: GetSetSources doesn't exist, use GetAllSourceIDs + GetSetPrimaryAppearances
function SD:GetSetItems(setID)
    if not self.apiAvailable then return {} end
    if not setID or type(setID) ~= "number" then return {} end
    
    local items = {}
    local itemsBySlot = {}  -- Group by slot for alternatives
    
    -- Step 1: Get all sourceIDs for this set
    local sourceIDs = self:GetAllSourceIDs(setID)
    if not sourceIDs or #sourceIDs == 0 then
        return {}
    end
    
    -- Step 2: Get primary appearances for collection status
    local primaryAppearances = self:GetSetPrimaryAppearances(setID)
    local collectedByAppearance = {}
    if type(primaryAppearances) == "table" then
        for _, app in ipairs(primaryAppearances) do
            if type(app) == "table" and app.appearanceID then
                collectedByAppearance[app.appearanceID] = app.collected
            end
        end
    end
    
    -- Step 3: Process each sourceID
    for _, sourceID in ipairs(sourceIDs) do
        if type(sourceID) == "number" then
            local itemData = self:GetItemFromSourceID(sourceID)
            if itemData then
                -- Check collection status
                itemData.collected = self:IsSourceCollected(sourceID)
                
                -- Also check by visualID if we have that data
                local vid = ETB:ResolveVisualID(itemData)
                if vid and collectedByAppearance[vid] then
                    itemData.collected = true
                end
                
                itemData.sourceID = sourceID
                
                -- Group by slot for alternatives display
                local slot = itemData.invType or "UNKNOWN"
                if not itemsBySlot[slot] then
                    itemsBySlot[slot] = {
                        primary = nil,
                        alternatives = {},
                        collected = false
                    }
                end
                
                -- First item for this slot becomes primary
                if not itemsBySlot[slot].primary then
                    itemsBySlot[slot].primary = itemData
                else
                    table.insert(itemsBySlot[slot].alternatives, itemData)
                end
                
                -- If ANY item for this slot is collected, the slot counts as collected
                if itemData.collected then
                    itemsBySlot[slot].collected = true
                end
                
                table.insert(items, itemData)
            end
        end
    end
    
    -- Update primary items with slot-level collection status
    for slot, slotData in pairs(itemsBySlot) do
        if slotData.primary then
            slotData.primary.slotCollected = slotData.collected
            slotData.primary.alternatives = slotData.alternatives
            slotData.primary.alternativeCount = #slotData.alternatives
        end
    end
    
    -- Sort by slot/invType for consistent display order
    local slotOrder = {
        INVTYPE_HEAD = 1, INVTYPE_SHOULDER = 2, INVTYPE_CHEST = 3,
        INVTYPE_ROBE = 3, INVTYPE_WAIST = 4, INVTYPE_LEGS = 5,
        INVTYPE_FEET = 6, INVTYPE_WRIST = 7, INVTYPE_HAND = 8,
        INVTYPE_CLOAK = 9,
    }
    table.sort(items, function(a, b)
        local orderA = slotOrder[a.invType] or 99
        local orderB = slotOrder[b.invType] or 99
        if orderA ~= orderB then
            return orderA < orderB
        end
        -- Within same slot, put collected items first
        if a.collected ~= b.collected then
            return a.collected == true
        end
        -- Safe string comparison (ensure both are strings)
        local nameA = tostring(a.name or "")
        local nameB = tostring(b.name or "")
        return nameA < nameB
    end)
    
    ETB:Debug("SetsData", string.format("GetSetItems final: %d items for set %d", #items, setID))
    
    return items
end

-- Get unique items per slot (primary + alternatives info)
function SD:GetSetItemsBySlot(setID)
    local allItems = self:GetSetItems(setID)
    
    if not allItems or #allItems == 0 then
        return {}
    end
    
    local bySlot = {}
    
    -- Canonical slot order (8 armor slots)
    local slotOrder = {
        "INVTYPE_HEAD", "INVTYPE_SHOULDER", "INVTYPE_CHEST",
        "INVTYPE_WAIST", "INVTYPE_LEGS", "INVTYPE_FEET", "INVTYPE_WRIST", 
        "INVTYPE_HAND"
    }
    
    -- Normalize slot names (ROBE = CHEST)
    local function normalizeSlot(slot)
        if slot == "INVTYPE_ROBE" then return "INVTYPE_CHEST" end
        return slot
    end
    
    -- Initialize slots
    for _, slot in ipairs(slotOrder) do
        bySlot[slot] = nil
    end
    
    -- Group items by slot, keeping first as primary
    for _, item in ipairs(allItems) do
        local slot = normalizeSlot(item.invType or "UNKNOWN")
        
        if bySlot[slot] == nil then
            -- First item for this slot becomes primary
            bySlot[slot] = {
                primary = item,
                alternatives = {},
                collected = item.collected or false
            }
        else
            -- Additional items are alternatives
            table.insert(bySlot[slot].alternatives, item)
            -- If ANY item for this slot is collected, mark slot as collected
            if item.collected then
                bySlot[slot].collected = true
            end
        end
    end
    
    -- Convert to ordered array
    local result = {}
    for _, slot in ipairs(slotOrder) do
        if bySlot[slot] then
            table.insert(result, bySlot[slot])
        end
    end
    
    return result
end

-- Helper: Check if a specific source is collected
function SD:IsSourceCollected(sourceID)
    if not sourceID then return false end
    if C_TransmogCollection and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
        local success, result = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, sourceID)
        if success then return result end
    end
    return false
end

-- Helper: Get first sourceID from an appearanceID
function SD:GetSourceIDFromAppearance(appearanceID)
    if not appearanceID then return nil end
    if C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
        local success, sources = pcall(C_TransmogCollection.GetAppearanceSources, appearanceID)
        if success and type(sources) == "table" and #sources > 0 then
            local first = sources[1]
            if type(first) == "number" then
                return first
            elseif type(first) == "table" and first.sourceID then
                return first.sourceID
            end
        end
    end
    return nil
end

-- Convert sourceID to item data
function SD:GetItemFromSourceID(sourceID)
    if not sourceID or type(sourceID) ~= "number" then 
        return nil 
    end
    
    -- Check if GetSourceInfo exists
    if not C_TransmogCollection or not C_TransmogCollection.GetSourceInfo then
        return nil
    end
    
    local success, sourceInfo = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
    
    if not success or type(sourceInfo) ~= "table" or not sourceInfo.itemID then
        return nil
    end
    
    local itemID = sourceInfo.itemID
    local name, _, quality, _, _, _, subType, _, invType, icon = GetItemInfo(itemID)

    -- If GetItemInfo hasn't cached the item yet, get the icon/equip slot from
    -- GetItemInfoInstant (synchronous) and prime the async cache for the NAME.
    -- IMPORTANT: on MoP Classic GetItemInfoInstant's FIRST return is the numeric
    -- itemID, not the name — using it as the name is what made slots read
    -- "70989" instead of the real item name. We leave name unresolved here and
    -- mark the entry so the UI can fill it in once GET_ITEM_INFO_RECEIVED fires.
    local nameResolved = (name ~= nil)
    if not name then
        local _, _, _, equipLoc, iconPath = GetItemInfoInstant(itemID)
        icon = icon or iconPath
        invType = invType or equipLoc
        name = "Loading..."
        GetItemInfo(itemID)  -- queue async resolution
    end

    -- Use categoryID for slot detection - this is the transmog category
    -- which is more reliable than invType from sourceInfo
    local invTypeStr = self:ConvertCategoryToInvType(sourceInfo.categoryID)

    return {
        itemID = itemID,
        name = tostring(name),  -- Ensure name is always a string!
        nameResolved = nameResolved,
        icon = icon,
        quality = quality or 3,
        invType = invTypeStr,
        categoryID = sourceInfo.categoryID,
        visualID = sourceInfo.visualID,
    }
end

-- Convert transmog categoryID to INVTYPE string
-- Transmog categories are different from inventory slot types!
function SD:ConvertCategoryToInvType(categoryID)
    if type(categoryID) == "string" then
        return categoryID  -- Already a string
    end
    
    -- Transmog category IDs (from C_TransmogCollection)
    local categoryMap = {
        [1] = "INVTYPE_HEAD",
        [2] = "INVTYPE_SHOULDER",
        [3] = "INVTYPE_CLOAK",      -- Back
        [4] = "INVTYPE_CHEST",
        [5] = "INVTYPE_BODY",       -- Shirt
        [6] = "INVTYPE_TABARD",
        [7] = "INVTYPE_WRIST",
        [8] = "INVTYPE_HAND",
        [9] = "INVTYPE_WAIST",
        [10] = "INVTYPE_LEGS",
        [11] = "INVTYPE_FEET",
        -- Weapon categories (12+)
        [12] = "INVTYPE_WEAPON",      -- One-Hand
        [13] = "INVTYPE_SHIELD",
        [14] = "INVTYPE_RANGED",      -- Bows
        [15] = "INVTYPE_CLOAK",       -- Back (duplicate?)
        [16] = "INVTYPE_2HWEAPON",
        [17] = "INVTYPE_WEAPON",
        [18] = "INVTYPE_WEAPON",
        [19] = "INVTYPE_WEAPON",
        [20] = "INVTYPE_ROBE",        -- Chest (Robe)
        [21] = "INVTYPE_WEAPONMAINHAND",
        [22] = "INVTYPE_WEAPONOFFHAND",
        [23] = "INVTYPE_HOLDABLE",    -- Held In Off-hand
        [24] = "INVTYPE_RANGEDRIGHT", -- Guns, Wands
        [25] = "INVTYPE_THROWN",
        [26] = "INVTYPE_RANGEDRIGHT",
    }
    
    return categoryMap[categoryID] or "UNKNOWN"
end

function SD:GetVariantSets(baseSetID)
    if not self.apiAvailable then return {} end
    if not baseSetID or type(baseSetID) ~= "number" then return {} end
    
    if C_TransmogSets.GetVariantSets then
        local success, result = pcall(C_TransmogSets.GetVariantSets, baseSetID)
        if success and result then
            return result
        end
    end
    
    return {}
end

function SD:GetBaseSetID(variantSetID)
    if not self.apiAvailable then return nil end
    if not variantSetID or type(variantSetID) ~= "number" then return nil end
    
    if C_TransmogSets.GetBaseSetID then
        local success, result = pcall(C_TransmogSets.GetBaseSetID, variantSetID)
        if success then
            return result
        end
    end
    
    return nil
end

-- =============================================================================
-- SET COMPLETION
-- =============================================================================

-- Get slot-based completion (the correct way - counts unique slots, not items)
function SD:GetSetCompletion(setID)
    if not self.apiAvailable then return 0, 0 end
    if not setID or type(setID) ~= "number" then return 0, 0 end
    
    -- BEST METHOD: Use GetSetPrimaryAppearances
    -- This returns one entry per SLOT (not per item), which is what we want
    local appearances = self:GetSetPrimaryAppearances(setID)
    if type(appearances) == "table" and #appearances > 0 then
        local collected = 0
        local total = #appearances
        for _, appearanceInfo in ipairs(appearances) do
            if type(appearanceInfo) == "table" and appearanceInfo.collected then
                collected = collected + 1
            end
        end
        return collected, total
    end
    
    -- FALLBACK: Use our slot-based calculation
    local slotItems = self:GetSetItemsBySlot(setID)
    if slotItems and #slotItems > 0 then
        local collected = 0
        local total = #slotItems
        for _, slotData in ipairs(slotItems) do
            if slotData.collected then
                collected = collected + 1
            end
        end
        return collected, total
    end
    
    -- Last resort: Check cached set info
    local setInfo = self.allSets[setID]
    if setInfo and setInfo.collected then
        return 1, 1
    end
    
    return 0, 0
end

-- Get detailed slot completion info (for UI display)
function SD:GetSlotCompletionDetails(setID)
    if not self.apiAvailable then return {} end
    if not setID or type(setID) ~= "number" then return {} end
    
    local slotItems = self:GetSetItemsBySlot(setID)
    local details = {}
    
    local slotDisplayNames = {
        INVTYPE_HEAD = "Head",
        INVTYPE_SHOULDER = "Shoulders",
        INVTYPE_CHEST = "Chest",
        INVTYPE_WAIST = "Waist",
        INVTYPE_LEGS = "Legs",
        INVTYPE_FEET = "Feet",
        INVTYPE_WRIST = "Wrist",
        INVTYPE_HAND = "Hands",
        INVTYPE_CLOAK = "Back",
    }
    
    for _, slotData in ipairs(slotItems) do
        local slotName = slotDisplayNames[slotData.primary.invType] or slotData.primary.invType or "Unknown"
        
        -- Count how many alternatives can complete this slot
        local totalOptions = 1 + (slotData.alternatives and #slotData.alternatives or 0)
        local collectedOptions = 0
        
        if slotData.primary.collected then
            collectedOptions = collectedOptions + 1
        end
        
        if slotData.alternatives then
            for _, alt in ipairs(slotData.alternatives) do
                if alt.collected then
                    collectedOptions = collectedOptions + 1
                end
            end
        end
        
        table.insert(details, {
            slotName = slotName,
            invType = slotData.primary.invType,
            isComplete = slotData.collected,
            primary = slotData.primary,
            alternatives = slotData.alternatives or {},
            totalOptions = totalOptions,
            collectedOptions = collectedOptions,
        })
    end
    
    return details
end

-- Debug function to investigate set data structure
function SD:DebugSet(setID)
    if not self.apiAvailable then
        ETB:Print("API not available")
        return
    end
    
    setID = tonumber(setID)
    if not setID then
        ETB:Print("Invalid setID")
        return
    end
    
    local setInfo = self:GetSetInfo(setID)
    ETB:Print(string.format("=== DEBUG SET %d ===", setID))
    
    if setInfo then
        ETB:Print(string.format("Name: %s", setInfo.name or "nil"))
        ETB:Print(string.format("Label: %s", setInfo.label or "nil"))
        ETB:Print(string.format("Description: %s", setInfo.description or "nil"))
        ETB:Print(string.format("BaseSetID: %s", tostring(setInfo.baseSetID)))
        ETB:Print(string.format("IsBaseSet: %s", tostring(setInfo.isBaseSet)))
        ETB:Print(string.format("Collected: %s", tostring(setInfo.collected)))
        ETB:Print(string.format("ClassMask: %s", tostring(setInfo.classMask)))
        ETB:Print(string.format("ExpansionID: %s", tostring(setInfo.expansionID)))
    else
        ETB:Print("Set info not available")
    end
    
    -- Test GetSetSources - returns { [sourceID] = isCollected }
    ETB:Print("")
    ETB:Print("GetSetSources result (sourceID -> isCollected):")
    local sources = self:GetSetSources(setID)
    local sourceCount = 0
    local sourceList = {}
    if type(sources) == "table" then
        for sourceID, isCollected in pairs(sources) do
            sourceCount = sourceCount + 1
            table.insert(sourceList, {sourceID = sourceID, collected = isCollected})
        end
        ETB:Print(string.format("  Total: %d source entries", sourceCount))
        
        -- Show first 5 with item info
        for i = 1, math.min(5, #sourceList) do
            local entry = sourceList[i]
            local itemID, slotName = nil, nil
            
            -- Get item info from sourceID
            if C_TransmogCollection and C_TransmogCollection.GetSourceInfo then
                local success, sourceInfo = pcall(C_TransmogCollection.GetSourceInfo, entry.sourceID)
                if success and type(sourceInfo) == "table" then
                    itemID = sourceInfo.itemID
                    slotName = sourceInfo.invType
                end
            end
            
            local itemName = itemID and GetItemInfo(itemID) or "Loading..."
            ETB:Print(string.format("  srcID=%d -> item=%s (%s) [%s]", 
                entry.sourceID, 
                tostring(itemID), 
                itemName,
                entry.collected and "COLLECTED" or "missing"))
        end
    else
        ETB:Print(string.format("  Type: %s (empty or wrong type)", type(sources)))
    end
    
    -- Test GetAllSourceIDs - returns array of sourceIDs
    ETB:Print("")
    ETB:Print("GetAllSourceIDs result (array of sourceIDs):")
    local allIDs = self:GetAllSourceIDs(setID)
    if type(allIDs) == "table" and #allIDs > 0 then
        ETB:Print(string.format("  Count: %d", #allIDs))
        for i = 1, math.min(3, #allIDs) do
            ETB:Print(string.format("  [%d] = %s", i, tostring(allIDs[i])))
        end
    else
        ETB:Print(string.format("  Empty or Type: %s", type(allIDs)))
    end
    
    -- Test GetSetPrimaryAppearances - returns array of {appearanceID, collected}
    ETB:Print("")
    ETB:Print("GetSetPrimaryAppearances result:")
    local appearances = self:GetSetPrimaryAppearances(setID)
    if type(appearances) == "table" and #appearances > 0 then
        ETB:Print(string.format("  Count: %d appearances", #appearances))
        for i = 1, math.min(5, #appearances) do
            local app = appearances[i]
            if type(app) == "table" then
                -- Try to get item from appearanceID
                local itemID = nil
                if app.appearanceID and C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
                    local success, appSources = pcall(C_TransmogCollection.GetAppearanceSources, app.appearanceID)
                    if success and type(appSources) == "table" and #appSources > 0 then
                        local srcID = appSources[1]
                        if type(srcID) == "number" and C_TransmogCollection.GetSourceInfo then
                            local success2, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, srcID)
                            if success2 and type(srcInfo) == "table" then
                                itemID = srcInfo.itemID
                            end
                        end
                    end
                end
                local itemName = itemID and GetItemInfo(itemID) or "?"
                ETB:Print(string.format("  [%d] appID=%s -> item=%s (%s) [%s]", 
                    i, tostring(app.appearanceID), tostring(itemID), itemName,
                    app.collected and "Y" or "N"))
            else
                ETB:Print(string.format("  [%d] = %s (type: %s)", i, tostring(app), type(app)))
            end
        end
    else
        ETB:Print(string.format("  Empty or Type: %s", type(appearances)))
    end
    
    -- Test variants
    ETB:Print("")
    local variants = self:GetVariantSets(setID)
    if type(variants) == "table" and #variants > 0 then
        ETB:Print(string.format("Variants (%d):", #variants))
        for i, v in ipairs(variants) do
            local varID = type(v) == "table" and v.setID or v
            local varInfo = self:GetSetInfo(varID)
            ETB:Print(string.format("  %d: setID=%s desc=%s", 
                i, tostring(varID), varInfo and varInfo.description or "?"))
        end
    else
        local baseID = self:GetBaseSetID(setID)
        if baseID and baseID ~= setID then
            ETB:Print(string.format("This is a variant. Base setID: %d", baseID))
        else
            ETB:Print("No variants found (this is base set or standalone)")
        end
    end
end

function SD:IsSetComplete(setID)
    local collected, total = self:GetSetCompletion(setID)
    return collected == total and total > 0
end

function SD:IsSetUsable(setID)
    if not self.apiAvailable then return true end
    if not setID or type(setID) ~= "number" then return true end
    
    if C_TransmogSets.IsSetUsable then
        local success, result = pcall(C_TransmogSets.IsSetUsable, setID)
        if success then
            return result
        end
    end
    
    return true
end

-- =============================================================================
-- FILTERING
-- =============================================================================

function SD:GetUsableSets()
    local usable = {}
    
    for setID, setInfo in pairs(self.allSets) do
        if self:IsSetUsable(setID) then
            usable[setID] = setInfo
        end
    end
    
    return usable
end

function SD:GetBaseSets()
    return self.baseSets
end

function SD:GetArmorSets()
    return self.setsByType[SET_TYPE_ARMOR] or {}
end

function SD:GetWeaponSets()
    return self.setsByType[SET_TYPE_WEAPON] or {}
end

function SD:GetSetsByExpansion(expansionID)
    local sets = {}
    
    for setID, setInfo in pairs(self.allSets) do
        if setInfo.expansionID == expansionID then
            sets[setID] = setInfo
        end
    end
    
    return sets
end

-- =============================================================================
-- SEARCH
-- =============================================================================

function SD:SearchSets(searchText)
    local results = {}
    local searchLower = searchText:lower()
    
    for setID, setInfo in pairs(self.allSets) do
        local nameMatch = setInfo.name and setInfo.name:lower():find(searchLower, 1, true)
        local labelMatch = setInfo.label and setInfo.label:lower():find(searchLower, 1, true)
        
        if nameMatch or labelMatch then
            results[setID] = setInfo
        end
    end
    
    return results
end

-- =============================================================================
-- UTILITY
-- =============================================================================

function SD:CountTable(t)
    local count = 0
    for _ in pairs(t) do count = count + 1 end
    return count
end

function SD:GetStats()
    return {
        apiAvailable = self.apiAvailable,
        totalSets = self:CountTable(self.allSets),
        baseSets = self:CountTable(self.baseSets),
        variantSets = self:CountTable(self.variantSets),
        armorSets = self:CountTable(self.setsByType[SET_TYPE_ARMOR] or {}),
        weaponSets = self:CountTable(self.setsByType[SET_TYPE_WEAPON] or {}),
    }
end

function SD:PrintDiscovery()
    ETB:Print("=== TRANSMOG SETS DISCOVERY ===")
    
    if not self:CheckAPI() then
        ETB:Print("|cffff0000C_TransmogSets API not available|r")
        return
    end
    
    -- Discover if not done yet
    if self:CountTable(self.allSets) == 0 then
        self:DiscoverSets()
    end
    
    local stats = self:GetStats()
    ETB:Print(string.format("Total Sets: %d", stats.totalSets))
    ETB:Print(string.format("  Base Sets: %d", stats.baseSets))
    ETB:Print(string.format("  Variant Sets: %d", stats.variantSets))
    ETB:Print(string.format("  Armor Sets: %d", stats.armorSets))
    ETB:Print(string.format("  Weapon Sets: %d", stats.weaponSets))
    
    -- Print first 5 sets as sample
    ETB:Print("")
    ETB:Print("Sample Sets:")
    local count = 0
    for setID, setInfo in pairs(self.baseSets) do
        if count >= 5 then break end
        local collected, total = self:GetSetCompletion(setID)
        ETB:Print(string.format("  [%d] %s (%d/%d)", 
            setID, setInfo.name or "Unknown", collected, total))
        count = count + 1
    end
end

-- =============================================================================
-- HIDDEN SETS (v14.1.0 Feature 3.6)
-- =============================================================================
-- User-toggleable per-set hide flag. Persisted in ETB_HiddenSets saved variable.
-- Set IDs that the user wants to skip — typically completed sets they're not
-- showing off, or sets that aren't relevant to their character.

function SD:InitHiddenSets()
    _G.ETB_HiddenSets = _G.ETB_HiddenSets or {}
    self.hiddenSets = _G.ETB_HiddenSets
end

function SD:IsSetHidden(setID)
    if not self.hiddenSets then self:InitHiddenSets() end
    return self.hiddenSets[setID] == true
end

function SD:SetHidden(setID, hidden)
    if not self.hiddenSets then self:InitHiddenSets() end
    if hidden then
        self.hiddenSets[setID] = true
    else
        self.hiddenSets[setID] = nil
    end
    if ETB.EventBus then
        ETB.EventBus:Fire("ETB_SET_HIDDEN_CHANGED", setID, hidden)
    end
end

function SD:ToggleHidden(setID)
    self:SetHidden(setID, not self:IsSetHidden(setID))
    return self:IsSetHidden(setID)
end

-- Returns array of hidden set IDs (for debugging / "show hidden" filter)
function SD:GetHiddenSets()
    if not self.hiddenSets then self:InitHiddenSets() end
    local out = {}
    for setID in pairs(self.hiddenSets) do
        out[#out + 1] = setID
    end
    return out
end

-- =============================================================================
-- INITIALIZATION
-- =============================================================================

ETB.EventBus:Register("ETB_INITIALIZED", function()
    SD:CheckAPI()
    SD:InitHiddenSets()
    -- Don't auto-discover on load - let user trigger it
end, ETB.EventBus.Priority.NORMAL)

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