-- =============================================================================
-- ETB v12.53.0 — Modules/SetBuilder/Items.lua
-- =============================================================================
-- Second SetBuilder submodule. Owns item loading, filtering, and display.
--
-- Three responsibilities:
--
-- 1. ITEM LOADING (async, multi-phase):
--    LoadAllItems → ResolveItemNames → VerifyAndFinish → FinishLoading
--    Iterates valid transmog categories, pulls all appearances+sources from
--    the Blizzard API, builds the in-memory item list, then drives the async
--    name-resolution loop (server-side GetItemInfo round-trips).
--
-- 2. FILTERING:
--    FilterItems — applies search text, slot filter, armor type filter to
--    owner.allItems and writes the result to owner.filteredItems.
--
-- 3. DISPLAY:
--    DisplayItems — paginates owner.filteredItems and renders item button
--    rows into owner.scrollChild. Wires up Add-button OnClick (delegates
--    back to owner:AddToSet which now lives in Export.lua) and tooltips.
--    ScheduleNameUpdate retries name resolution for visible items.
--    UpdateSlotHighlight tints owner.selectedSlot's background to indicate
--    the active slot; all other slots use the default tint.
--
-- Public API (all take owner = SB singleton as first arg):
--   Items:LoadAllItems(owner)
--   Items:ResolveItemNames(owner)
--   Items:VerifyAndFinish(owner)
--   Items:FinishLoading(owner)
--   Items:FilterItems(owner)
--   Items:DisplayItems(owner)
--   Items:ScheduleNameUpdate(owner, items)
--   Items:UpdateSlotHighlight(owner)
--
-- Owner contract — fields read/written:
--   owner.allItems, .filteredItems, .pendingNames
--   owner.loadingComplete, .loadStats
--   owner.searchText, .filterSlot, .filterArmorType
--   owner.currentPage, .itemsPerPage
--   owner.itemButtons, .itemInfoFrame
--   owner.statsText, .pageText, .scrollChild, .slotFrames
--
-- Owner-exposed file-locals (set in main SetBuilder.lua):
--   owner.SLOT_NAMES, .INVTYPE_TO_SLOT
--   owner.ARMOR_SUBCLASS_MAP, .CATEGORY_TO_SLOT
--
-- Owner methods called:
--   owner:AddToSet(item)  -- Add-button click handler
--
-- Globals/Blizzard:
--   C_TransmogCollection (GetCategoryAppearances, GetAppearanceSources)
--   GetItemInfoInstant, GetItemInfo
--   GetItemQualityColor
--   CreateFrame, C_Timer.After, GameTooltip
-- =============================================================================

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

NS.ETB.SetBuilder = NS.ETB.SetBuilder or ETB.SetBuilder or {}
local Items = {}
NS.ETB.SetBuilder.Items = Items

-- =============================================================================
-- ITEM LOADING — async multi-phase pipeline
-- =============================================================================

