--[[
============================================================================
SetCompletion.lua  (added in v15.8.16)
============================================================================

Detects when a transmog set transitions from incomplete to fully collected
and fires ETB_SET_COMPLETED for CollectionToast (or any other listener) to
show a notification.

Sources of set data this module unifies:
  1. C_TransmogSets.GetAllSets()                  -- Blizzard's official sets
  2. ETB.CommunitySetData (loaded by CommunitySetLoader)  -- our 702 sets

Both kinds of sets go through the same completion-detection path, so the
toast looks unified (per user spec: "Q1: Keep it unified").

----------------------------------------------------------------------------
HOW SET COMPLETION IS DETECTED
----------------------------------------------------------------------------

At PLAYER_LOGIN we build two indexes:
  * setRegistry  : { [setKey] = { name=, kind=, pieces={visualID, ...} } }
                   setKey is "blz:<setID>" for Blizzard sets or
                            "com:<index>" for community sets.
  * visualToSets : { [visualID] = { setKey, setKey, ... } }   reverse index

At PLAYER_LOGIN + 4s we snapshot which sets are ALREADY COMPLETE on this
account. Those go into ETB_CompletedSets SavedVariable and are NEVER toasted.
This is the "Q3: only toast NEW completions after install" requirement.

Thereafter, every ETB_ITEM_COLLECTED fires _onItemCollected which:
  1. Resolves the itemID to its visualID (via C_TransmogCollection.GetItemInfo)
  2. Looks up which sets contain that visualID
  3. For each such set, checks if every piece is now collected
  4. If yes AND not already in ETB_CompletedSets — adds it and fires
     ETB_SET_COMPLETED with the set info

----------------------------------------------------------------------------
WHY THIS DESIGN
----------------------------------------------------------------------------

The reverse index makes per-item-collection checks O(set-count-for-this-visual)
instead of O(total-sets-in-game). Most visualIDs belong to 1-3 sets.

The "completed at install" snapshot avoids spamming 252 toasts on first
launch (per Q3).

The setKey namespacing ("blz:" / "com:") lets the SavedVariable persist
both kinds and survive data updates: when a community set's index shifts
because of a data refresh, we'd re-detect completion only if NEW visuals
are added — that's acceptable.

============================================================================
]]

local ADDON_NAME, NS = ...
NS.ETB = NS.ETB or {}
local ETB = NS.ETB

NS.ETB.SetCompletion = NS.ETB.SetCompletion or {}
local SC = NS.ETB.SetCompletion

SC._initialized = false
SC._setRegistry = {}       -- setKey → set record
SC._visualToSets = {}      -- visualID → { setKey, ... }

-- ============================================================================
-- BUILDING THE REGISTRY
-- ============================================================================

local function _addToVisualIndex(visualID, setKey)
    if not visualID or not setKey then return end
    SC._visualToSets[visualID] = SC._visualToSets[visualID] or {}
    -- Avoid duplicates per visualID
    for _, k in ipairs(SC._visualToSets[visualID]) do
        if k == setKey then return end
    end
    table.insert(SC._visualToSets[visualID], setKey)
end

local function _buildBlizzardSetIndex()
    if not C_TransmogSets or not C_TransmogSets.GetAllSets then return end
    local ok, allSets = pcall(C_TransmogSets.GetAllSets)
    if not ok or not allSets then return end

    for _, info in ipairs(allSets) do
        if info and info.setID and info.name then
            local setKey = "blz:" .. info.setID
            local pieces = {}

            -- GetSetPrimaryAppearances returns { {appearanceID=, sourceID=}, ... }
            if C_TransmogSets.GetSetPrimaryAppearances then
                local appOK, appearances = pcall(C_TransmogSets.GetSetPrimaryAppearances, info.setID)
                if appOK and appearances then
                    for _, app in ipairs(appearances) do
                        local visualID = app.appearanceID or app.visualID
                        if visualID then
                            table.insert(pieces, visualID)
                            _addToVisualIndex(visualID, setKey)
                        end
                    end
                end
            end

            -- Fall back to GetAllSourceIDs if no primary appearances came back
            if #pieces == 0 and C_TransmogSets.GetAllSourceIDs and C_TransmogCollection
               and C_TransmogCollection.GetSourceInfo then
                local sOK, sourceIDs = pcall(C_TransmogSets.GetAllSourceIDs, info.setID)
                if sOK and sourceIDs then
                    local seen = {}
                    for _, sid in ipairs(sourceIDs) do
                        local siOK, si = pcall(C_TransmogCollection.GetSourceInfo, sid)
                        if siOK and si and si.visualID and not seen[si.visualID] then
                            seen[si.visualID] = true
                            table.insert(pieces, si.visualID)
                            _addToVisualIndex(si.visualID, setKey)
                        end
                    end
                end
            end

            if #pieces > 0 then
                SC._setRegistry[setKey] = {
                    name = info.name,
                    kind = "blizzard",
                    blzSetID = info.setID,
                    pieces = pieces,
                }
            end
        end
    end
