--[[
============================================================================
ETB - CommunitySetLoader.lua
============================================================================
Loads 778 MoP-validated community sets from CommunitySets.lua and resolves
sourceIDs at runtime via C_TransmogCollection APIs.

Provides the same interface pattern as ETB.CustomSets so SetsUI can query
community sets using GetAllSets(), GetSet(), GetSetCompletion().

Data format in CommunitySets.lua:
  { name, armorType(1-5), expansion(0-4), filterType(1-7),
    items = { [invSlot] = { itemID, visualID } } }
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB
if not ETB then return end

ETB.CommunitySets = ETB.CommunitySets or {}
local CS = ETB.CommunitySets

-- ============================================================================
-- CONSTANTS
-- ============================================================================

local ARMOR_NAMES = { [1] = "CLOTH", [2] = "LEATHER", [3] = "MAIL", [4] = "PLATE", [5] = "COSMETIC", [13] = "COSMETIC" }
local ARMOR_DISPLAY = { [1] = "Cloth", [2] = "Leather", [3] = "Mail", [4] = "Plate", [5] = "Cosmetic", [13] = "Cosmetic" }
local EXP_NAMES = { [0] = "Classic", [1] = "TBC", [2] = "Wrath", [3] = "Cata", [4] = "MoP" }
local FILTER_NAMES = {
    [1] = "Dungeon", [2] = "Tier Recolor", [3] = "Quest",
    [4] = "World/Crafted", [5] = "Holiday", [6] = "Misc", [7] = "Low-Level",
}

local CLASS_ARMOR = {
    MAGE = "CLOTH", WARLOCK = "CLOTH", PRIEST = "CLOTH",
    ROGUE = "LEATHER", DRUID = "LEATHER", MONK = "LEATHER",
    HUNTER = "MAIL", SHAMAN = "MAIL",
    WARRIOR = "PLATE", PALADIN = "PLATE", DEATHKNIGHT = "PLATE",
}

-- ============================================================================
-- STATE
-- ============================================================================

CS.sets = {}
CS.loaded = false
CS.playerArmor = "CLOTH"
CS.resolveComplete = false

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

function CS:Initialize()
    local _, playerClass = UnitClass("player")
    self.playerArmor = CLASS_ARMOR[playerClass] or "CLOTH"

    self:LoadData()

    -- Start async source resolution after game has fully settled
    -- Delay longer to avoid competing with other init work
    C_Timer.After(5, function()
        self:ResolveAllSources()
    end)
end

function CS:LoadData()
    if self.loaded then return end
    if not ETB.CommunitySetData then
        ETB:Debug("CommunitySets", "CommunitySetData not loaded yet")
        return
    end

    local count = 0
    for i, raw in ipairs(ETB.CommunitySetData) do
        local name = raw[1]
        local armorType = raw[2]
        local expansion = raw[3]
        local filterType = raw[4]
        local rawItems = raw[5]

        local armorKey = ARMOR_NAMES[armorType] or "CLOTH"
        local setID = "COMMUNITY_" .. i

        -- Include ALL items unconditionally — names, icons, and collection
        -- status are resolved async by ResolveAllSources()
        local items = {}
        local pieceCount = 0
        for invSlot, itemData in pairs(rawItems) do
            items[invSlot] = {
                itemID = itemData[1],
                visualID = itemData[2],
                sourceID = 0,
                name = nil,
                icon = nil,
                quality = 2,
                isCollected = false,
            }
            pieceCount = pieceCount + 1
        end

        -- Skip sets with no items (broken entries)
        if pieceCount > 0 then
            self.sets[setID] = {
                id = setID,
                name = name,
                armorType = armorKey,
                armorDisplay = ARMOR_DISPLAY[armorType] or "Unknown",
                expansion = expansion,
                expansionName = EXP_NAMES[expansion] or "Unknown",
                filterType = filterType,
                filterName = FILTER_NAMES[filterType] or "Unknown",
                source = string.format("%s %s", EXP_NAMES[expansion] or "", FILTER_NAMES[filterType] or ""),
                items = items,
                pieceCount = pieceCount,
                collectedCount = 0,
                isCommunitySet = true,
                isUsable = (armorKey == self.playerArmor) or (armorType == 5) or (armorType == 13),
            }
            count = count + 1
        end
    end

    self.loaded = true
    ETB:Debug("CommunitySets", count .. " community sets loaded")
end

-- ============================================================================
-- RUNTIME SOURCE RESOLUTION (async batch)
-- ============================================================================

function CS:ResolveAllSources()
    if not self.loaded then return end

    local queue = {}
    for setID, set in pairs(self.sets) do
        for invSlot, item in pairs(set.items) do
            table.insert(queue, { set = set, slot = invSlot, item = item })
        end
    end

    local index = 0
    local BATCH_SIZE = 8           -- Max items per frame (was 25 — way too many)
    local TIME_BUDGET = 0.006      -- 6ms max per frame to avoid hitches
    local resolveFrame = CreateFrame("Frame")
    local getTime = debugprofilestop or function() return 0 end
    local itemsReplaced = 0        -- Track post-MoP item replacements

    resolveFrame:SetScript("OnUpdate", function(self, elapsed)
        local startTime = getTime()
        local processed = 0

        while processed < BATCH_SIZE and index < #queue do
            -- Time check every item to stay under budget
            if processed > 0 and (getTime() - startTime) > (TIME_BUDGET * 1000) then
                break
            end

            index = index + 1
            local entry = queue[index]
            local item = entry.item
            local set = entry.set

            -- v15.8.12: Determine the armor types acceptable for this set's
            -- source-replacement. Same-armor-type sets (Cloth/Leather/Mail/
            -- Plate) must only resolve to sources of that armor type — never
            -- rewrite a Plate set entry to a Mail itemID just because the
            -- user has the mail version collected. Cosmetic sets (5/13)
            -- and weapons/misc accept any armor type.
            local setArmorType = set and set.armorType  -- numeric 1-4 or 5/13
            local setIsCosmetic = (setArmorType == 5) or (setArmorType == 13) or not setArmorType
            local function _sourceMatchesSetArmor(srcInfo)
                if setIsCosmetic then return true end
                if not srcInfo or not srcInfo.itemID then return true end
                -- Check the source's underlying item's armor classification
                if not GetItemInfoInstant then return true end
                local _, _, _, _, _, classID, subclassID = GetItemInfoInstant(srcInfo.itemID)
                if classID ~= 4 then return true end  -- not armor → fine for any set
                if subclassID == 0 or subclassID == 6 then return true end  -- misc / shield
                -- subclassID 1..4 maps to CLOTH/LEATHER/MAIL/PLATE which
                -- directly aligns with our setArmorType numeric value
                return subclassID == setArmorType
            end

            -- 1. Load item info FIRST via sync API (no network, no hitch)
            -- IMPORTANT: On MoP Classic 5.4.8, GetItemInfoInstant returns
            --   (itemID, itemType, itemSubType, equipLoc, icon, classID, subclassID)
            -- The first return is the NUMERIC itemID, not a name. Earlier code
            -- stored that number as item.name; now we use GetItemInfoInstant
            -- only for "does this item exist" + icon lookup, and try
            -- GetItemInfo for the actual string name (cached, may be nil if
            -- the client hasn't seen this item yet).
            local itemExists = false
            if GetItemInfoInstant then
                local instantID, _, _, _, iIcon = GetItemInfoInstant(item.itemID)
                if instantID then
                    -- instantID matches item.itemID — its presence just confirms
                    -- the item is known to the client. Don't store it as a name.
                    if iIcon then item.icon = iIcon end
                    -- Try to get the real string name from the cached item DB
                    local realName = GetItemInfo and GetItemInfo(item.itemID)
                    if realName then item.name = realName end
                    -- If realName is nil, leave whatever item.name was already
                    -- set by upstream ATT data — better than overwriting with junk.
                    itemExists = true
                end
            end

            -- 2. Resolve source from visualID (sync API, no network)
            local resolvedVID = ETB:ResolveVisualID(item)
            if resolvedVID
               and C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
                local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, resolvedVID)
                if ok and sources then
                    local bestSourceID = nil
                    local bestItemID = nil
                    local bestCollected = false
                    local bestQuality = nil
                    
                    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)
                            -- v15.8.12: only consider sources matching this set's armor type
                            if srcOK and srcInfo and _sourceMatchesSetArmor(srcInfo) then
                                -- Always track a valid replacement itemID
                                local srcItemID = srcInfo.itemID

                                if srcInfo.isCollected then
                                    -- Collected source: best choice
                                    bestSourceID = sourceID
                                    bestCollected = true
                                    bestQuality = srcInfo.quality
                                    if srcItemID and srcItemID > 0 then
                                        bestItemID = srcItemID
                                    end
                                    break  -- Found collected, stop searching
                                elseif not bestSourceID then
                                    -- First valid source as fallback
                                    bestSourceID = sourceID
                                    bestQuality = srcInfo.quality
                                    if srcItemID and srcItemID > 0 then
                                        bestItemID = srcItemID
                                    end
                                end
                            end
                        end
                    end
                    
                    if bestSourceID then
                        item.sourceID = bestSourceID
                        if bestCollected then item.isCollected = true end
                        if type(bestQuality) == "number" then item.quality = bestQuality end
                    end
                    
                    -- KEY FIX: If original itemID doesn't exist in MoP client,
                    -- replace it with a valid itemID from the same appearance
                    if not itemExists and bestItemID and bestItemID > 0 then
                        -- v14.7.0: was replaceName/replaceIcon — but on MoP 5.4.8
                        -- the first GetItemInfoInstant return is the numeric itemID,
                        -- not a name. Use it as a truthy "exists" check, get the
                        -- real name via GetItemInfo (cached, may be nil).
                        local replaceID, _, _, _, replaceIcon = GetItemInfoInstant(bestItemID)
                        if replaceID then
                            item.itemID = bestItemID
                            local realName = GetItemInfo and GetItemInfo(bestItemID)
                            if realName then item.name = realName end
                            item.icon = replaceIcon
                            itemExists = true
                            itemsReplaced = itemsReplaced + 1
                        end
                    end
                end
            end

            -- 2b. Fallback: if step 2 yielded no collected source,
            --     try via itemID → GetItemInfo → fresh runtime visualID
            --     (Retail visualID may differ from MoP Classic)
            if not item.isCollected and item.itemID
               and C_TransmogCollection and C_TransmogCollection.GetItemInfo then
                local iOK, iVisualID = pcall(C_TransmogCollection.GetItemInfo, item.itemID)
                if iOK and iVisualID and iVisualID > 0 then
                    local ok2, sources2 = pcall(C_TransmogCollection.GetAppearanceSources, iVisualID)
                    if ok2 and sources2 then
                        for _, src in ipairs(sources2) do
                            local sourceID = type(src) == "table" and src.sourceID or src
                            if sourceID then
                                local srcOK, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
                                -- v15.8.12: same armor-type guard as the primary walk
                                if srcOK and srcInfo and _sourceMatchesSetArmor(srcInfo) then
                                    if srcInfo.isCollected then
                                        item.sourceID = sourceID
                                        item.isCollected = true
                                        if type(srcInfo.quality) == "number" then item.quality = srcInfo.quality end
                                        if iVisualID ~= item.visualID then
                                            item.visualID = iVisualID
                                        end
                                        -- Replace invalid itemID from this path
                                        if not itemExists and srcInfo.itemID and srcInfo.itemID > 0 then
                                            -- v14.7.0: same fix as above — first return is itemID
                                            local rID, _, _, _, rIcon = GetItemInfoInstant(srcInfo.itemID)
                                            if rID then
                                                item.itemID = srcInfo.itemID
                                                local realName = GetItemInfo and GetItemInfo(srcInfo.itemID)
                                                if realName then item.name = realName end
                                                item.icon = rIcon
                                                itemExists = true
                                                itemsReplaced = itemsReplaced + 1
                                            end
                                        end
                                        break
                                    elseif item.sourceID == 0 then
                                        item.sourceID = sourceID
                                        if type(srcInfo.quality) == "number" then item.quality = srcInfo.quality end
                                        -- Replace invalid itemID even for uncollected
                                        if not itemExists and srcInfo.itemID and srcInfo.itemID > 0 then
                                            -- v14.7.0: same fix
                                            local rID, _, _, _, rIcon = GetItemInfoInstant(srcInfo.itemID)
                                            if rID then
                                                item.itemID = srcInfo.itemID
                                                local realName = GetItemInfo and GetItemInfo(srcInfo.itemID)
                                                if realName then item.name = realName end
                                                item.icon = rIcon
                                                itemExists = true
                                                itemsReplaced = itemsReplaced + 1
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end

            -- 2c. Fallback: check PlayerHasTransmog if available
            if not item.isCollected and item.sourceID and item.sourceID > 0 then
                if C_TransmogCollection.PlayerHasTransmog then
                    local hasOK, hasTmog = pcall(C_TransmogCollection.PlayerHasTransmog, item.sourceID)
                    if hasOK and hasTmog then
                        item.isCollected = true
                    end
                end
                if not item.isCollected and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
                    local hasOK, hasTmog = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, item.sourceID)
                    if hasOK and hasTmog then
                        item.isCollected = true
                    end
                end
            end

            -- 3. Try full GetItemInfo only if Instant didn't resolve name
            --    This is async and can trigger server requests, so do it sparingly
            if not item.name or type(item.name) == "number" then
                local name, link, quality, _, _, _, _, _, _, icon = GetItemInfo(item.itemID)
                if name then
                    item.name = name
                    item.icon = icon
                    if type(quality) == "number" then item.quality = quality end
                end
            end

            processed = processed + 1
        end

        -- Update collection counts every ~400 items or at end
        if index % 400 == 0 or index >= #queue then
            CS:UpdateCollectionCounts()
        end

        if index >= #queue then
            self:SetScript("OnUpdate", nil)
            CS.resolveComplete = true
            CS:UpdateCollectionCounts()
            ETB:Debug("CommunitySets", string.format("Source resolution complete (%d items, %d post-MoP items replaced)", index, itemsReplaced))
            if itemsReplaced > 0 then
                ETB:Debug("CommunitySets", string.format("Replaced %d post-MoP itemIDs with valid MoP alternatives via visualID", itemsReplaced))
            end

            -- Refresh SetsUI after community resolution completes
            if ETB.SetsUI and ETB.SetsUI.frame and ETB.SetsUI.frame:IsShown() then
                ETB.SetsUI:RefreshSetList()
            end
            
            -- Also refresh TransmogUI sets panel if it's open
            if ETB.TransmogUI and ETB.TransmogUI.frame and ETB.TransmogUI.frame:IsShown() then
                if ETB.TransmogUI.RefreshSets then
                    ETB.TransmogUI:RefreshSets()
                end
            end
        end
    end)