function Items:LoadAllItems(owner)
    owner.allItems = {}
    owner.pendingNames = {}  -- Track items needing name resolution
    owner.loadingComplete = false

    -- Comprehensive tracking to find where items are lost
    owner.loadStats = {
        byCategory = {},
        totalAppearances = 0,
        totalSources = 0,
        totalItems = 0,
        skippedNoItemID = 0,
        skippedNoSlot = 0,
        errors = 0,
    }

    owner.statsText:SetText("|cffffd100Loading transmog items...|r")

    -- Valid transmog categories in MoP Classic
    -- Categories 1-11 are armor slots, 12-13 are the main weapon categories
    local validCategories = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}

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

    local currentIndex = 1

    local function LoadCategory()
        if currentIndex > #validCategories then
            -- All categories loaded, now resolve pending names
            Items:ResolveItemNames(owner)
            return
        end

        local categoryID = validCategories[currentIndex]
        local catName = categoryNames[categoryID] or ("Category " .. categoryID)

        owner.statsText:SetText(string.format("|cffffd100Loading %s... (%d items so far)|r", catName, #owner.allItems))

        -- Track stats for this category
        local catStats = {
            name = catName,
            appearances = 0,
            sources = 0,
            items = 0,
            noItemID = 0,
            errors = 0,
        }

        -- Get appearances for this category
        local success, appearances = pcall(C_TransmogCollection.GetCategoryAppearances, categoryID)

        if not success then
            catStats.errors = catStats.errors + 1
            ETB:Print(string.format("|cffff0000[Set Builder]|r ERROR getting appearances for %s: %s", catName, tostring(appearances)))
        elseif appearances and #appearances > 0 then
            catStats.appearances = #appearances
            owner.loadStats.totalAppearances = owner.loadStats.totalAppearances + #appearances

            for _, appearance in ipairs(appearances) do
                -- Get ALL item sources for this appearance
                local srcSuccess, sources = pcall(C_TransmogCollection.GetAppearanceSources, appearance.visualID)

                if not srcSuccess then
                    catStats.errors = catStats.errors + 1
                elseif sources then
                    catStats.sources = catStats.sources + #sources
                    owner.loadStats.totalSources = owner.loadStats.totalSources + #sources

                    for _, source in ipairs(sources) do
                        if source.itemID then
                            -- Get item info from GetItemInfoInstant
                            local _, _, _, equipLoc, itemIcon, itemClassID, itemSubClassID = GetItemInfoInstant(source.itemID)

                            -- Determine type (armor or weapon)
                            local armorType = nil
                            local weaponType = nil
                            if itemClassID == 4 then -- Armor
                                armorType = owner.ARMOR_SUBCLASS_MAP[itemSubClassID]
                            elseif itemClassID == 2 then -- Weapon
                                weaponType = "WEAPON"
                            end

                            -- Map slot - ALWAYS assign a slot, never skip
                            local slot = owner.INVTYPE_TO_SLOT[equipLoc]
                            if not slot then
                                slot = owner.CATEGORY_TO_SLOT[categoryID]
                            end
                            if not slot then
                                -- Fallback: use category as slot for armor, 10 for weapons
                                if categoryID <= 11 then
                                    slot = categoryID
                                else
                                    slot = 10
                                end
                            end

                            -- Try to get name from GetItemInfo (may be nil if not cached)
                            local itemName, _, quality = GetItemInfo(source.itemID)

                            local item = {
                                itemID = source.itemID,
                                name = itemName,  -- May be nil
                                icon = itemIcon or source.itemTexture or "Interface\\Icons\\INV_Misc_QuestionMark",
                                slot = slot,
                                armorType = armorType,
                                weaponType = weaponType,
                                quality = quality or 2,
                                isCollected = source.isCollected,
                                visualID = appearance.visualID,
                                categoryID = categoryID,
                                sourceID = source.sourceID,  -- Track sourceID too
                            }

                            table.insert(owner.allItems, item)
                            catStats.items = catStats.items + 1
                            owner.loadStats.totalItems = owner.loadStats.totalItems + 1

                            -- Track items without names for later resolution
                            if not itemName then
                                owner.pendingNames[source.itemID] = item
                            end
                        else
                            catStats.noItemID = catStats.noItemID + 1
                            owner.loadStats.skippedNoItemID = owner.loadStats.skippedNoItemID + 1
                        end
                    end
                end
            end
        end

        owner.loadStats.byCategory[categoryID] = catStats
        owner.loadStats.errors = owner.loadStats.errors + catStats.errors

        currentIndex = currentIndex + 1

        -- Process next category after a brief delay to prevent freezing
        C_Timer.After(0.02, LoadCategory)
    end

    -- Start loading
    LoadCategory()
end

function Items:ResolveItemNames(owner)
    local pendingCount = 0
    for _ in pairs(owner.pendingNames) do
        pendingCount = pendingCount + 1
    end

    if pendingCount == 0 then
        Items:VerifyAndFinish(owner)
        return
    end

    ETB:Print(string.format("|cff00ffff[Set Builder]|r Resolving %d item names (this may take a moment)...", pendingCount))
    owner.statsText:SetText(string.format("|cffffd100Requesting %d item names...|r", pendingCount))

    -- PHASE 1: Request all items from server (GetItemInfo triggers server request)
    local requestCount = 0
    for itemID, item in pairs(owner.pendingNames) do
        GetItemInfo(itemID)  -- This triggers the server to send item data
        requestCount = requestCount + 1
    end

    ETB:Print(string.format("|cff00ffff[Set Builder]|r Requested %d items from server", requestCount))

    -- PHASE 2: Register for item info events and resolve in batches
    local resolved = 0
    local totalPending = pendingCount
    local retryCount = 0
    local maxRetries = 20  -- More retries since we're waiting for server

    -- Create event frame for GET_ITEM_INFO_RECEIVED
    if not owner.itemInfoFrame then
        owner.itemInfoFrame = CreateFrame("Frame")
    end

    local function ProcessPending()
        local stillPending = {}
        local newlyResolved = 0

        for itemID, item in pairs(owner.pendingNames) do
            local itemName, _, quality = GetItemInfo(itemID)
            if itemName then
                item.name = itemName
                if type(quality) == "number" then item.quality = quality end
                newlyResolved = newlyResolved + 1
            else
                stillPending[itemID] = item
            end
        end

        resolved = resolved + newlyResolved
        owner.pendingNames = stillPending

        -- Count remaining
        local remaining = 0
        for _ in pairs(owner.pendingNames) do
            remaining = remaining + 1
        end

        local percent = math.floor((resolved / totalPending) * 100)
        owner.statsText:SetText(string.format("|cffffd100Resolving: %d/%d (%d%%) - retry %d|r",
            resolved, totalPending, percent, retryCount))

        if remaining > 0 and retryCount < maxRetries then
            retryCount = retryCount + 1
            -- Re-request unresolved items
            local reRequested = 0
            for itemID, item in pairs(owner.pendingNames) do
                GetItemInfo(itemID)
                reRequested = reRequested + 1
                if reRequested > 1000 then break end  -- Batch requests
            end
            -- Longer delay to let server respond
            C_Timer.After(1.0, ProcessPending)
        else
            -- Done - accept remaining as unnamed
            local unnamed = 0
            for itemID, item in pairs(owner.pendingNames) do
                if not item.name then
                    item.name = "Item #" .. itemID
                    unnamed = unnamed + 1
                end
            end

            ETB:Print(string.format("|cff00ff00[Set Builder]|r Named %d/%d items (%.1f%%)",
                resolved, totalPending, (resolved/totalPending)*100))
            if unnamed > 0 then
                ETB:Print(string.format("|cffffff00[Set Builder]|r %d items remain unnamed (will show as Item #ID)", unnamed))
            end

            owner.pendingNames = nil
            Items:VerifyAndFinish(owner)
        end
    end

    -- Start processing after initial delay to let first batch of server responses arrive
    C_Timer.After(2.0, ProcessPending)
end

function Items:VerifyAndFinish(owner)
    -- VERIFICATION PHASE - Compare loaded items vs API totals
    owner.statsText:SetText("|cffffd100Verifying items...|r")

    -- Count totals by category and armor type
    local byCategory = {}
    local byArmorType = { CLOTH = 0, LEATHER = 0, MAIL = 0, PLATE = 0, OTHER = 0 }
    local bySlot = {}
    local namedCount = 0
    local unnamedCount = 0
    local totalItems = #owner.allItems
    local weaponCount = 0

    for _, item in ipairs(owner.allItems) do
        -- Count by category
        byCategory[item.categoryID] = (byCategory[item.categoryID] or 0) + 1

        -- Count by slot
        bySlot[item.slot] = (bySlot[item.slot] or 0) + 1

        -- Count by armor type
        if item.weaponType then
            weaponCount = weaponCount + 1
        elseif item.armorType then
            byArmorType[item.armorType] = byArmorType[item.armorType] + 1
        else
            byArmorType.OTHER = byArmorType.OTHER + 1
        end

        -- Count named vs unnamed
        if item.name and not item.name:match("^Item #") then
            namedCount = namedCount + 1
        else
            unnamedCount = unnamedCount + 1
        end
    end

    -- Print comprehensive verification report
    ETB:Print("|cff00ff00============================================|r")
    ETB:Print("|cff00ff00=== SET BUILDER LOAD VERIFICATION ===|r")
    ETB:Print("|cff00ff00============================================|r")

    -- API Stats (what we tried to load)
    ETB:Print("")
    ETB:Print("|cffffd100[API TOTALS]|r")
    ETB:Print(string.format("  Appearances tried: %d", owner.loadStats.totalAppearances))
    ETB:Print(string.format("  Sources found: %d", owner.loadStats.totalSources))
    ETB:Print(string.format("  Items created: %d", owner.loadStats.totalItems))
    if owner.loadStats.skippedNoItemID > 0 then
        ETB:Print(string.format("  |cffff0000Skipped (no itemID): %d|r", owner.loadStats.skippedNoItemID))
    end
    if owner.loadStats.errors > 0 then
        ETB:Print(string.format("  |cffff0000Errors: %d|r", owner.loadStats.errors))
    end

    -- Per-category breakdown
    ETB:Print("")
    ETB:Print("|cffffd100[BY CATEGORY]|r")
    local categoryNames = {
        [1] = "Head", [2] = "Shoulder", [3] = "Back", [4] = "Chest", [5] = "Shirt",
        [6] = "Tabard", [7] = "Wrist", [8] = "Hands", [9] = "Waist", [10] = "Legs", [11] = "Feet",
        [12] = "Main Hand", [13] = "Off Hand"
    }
    for catID = 1, 13 do
        local catStats = owner.loadStats.byCategory[catID]
        if catStats then
            local catName = categoryNames[catID] or ("Cat " .. catID)
            local loadedCount = byCategory[catID] or 0
            local status = ""
            if catStats.sources ~= loadedCount then
                status = string.format(" |cffff0000MISSING %d!|r", catStats.sources - loadedCount)
            elseif catStats.noItemID > 0 then
                status = string.format(" |cffffff00(%d no itemID)|r", catStats.noItemID)
            end
            ETB:Print(string.format("  %s: %d appearances → %d sources → %d items%s",
                catName, catStats.appearances, catStats.sources, loadedCount, status))
        end
    end

    -- Armor type breakdown
    ETB:Print("")
    ETB:Print("|cffffd100[BY ARMOR TYPE]|r")
    local armorTotal = byArmorType.CLOTH + byArmorType.LEATHER + byArmorType.MAIL + byArmorType.PLATE
    ETB:Print(string.format("  Cloth: %d | Leather: %d | Mail: %d | Plate: %d | Other: %d | Weapons: %d",
        byArmorType.CLOTH, byArmorType.LEATHER, byArmorType.MAIL, byArmorType.PLATE,
        byArmorType.OTHER, weaponCount))
    ETB:Print(string.format("  Total Armor: %d | Total Weapons: %d | Grand Total: %d",
        armorTotal + byArmorType.OTHER, weaponCount, totalItems))

    -- Names status
    ETB:Print("")
    ETB:Print("|cffffd100[NAMES STATUS]|r")
    ETB:Print(string.format("  Named: %d (%.1f%%) | Unnamed: %d (%.1f%%)",
        namedCount, (namedCount/math.max(1,totalItems))*100,
        unnamedCount, (unnamedCount/math.max(1,totalItems))*100))

    ETB:Print("|cff00ff00============================================|r")

    -- Now finish loading
    Items:FinishLoading(owner)
end

function Items:FinishLoading(owner)
    -- Sort by name (items without names go to end)
    table.sort(owner.allItems, function(a, b)
        local nameA = a.name or "~" .. a.itemID  -- ~ sorts after letters
        local nameB = b.name or "~" .. b.itemID
        return nameA < nameB
    end)

    owner.loadingComplete = true
    Items:FilterItems(owner)

    -- Update stats text with final count
    owner.statsText:SetText(string.format("|cff00ff00Loaded %d items|r", #owner.allItems))
end

-- =============================================================================
-- FILTER + DISPLAY
-- =============================================================================

function Items:FilterItems(owner)
    owner.filteredItems = {}

    -- Pre-process search text into words for multi-word matching
    local searchWords = {}
    local searchText = owner.searchText or ""
    if searchText ~= "" then
        local searchLower = searchText:lower()
        for word in searchLower:gmatch("%S+") do
            table.insert(searchWords, word)
        end
    end

    for _, item in ipairs(owner.allItems) do
        local include = true

        -- Search filter - supports multi-word search
        -- ALL search words must be present in the item name
        if #searchWords > 0 then
            local itemName = (item.name or ""):lower()
            for _, word in ipairs(searchWords) do
                if not itemName:find(word, 1, true) then
                    include = false
                    break
                end
            end
        end

        -- Slot filter
        if include and owner.filterSlot ~= "all" then
            if item.slot ~= owner.filterSlot then
                include = false
            end
        end

        -- Armor type filter
        if include and owner.filterArmorType ~= "all" then
            if item.armorType ~= owner.filterArmorType then
                include = false
            end
        end

        if include then
            table.insert(owner.filteredItems, item)
        end
    end

    Items:DisplayItems(owner)
end

function Items:DisplayItems(owner)
    -- Clear existing buttons
    for _, btn in ipairs(owner.itemButtons) do
        btn:Hide()
    end

    local startIdx = (owner.currentPage - 1) * owner.itemsPerPage + 1
    local endIdx = math.min(startIdx + owner.itemsPerPage - 1, #owner.filteredItems)

    local yOffset = 0
    local btnHeight = 26

    -- Track items needing name resolution on this page
    local needsNameUpdate = {}

    for i = startIdx, endIdx do
        local item = owner.filteredItems[i]
        local btnIdx = i - startIdx + 1

        local btn = owner.itemButtons[btnIdx]
        if not btn then
            btn = CreateFrame("Button", nil, owner.scrollChild)
            btn:SetSize(400, btnHeight)
            btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")

            local bg = btn:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            bg:SetColorTexture(0.1, 0.1, 0.1, 0.3)
            btn.bg = bg

            local icon = btn:CreateTexture(nil, "ARTWORK")
            icon:SetSize(22, 22)
            icon:SetPoint("LEFT", 2, 0)
            btn.icon = icon

            local name = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            name:SetPoint("LEFT", icon, "RIGHT", 5, 0)
            name:SetWidth(250)
            name:SetJustifyH("LEFT")
            btn.itemName = name

            local slot = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            slot:SetPoint("LEFT", name, "RIGHT", 5, 0)
            slot:SetWidth(70)
            btn.slotText = slot

            local addBtn = CreateFrame("Button", nil, btn, "UIPanelButtonTemplate")
            addBtn:SetSize(40, 20)
            addBtn:SetPoint("RIGHT", -5, 0)
            addBtn:SetText("Add")
            btn.addBtn = addBtn

            owner.itemButtons[btnIdx] = btn
        end

        btn:SetPoint("TOPLEFT", 0, -yOffset)
        btn.icon:SetTexture(item.icon)

        -- Try to get name if we don't have it
        local displayName = item.name
        if not displayName or displayName:match("^Item #") then
            -- Try to get name now
            local itemName = GetItemInfo(item.itemID)
            if itemName then
                item.name = itemName
                displayName = itemName
            else
                displayName = "|cff888888Loading...|r"
                needsNameUpdate[item.itemID] = { item = item, btn = btn }
            end
        end

        -- Color by quality
        local r, g, b = GetItemQualityColor(item.quality or 1)
        btn.itemName:SetText(displayName)
        if displayName and not displayName:match("Loading") then
            btn.itemName:SetTextColor(r, g, b)
        end

        btn.slotText:SetText("|cff888888" .. (owner.SLOT_NAMES[item.slot] or "?") .. "|r")

        -- Highlight if collected
        if item.isCollected then
            btn.bg:SetColorTexture(0.1, 0.3, 0.1, 0.3)
        else
            btn.bg:SetColorTexture(0.1, 0.1, 0.1, 0.3)
        end

        -- Closure-capture owner so OnClick dispatches to owner:AddToSet
        -- (which now lives in Export.lua, accessed via SB:AddToSet delegator)
        btn.addBtn:SetScript("OnClick", function()
            owner:AddToSet(item)
        end)

        btn:SetScript("OnEnter", function()
            GameTooltip:SetOwner(btn, "ANCHOR_LEFT")
            if item.itemID and item.itemID > 0 then
                ETB:SafeSetTooltipItem(GameTooltip, item.itemID)
            else
                GameTooltip:SetText(item.name or "Unknown")
            end
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        btn:Show()
        yOffset = yOffset + btnHeight
    end

    owner.scrollChild:SetHeight(math.max(yOffset, 1))

    -- Update stats and pagination
    local totalPages = math.ceil(#owner.filteredItems / owner.itemsPerPage)
    owner.statsText:SetText(string.format("|cff888888Showing %d-%d of %d items|r",
        startIdx, endIdx, #owner.filteredItems))
    owner.pageText:SetText(string.format("Page %d / %d", owner.currentPage, math.max(totalPages, 1)))

    -- Schedule async name resolution for items on this page
    if next(needsNameUpdate) then
        Items:ScheduleNameUpdate(owner, needsNameUpdate)
    end
end

function Items:ScheduleNameUpdate(owner, items)
    local retries = 0
    local maxRetries = 10

    local function TryUpdate()
        local stillPending = false
        for itemID, data in pairs(items) do
            local itemName, _, quality = GetItemInfo(itemID)
            if itemName then
                data.item.name = itemName
                if type(quality) == "number" then data.item.quality = quality end
                local r, g, b = GetItemQualityColor(data.item.quality or 1)
                if data.btn and data.btn.itemName then
                    data.btn.itemName:SetText(itemName)
                    data.btn.itemName:SetTextColor(r, g, b)
                end
                items[itemID] = nil
            else
                stillPending = true
            end
        end

        retries = retries + 1
        if stillPending and retries < maxRetries then
            C_Timer.After(0.5, TryUpdate)
        end
    end

    C_Timer.After(0.3, TryUpdate)
end

-- v15.8.0: implement the slot-highlight that was previously a no-op stub.
-- Called by SetBuilder:UpdateSlotHighlight(). Highlights `owner.selectedSlot`
-- (the slot index 1..9 the user clicked) by tinting its frame background.
-- Other slots get the default tint.
--
-- This function tolerates `selectedSlot` being nil (no active selection) —
-- in that case all slots return to their default appearance.

local SLOT_HIGHLIGHT_R, SLOT_HIGHLIGHT_G, SLOT_HIGHLIGHT_B, SLOT_HIGHLIGHT_A = 0.95, 0.85, 0.20, 0.50
local SLOT_DEFAULT_R,   SLOT_DEFAULT_G,   SLOT_DEFAULT_B,   SLOT_DEFAULT_A   = 0.15, 0.15, 0.15, 0.80

function Items:UpdateSlotHighlight(owner)
    if not owner.slotFrames then return end
    local active = owner.selectedSlot
    for slot = 1, 9 do
        local frame = owner.slotFrames[slot]
        if frame then
            -- The "background" texture may be set as frame.bg (the explicit
            -- pattern) or as the first child texture (a few older sites).
            -- Try both, prefer the explicit one.
            local bg = frame.bg
            if not bg then
                local child = frame:GetChildren()
                if child and child.SetColorTexture then bg = child end
            end
            if bg and bg.SetColorTexture then
                if active == slot then
                    bg:SetColorTexture(SLOT_HIGHLIGHT_R, SLOT_HIGHLIGHT_G,
                                       SLOT_HIGHLIGHT_B, SLOT_HIGHLIGHT_A)
                else
                    bg:SetColorTexture(SLOT_DEFAULT_R, SLOT_DEFAULT_G,
                                       SLOT_DEFAULT_B, SLOT_DEFAULT_A)
                end
            end
        end
    end
end