end

local function _buildCommunitySetIndex()
    if not ETB.CommunitySetData then return end
    for i, entry in ipairs(ETB.CommunitySetData) do
        -- Schema: {name, armorType, expansion, filterType, {[invSlot]={itemID,visualID}}}
        if type(entry) == "table" and entry[1] and type(entry[5]) == "table" then
            local setKey = "com:" .. i
            local pieces = {}
            for _, slotEntry in pairs(entry[5]) do
                if type(slotEntry) == "table" and slotEntry[2] and slotEntry[2] > 0 then
                    table.insert(pieces, slotEntry[2])
                    _addToVisualIndex(slotEntry[2], setKey)
                end
            end
            if #pieces > 0 then
                SC._setRegistry[setKey] = {
                    name = entry[1],
                    kind = "community",
                    pieces = pieces,
                }
            end
        end
    end
end

-- ============================================================================
-- COMPLETION CHECK
-- ============================================================================
--
-- A set is "complete" iff every visualID in its pieces list is collected
-- on this character account. We reuse ETB.CollectionAPI._isAppearanceCollected
-- when available (handles the multi-source case correctly), otherwise fall
-- back to PlayerHasTransmog.

local function _isVisualCollected(visualID)
    if not visualID then return false end
    -- Preferred path: ETB's collection helper (knows about all sources)
    if ETB.CollectionAPI and ETB.CollectionAPI.IsAppearanceCollected then
        local ok, res = pcall(ETB.CollectionAPI.IsAppearanceCollected, ETB.CollectionAPI, visualID)
        if ok then return res end
    end
    -- Fallback: native API on MoP 5.4.8
    if C_TransmogCollection and C_TransmogCollection.GetAllAppearanceSources then
        local sOK, sources = pcall(C_TransmogCollection.GetAllAppearanceSources, visualID)
        if sOK and sources then
            for _, sid in ipairs(sources) do
                if C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
                    local hk, has = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, sid)
                    if hk and has then return true end
                end
            end
        end
    end
    if PlayerHasTransmog then
        local ok, has = pcall(PlayerHasTransmog, visualID)
        if ok and has then return true end
    end
    return false
end

function SC:IsSetComplete(setKey)
    local rec = self._setRegistry[setKey]
    if not rec then return false end
    for _, visualID in ipairs(rec.pieces) do
        if not _isVisualCollected(visualID) then return false end
    end
    return true
end

-- ============================================================================
-- BASELINE SNAPSHOT (Q3: don't fire toasts for sets already complete at install)
-- ============================================================================
--
-- ETB_CompletedSets is the persistent "already celebrated" SavedVariable.
-- On first ever run, this is empty; we scan every set, mark complete ones
-- as "baseline-completed" (in the SV) WITHOUT firing toasts, then thereafter
-- only NEW completions fire.
--
-- The SV is per-account. If the user installs on alt #2 the baseline takes
-- their alt #2 collection into account on first run. Subsequent collections
-- on alt #1 that complete sets fire toasts when the user logs back into alt
-- #1, because PlayerHasTransmog is per-character (well, account-wide for
-- transmog appearances) but the check still works.

-- v15.8.17: chunked baseline scan. Walks the set registry in slices of
-- BASELINE_CHUNK_SIZE per frame so we don't freeze the client for several
-- frames at +3s post-login. Total work is the same but spread across many
-- frames; total wall-clock time is slightly higher but each frame stays
-- under the 16ms render budget.
--
-- We MUST complete the baseline before subscribing to ETB_ITEM_COLLECTED.
-- Wire-up of the listener happens in the final chunk's continuation.

local BASELINE_CHUNK_SIZE = 40   -- sets scanned per frame
local INDEX_CHUNK_SIZE = 50      -- Blizzard sets indexed per frame