end

function CS:UpdateCollectionCounts()
    if not C_TransmogCollection then return end
    
    for setID, set in pairs(self.sets) do
        local collected = 0
        for _, item in pairs(set.items) do
            -- Re-check live collection status via API
            local wasCollected = item.isCollected
            
            -- Method 1: sourceID-based check (most reliable)
            if item.sourceID and item.sourceID > 0 then
                if C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
                    local ok, result = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, item.sourceID)
                    if ok and result then
                        item.isCollected = true
                    end
                end
            end
            
            -- Method 2: appearance sources check (catches shared appearances)
            if not item.isCollected and item.visualID and item.visualID > 0 then
                if C_TransmogCollection.GetAppearanceSources then
                    local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, item.visualID)
                    if ok and sources then
                        for _, src in ipairs(sources) do
                            local sid = type(src) == "table" and src.sourceID or src
                            if sid then
                                local siOK, si = pcall(C_TransmogCollection.GetSourceInfo, sid)
                                if siOK and si and si.isCollected then
                                    item.isCollected = true
                                    if item.sourceID == 0 then
                                        item.sourceID = sid
                                    end
                                    break
                                end
                            end
                        end
                    end
                end
            end
            
            if item.isCollected then
                collected = collected + 1
            end
        end
        set.collectedCount = collected
    end
end

-- ============================================================================
-- QUERY API (matches CustomSets interface)
-- ============================================================================

function CS:GetAllSets()
    if not self.loaded then self:LoadData() end
    return self.sets
end

function CS:GetSet(setID)
    return self.sets[setID]
end

function CS:GetSetCompletion(setID)
    local set = self.sets[setID]
    if not set then return 0, 0 end
    return set.collectedCount, set.pieceCount
end

function CS:GetSetCount()
    local count = 0
    for _ in pairs(self.sets) do count = count + 1 end
    return count
end

function CS:IsSetUsable(setID)
    local set = self.sets[setID]
    if not set then return false end
    return set.isUsable
end

-- ============================================================================
-- AUDIT  (v15.8.13)
-- ============================================================================
-- Walks every loaded community set and reports per-slot issues:
--   * "wrong-armor-itemID" — the itemID in the data file is for a different
--     armor type than the set's declared armorType. (e.g. a Plate set entry
--     pointing to a Mail itemID.)
--   * "unloaded" — the itemID doesn't resolve to a known item via the client
--     (GetItemInfoInstant returns nothing). Usually means the item doesn't
--     exist on this client version.
--
-- Returns a report table:
--   { totalSets = N, totalSlots = M, mismatches = {{setName, slot, ...}},
--     unloaded = {...} }
--
-- This is a diagnostic tool. The runtime resolver (v15.8.12) already refuses
-- to silently rewrite cross-armor-type itemIDs, but the data file itself
-- still has whatever wrong-armor entries the upstream MoP Classic dump had.
-- This audit produces the list to fix.