function SC:_takeBaselineIfNeeded(onComplete)
    ETB_CompletedSets = ETB_CompletedSets or {}
    if ETB_CompletedSets._baselineTaken then
        if onComplete then onComplete(0) end
        return
    end

    -- Flatten set keys into an indexable list so we can chunk over them
    local keys = {}
    for setKey in pairs(self._setRegistry) do
        table.insert(keys, setKey)
    end

    local cursor, count = 1, 0
    local function processChunk()
        if ETB.IsStartupIdle and not ETB:IsStartupIdle({ ignoreQuietWindow = true }) then
            C_Timer.After(1, processChunk)
            return
        end
        local endIdx = math.min(cursor + BASELINE_CHUNK_SIZE - 1, #keys)
        local _bcT0 = ETB.Perf and debugprofilestop and debugprofilestop()
        for i = cursor, endIdx do
            local setKey = keys[i]
            if SC:IsSetComplete(setKey) then
                ETB_CompletedSets[setKey] = time()
                count = count + 1
            end
        end
        if _bcT0 then ETB.Perf:Record("setCompletion.baselineChunk", debugprofilestop() - _bcT0) end
        cursor = endIdx + 1
        if cursor <= #keys then
            C_Timer.After(0, processChunk)  -- next frame
        else
            ETB_CompletedSets._baselineTaken = true
            ETB_CompletedSets._baselineCount = count
            ETB_CompletedSets._baselineTime = time()
            if ETB.Debug then
                ETB:Debug("SetCompletion", string.format(
                    "Baseline complete (chunked): %d sets already complete at install — these will NOT toast", count))
            end
            if onComplete then onComplete(count) end
        end
    end

    -- Kick off
    if #keys == 0 then
        ETB_CompletedSets._baselineTaken = true
        ETB_CompletedSets._baselineCount = 0
        if onComplete then onComplete(0) end
        return
    end
    processChunk()
end

-- ============================================================================
-- ITEM-COLLECTED HANDLER
-- ============================================================================

function SC:_onItemCollected(event, itemID, record)
    if not itemID then return end
    -- v15.8.17: defensive guard — should only fire after baseline completes,
    -- but skip silently if somehow invoked early.
    if not self._listenerReady then return end

    -- Resolve item → visualID
    local visualID
    if C_TransmogCollection and C_TransmogCollection.GetItemInfo then
        local ok, appID = pcall(C_TransmogCollection.GetItemInfo, itemID)
        if ok and appID then visualID = appID end
    end
    if not visualID then return end

    -- Look up sets containing this visual
    local affectedSets = self._visualToSets[visualID]
    if not affectedSets then return end

    for _, setKey in ipairs(affectedSets) do
        if not (ETB_CompletedSets and ETB_CompletedSets[setKey]) then
            -- Not yet celebrated. Recompute completion.
            if self:IsSetComplete(setKey) then
                local rec = self._setRegistry[setKey]
                if rec then
                    ETB_CompletedSets = ETB_CompletedSets or {}
                    ETB_CompletedSets[setKey] = time()
                    if ETB.EventBus then
                        ETB.EventBus:Fire("ETB_SET_COMPLETED", setKey, rec, itemID)
                    end
                end
            end
        end
    end
end

-- ============================================================================
-- INIT
-- ============================================================================

function SC:Initialize()
    if self._initialized then return end

    -- Build indexes. The Blizzard side runs immediately; the community side
    -- depends on CommunitySetData being loaded (CommunitySetLoader fires
    -- Initialize at PLAYER_LOGIN+2s; ETB Phase 2 init runs at +3s; we're
    -- called from Phase 2). The data should be present. These are synchronous
    -- (not chunked); instrument them so the first-run cost is visible in /etb perf
    -- instead of hiding inside an uninstrumented RunSpread/timer callback.
    local _biT0 = ETB.Perf and debugprofilestop and debugprofilestop()
    _buildBlizzardSetIndex()
    _buildCommunitySetIndex()
    if _biT0 then ETB.Perf:Record("setCompletion.buildIndex", debugprofilestop() - _biT0) end

    local setCount = 0
    for _ in pairs(self._setRegistry) do setCount = setCount + 1 end

    -- v15.8.17: baseline scan runs ASYNCHRONOUSLY across multiple frames.
    -- We don't subscribe to ETB_ITEM_COLLECTED until baseline is complete
    -- — otherwise the listener would mistake "already collected" appearances
    -- for new ones during the scan and spam toasts.
    self:_takeBaselineIfNeeded(function(_)
        if ETB.EventBus then
            ETB.EventBus:Register("ETB_ITEM_COLLECTED", function(event, itemID, record)
                SC:_onItemCollected(event, itemID, record)
            end)
        end
        SC._listenerReady = true
        if ETB.Debug then
            ETB:Debug("SetCompletion", "Item-collected listener now active")
        end
    end)

    self._initialized = true
    if ETB.Debug then
        ETB:Debug("SetCompletion", string.format(
            "Initialized: %d sets indexed (%d Blizzard, %d community), %d visualID entries in reverse index",
            setCount,
            (function() local n=0 for _,r in pairs(self._setRegistry) do if r.kind=="blizzard" then n=n+1 end end return n end)(),
            (function() local n=0 for _,r in pairs(self._setRegistry) do if r.kind=="community" then n=n+1 end end return n end)(),
            (function() local n=0 for _ in pairs(self._visualToSets) do n=n+1 end return n end)()))
    end
end

-- Debug helpers for /etb commands
function SC:GetStats()
    local blz, com = 0, 0
    for _, rec in pairs(self._setRegistry) do
        if rec.kind == "blizzard" then blz = blz + 1
        elseif rec.kind == "community" then com = com + 1 end
    end
    local visEntries = 0
    for _ in pairs(self._visualToSets) do visEntries = visEntries + 1 end
    local celebrated = 0
    if ETB_CompletedSets then
        for k, _ in pairs(ETB_CompletedSets) do
            if not k:find("^_") then celebrated = celebrated + 1 end
        end
    end
    return {
        blzSets = blz,
        communitySets = com,
        totalSets = blz + com,
        visualEntries = visEntries,
        celebratedSets = celebrated,
    }
end

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