function CS:Audit()
    local report = {
        totalSets = 0,
        totalSlots = 0,
        mismatches = {},  -- entries with itemID of wrong armor type for the set
        unloaded = {},    -- entries whose itemID doesn't resolve client-side
        misc = {},        -- shields/cloaks/etc; tracked for completeness
    }

    if not self.loaded then
        return report, "Community sets not loaded yet"
    end

    -- Map set's numeric armorType (1=Cloth..4=Plate, 5=Cosmetic, 13=Cosmetic)
    -- to the subclassID that GetItemInfoInstant returns for armor (1..4).
    -- Cosmetic (5/13) accepts any armor type.
    for setID, set in pairs(self.sets) do
        report.totalSets = report.totalSets + 1
        local setArmorType = set.armorType
        local isCosmetic = (setArmorType == 5) or (setArmorType == 13)

        for invSlot, item in pairs(set.items or {}) do
            report.totalSlots = report.totalSlots + 1
            local itemID = item.itemID
            if itemID and itemID > 0 then
                local resolved, _, _, _, _, classID, subclassID = GetItemInfoInstant(itemID)
                if not resolved then
                    table.insert(report.unloaded, {
                        setName = set.name, setID = setID, invSlot = invSlot,
                        itemID = itemID,
                    })
                elseif classID == 4 and not isCosmetic then
                    -- Armor item; check subclass against set's declared armorType.
                    -- subclass 0 = misc (cloaks/shirts/tabards) — any class OK.
                    -- subclass 6 = shield — bypass.
                    -- subclass 1..4 = cloth/leather/mail/plate
                    if subclassID == 0 or subclassID == 6 then
                        table.insert(report.misc, {
                            setName = set.name, setID = setID, invSlot = invSlot,
                            itemID = itemID, subclassID = subclassID,
                        })
                    elseif subclassID ~= setArmorType then
                        -- Mismatch: e.g. set says Plate (4) but item is Mail (3)
                        local armorNames = { [1]="Cloth", [2]="Leather", [3]="Mail", [4]="Plate" }
                        table.insert(report.mismatches, {
                            setName = set.name, setID = setID, invSlot = invSlot,
                            itemID = itemID,
                            setArmor = armorNames[setArmorType] or tostring(setArmorType),
                            itemArmor = armorNames[subclassID] or tostring(subclassID),
                        })
                    end
                end
                -- classID 2 (weapon) or non-armor: not checked here
            end
        end
    end

    return report
end

-- Pretty-prints the audit report to chat, suitable for /etb auditcs.
-- For large reports, also dumps to a SavedVariable for offline review.
function CS:PrintAuditReport(report)
    if not report then return end
    print(string.format("|cff00ffff[ETB CS Audit]|r %d sets, %d slots scanned",
        report.totalSets, report.totalSlots))

    if #report.mismatches == 0 then
        print("  |cff00ff00No wrong-armor-type mismatches found.|r")
    else
        print(string.format("  |cffff6666%d wrong-armor-type mismatches:|r", #report.mismatches))
        -- Cap chat output at 20 entries; full list goes to SV
        local cap = math.min(20, #report.mismatches)
        for i = 1, cap do
            local m = report.mismatches[i]
            print(string.format("    [%d] |cffff9900%s|r slot %s: %s set has %s item (id %d)",
                i, m.setName, tostring(m.invSlot), m.setArmor, m.itemArmor, m.itemID))
        end
        if #report.mismatches > cap then
            print(string.format("    ... and %d more (full list in ETB_CommunitySetsAudit SV)",
                #report.mismatches - cap))
        end
    end

    if #report.unloaded > 0 then
        print(string.format("  |cffaaaaaa%d items not loaded by client (likely retail-only IDs)|r",
            #report.unloaded))
    end

    -- Persist full report to SavedVariable for offline analysis
    ETB_CommunitySetsAudit = {
        timestamp = time(),
        totalSets = report.totalSets,
        totalSlots = report.totalSlots,
        mismatches = report.mismatches,
        unloaded = report.unloaded,
        misc = report.misc,
    }
    print("|cff00ffff[ETB CS Audit]|r Full report saved to |cffaaaaaaETB_CommunitySetsAudit|r SavedVariable.")
end

-- ============================================================================
-- INITIALIZATION HOOK
-- ============================================================================

local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("PLAYER_LOGIN")
initFrame:SetScript("OnEvent", function(self, event)
    C_Timer.After(2, function()
        CS:Initialize()
    end)
end)

ETB:Debug("CommunitySets", "CommunitySetLoader.lua loaded")
