--[[
============================================================================
UI/TransmogUI/Apply.lua  (added in v14.11.0 — Audit Batch 3 Part 3)
============================================================================

Pending-transmog state machine and apply logic for the TransmogUI orchestrator.
Owner-pattern: each method takes `owner` (the TUI orchestrator) as first
argument. The orchestrator keeps thin delegator methods so existing call
sites keep working.

Methods owned here (15):
  BuildTransmogSlotIDs     — populate owner.transmogSlotIDs from SLOTS
  GetTransmogSlotID        — invSlot -> transmog slot ID via slot data
  CanApplyTransmog         — check if player is at a Transmogrifier NPC
  CallSetPending           — three-way detection wrapper around C_Transmog.SetPending
                             (TransmogUtil objects / plain tables / 3-arg form)
  FindHiddenSourceIDs      — discover hidden-appearance sourceIDs per slot
                             via category iteration
  CallSetPendingHidden     — apply a hidden-slot toggle via SetPending
  GetAllCollectedSources   — iterate appearance sources to find collected ones
                             matching an itemID
  TryApplyWithCollectedSources — fallback chain for applying a transmog when
                             the direct sourceID isn't collected
  GetCollectedSourceForItem — find a collected source for a given itemID
  GetSourceIDForPending    — three-priority resolution: cached map -> all
                             sources -> uncollected fallback
  UpdateCost               — render the cost summary on the bottom bar
  ApplyTransmogs           — apply ALL pending transmogs (the main entry point)
  ApplyTransmogsArmor      — apply only the armor slots
  ApplyTransmogsWeapons    — apply only the weapon slots
  ClearPendingAfterApply   — wipe owner.pendingTransmogs and reset slot borders

Methods kept on TUI itself:
  Reset                    — already a thin delegator to Frame:Reset, lives
                             at file scope where it's near other Frame
                             delegators. Conceptually a Frame-lifecycle
                             concern, not Apply.

Closure / shadowing notes (extending v12.63 + v14.9 + v14.10 lessons):
  - All 15 methods are pure synchronous logic. NO C_Timer.After callbacks,
    NO frame-script closures, NO `function(self,...)` patterns. Apply
    operations are immediate: build the list, push pending state to
    Blizzard, fire ApplyAllPending. Verified by closure-shadowing audit
    in test_v14_11_extraction.lua.
  - `owner.ARMOR_SLOTS`, `owner.WEAPON_SLOTS`, `owner.INVSLOT_TO_CATEGORIES`,
    `owner.SLOT_NAME_TO_TOKEN`, `owner.INV_TO_SLOT_NAME`, `owner.SLOT_NAME_TO_INV`,
    `owner.SLOTS` — all sourced from TUI exposures at TransmogUI.lua:506-510
    (added during v14.11.0 pre-extraction).

Apply-local file-level state:
  SLOT_TO_CATEGORY         — invSlot -> categoryID mapping for hidden-appearance
                             discovery. Used only by FindHiddenSourceIDs;
                             moved here from TransmogUI.lua:1130 since it
                             doesn't need to be shared.

  (v15.4.0 cleanup: TRANSMOG_TYPE_APPEARANCE = 0 and SETPENDING_METHOD = nil
   were dead-code declarations carried over from the v14.11 extraction. Both
   were declared but never read anywhere. Removed.)

External dependencies pulled from owner:
  owner.transmogSlotIDs    — built lazily by BuildTransmogSlotIDs
  owner.pendingTransmogs   — primary state map, written by Slots/Sets/Capture
  owner.itemIDToSourceID   — cached itemID -> sourceID lookups
  owner._hiddenSourceIDs   — populated by FindHiddenSourceIDs (cache)
  owner.frame              — for bottom bar (UpdateCost) + slot buttons
                             (ClearPendingAfterApply)
  owner:CanApplyTransmog, owner:CallSetPending(Hidden), etc. — internal
                             cross-method calls; the delegator dispatch
                             handles these transparently.
============================================================================
]]--
local ADDON_NAME, NS = ...
local ETB = NS.ETB
NS.ETB.TransmogUI = NS.ETB.TransmogUI or {}
NS.ETB.TransmogUI.Apply = NS.ETB.TransmogUI.Apply or {}
local Apply = NS.ETB.TransmogUI.Apply

-- =============================================================================
-- METHODS (extracted from UI/TransmogUI.lua in v14.11.0)
-- =============================================================================

function Apply:BuildTransmogSlotIDs(owner)
    if not GetInventorySlotInfo then return end
    owner.transmogSlotIDs = {}
    for _, slot in ipairs(owner.SLOTS) do
        local token = owner.SLOT_NAME_TO_TOKEN[slot.name]
        if token then
            local id = GetInventorySlotInfo(token)
            if id then owner.transmogSlotIDs[slot.invSlot] = id end
        end
    end
end

function Apply:GetTransmogSlotID(owner, invSlot)
    -- Use runtime-built slot IDs if available
    if owner.transmogSlotIDs and owner.transmogSlotIDs[invSlot] then
        return owner.transmogSlotIDs[invSlot]
    end
    -- Fallback: try to get via token
    local name = owner.INV_TO_SLOT_NAME[invSlot]
    if not name then return invSlot end
    local token = owner.SLOT_NAME_TO_TOKEN[name]
    if not token or not GetInventorySlotInfo then return invSlot end
    local id = GetInventorySlotInfo(token)
    return id or invSlot
end

function Apply:CanApplyTransmog(owner)
    -- Check if we're at a transmog NPC via API
    if C_Transmog and C_Transmog.IsAtTransmogNPC and C_Transmog.IsAtTransmogNPC() then
        return true
    end
    -- Our UI open and we switched from Blizzard at NPC = still at NPC, Apply should work
    if owner.frame and owner.frame:IsShown() and owner.blizzardTransmogFrame then
        return true
    end
    -- Fallback: check if Blizzard transmog frame is visible
    return (TransmogrifyFrame and TransmogrifyFrame:IsShown()) or (WardrobeFrame and WardrobeFrame:IsShown())
end

-- Call C_Transmog.SetPending with correct signature for MoP Classic.
-- MoP Classic (50400+) requires TransmogUtil.CreateTransmogLocation and CreateTransmogPendingInfo objects.
-- Falls back to plain tables for older clients.

function Apply:CallSetPending(owner, slotID, sourceID)
    if not C_Transmog or not C_Transmog.SetPending then return false, "no API" end
    
    -- Method 1: Try TransmogUtil objects first (seems to be required for MoP Classic)
    if TransmogUtil and TransmogUtil.CreateTransmogLocation and TransmogUtil.CreateTransmogPendingInfo then
        local lok, transmogLocation = pcall(TransmogUtil.CreateTransmogLocation, slotID, 0, 0)
        if lok and transmogLocation then
            local pendingType = (Enum and Enum.TransmogPendingType and Enum.TransmogPendingType.Apply) or 0
            local pok, pendingInfo = pcall(TransmogUtil.CreateTransmogPendingInfo, pendingType)
            if pok and pendingInfo then
                if pendingInfo.Init then
                    pcall(pendingInfo.Init, pendingInfo, pendingType, sourceID)
                end
                pendingInfo.sourceID = sourceID
                pendingInfo.transmogID = sourceID
                
                local ok, err = pcall(C_Transmog.SetPending, transmogLocation, pendingInfo)
                if ok then
                    -- VERIFY it actually worked
                    if C_Transmog.GetPending then
                        local pendingOk, pendingResult = pcall(C_Transmog.GetPending, transmogLocation)
                        if pendingOk and pendingResult then
                            local pendingID = type(pendingResult) == "table" and (pendingResult.transmogID or pendingResult.sourceID) or pendingResult
                            if pendingID and pendingID > 0 then
                                return true, "VERIFIED"
                            else
                                return false, "SET_BUT_NOT_PENDING"
                            end
                        else
                            return false, "GETPENDING_FAILED"
                        end
                    end
                    return true, "NO_VERIFY_API"
                end
            end
        end
    end
    
    -- Method 2: Simple 2-arg as fallback
    local ok2, err2 = pcall(C_Transmog.SetPending, slotID, sourceID)
    if ok2 then
        return true, "2arg"
    end
    
    return false, "ALL_FAILED"
end

-- Call C_Transmog.SetPending with ToggleOff type to hide a slot.
-- MoP Classic uses TransmogPendingType.ToggleOff (1) to hide slots.
--
-- Note: owner._hiddenSourceIDs (the cache) is initialized to nil at TUI
-- table-construction time in TransmogUI.lua. This file populates it
-- on demand via FindHiddenSourceIDs.

-- Slot → categoryID mapping for finding hidden appearances
local SLOT_TO_CATEGORY = {
    [1] = 1,   -- Head
    [3] = 2,   -- Shoulder
    [5] = 4,   -- Chest
    [6] = 9,   -- Waist
    [7] = 10,  -- Legs
    [8] = 11,  -- Feet
    [9] = 7,   -- Wrist
    [10] = 8,  -- Hands
    [15] = 3,  -- Back
}

function Apply:FindHiddenSourceIDs(owner)
    if owner._hiddenSourceIDs then return owner._hiddenSourceIDs end
    owner._hiddenSourceIDs = {}
    
    if not C_TransmogCollection then return owner._hiddenSourceIDs end
    
    for invSlot, catID in pairs(SLOT_TO_CATEGORY) do
        -- Method 1: Search category appearances for isHideVisual
        if C_TransmogCollection.GetCategoryAppearances then
            local ok, appearances = pcall(C_TransmogCollection.GetCategoryAppearances, catID)
            if ok and appearances then
                for _, app in ipairs(appearances) do
                    if app.isHideVisual then
                        -- Found the hidden appearance — get its sourceID
                        local srcOK, sources = pcall(C_TransmogCollection.GetAppearanceSources, app.visualID)
                        if srcOK and sources and #sources > 0 then
                            local src = sources[1]
                            local sid = type(src) == "table" and src.sourceID or src
                            if sid and sid > 0 then
                                owner._hiddenSourceIDs[invSlot] = sid
                                ETB:Debug("TransmogUI", string.format("Hidden sourceID for slot %d: %d (visualID=%d)", invSlot, sid, app.visualID))
                            end
                        end
                        break
                    end
                end
            end
        end
    end
    
    local count = 0
    for _ in pairs(owner._hiddenSourceIDs) do count = count + 1 end
    ETB:Debug("TransmogUI", string.format("Found %d hidden-appearance sourceIDs", count))
    
    return owner._hiddenSourceIDs
end

function Apply:CallSetPendingHidden(owner, slotID)
    if not C_Transmog or not C_Transmog.SetPending then return false, "no API" end
    
    -- Convert transmog slotID to invSlot for our lookup
    local invSlot = slotID  -- In MoP Classic these are typically the same
    
    -- Method 1: Apply actual hidden-appearance sourceID as a normal transmog
    -- This is the most reliable method — it applies a real "invisible" appearance
    local hiddenSources = owner:FindHiddenSourceIDs()
    local hiddenSrcID = hiddenSources[invSlot]
    
    if hiddenSrcID then
        local ok, result = owner:CallSetPending(slotID, hiddenSrcID)
        if ok and result == "VERIFIED" then
            return true, "HiddenSource"
        end
    end
    
    -- Method 2: TransmogUtil ToggleOff (Retail-style)
    if TransmogUtil and TransmogUtil.CreateTransmogLocation and TransmogUtil.CreateTransmogPendingInfo then
        local lok, transmogLocation = pcall(TransmogUtil.CreateTransmogLocation, slotID, 0, 0)
        if lok and transmogLocation then
            local toggleOffType = (Enum and Enum.TransmogPendingType and Enum.TransmogPendingType.ToggleOff) or 1
            local pok, pendingInfo = pcall(TransmogUtil.CreateTransmogPendingInfo, toggleOffType)
            if pok and pendingInfo then
                if pendingInfo.Init then
                    pcall(pendingInfo.Init, pendingInfo, toggleOffType, 0)
                end
                local ok, err = pcall(C_Transmog.SetPending, transmogLocation, pendingInfo)
                if ok then
                    -- VERIFY it actually set a pending
                    local vOk, vResult = pcall(C_Transmog.GetPending, transmogLocation)
                    if vOk and vResult then
                        return true, "ToggleOff"
                    end
                end
            end
        end
    end
    
    -- Method 3: ClearPending — at minimum removes any previous pending for this slot
    -- This won't "hide" but at least won't apply a wrong appearance
    if C_Transmog.ClearPending then
        pcall(C_Transmog.ClearPending, slotID)
    end
    
    return false, "HIDDEN_NOT_SUPPORTED"
end

-- Find ALL collected sourceIDs for a given itemID's appearance.
-- Returns a list of {sourceID, isExactItem} sorted with exact item matches first.
function Apply:GetAllCollectedSources(owner, itemID)
    local results = {}
    if not itemID or not C_TransmogCollection then return results end

    if C_TransmogCollection.GetItemInfo then
        local ok, appearanceID = pcall(C_TransmogCollection.GetItemInfo, itemID)
        if ok and appearanceID then
            local sok, sources = pcall(C_TransmogCollection.GetAppearanceSources, appearanceID)
            if sok and sources then
                -- First pass: exact item matches
                for _, src in ipairs(sources) do
                    if type(src) == "table" and src.isCollected and src.sourceID then
                        local srcItemID = src.itemID
                        if not srcItemID and C_TransmogCollection.GetSourceInfo then
                            local si = C_TransmogCollection.GetSourceInfo(src.sourceID)
                            srcItemID = si and si.itemID
                        end
                        if srcItemID == itemID then
                            table.insert(results, {sourceID = src.sourceID, isExact = true})
                        end
                    end
                end
                -- Second pass: alternate sources (same appearance, different item)
                for _, src in ipairs(sources) do
                    if type(src) == "table" and src.isCollected and src.sourceID then
                        local srcItemID = src.itemID
                        if not srcItemID and C_TransmogCollection.GetSourceInfo then
                            local si = C_TransmogCollection.GetSourceInfo(src.sourceID)
                            srcItemID = si and si.itemID
                        end
                        if srcItemID ~= itemID then
                            table.insert(results, {sourceID = src.sourceID, isExact = false})
                        end
                    end
                end
            end
        end
    end

    return results
end

-- Try to apply a transmog for a slot by iterating through all collected sources.
-- Returns: ok, resultString, sourceID
-- This handles the account-wide vs character-usable distinction:
-- isCollected can be true for sources collected on other characters that
-- this character can't actually use (e.g. wrong armor type).
function Apply:TryApplyWithCollectedSources(owner, slotID, itemID)
    local collectedSources = owner:GetAllCollectedSources(itemID)
    
    if #collectedSources == 0 then
        return false, "NOT_COLLECTED", nil
    end
    
    -- Try each collected source - the first one that verifies wins
    for i, entry in ipairs(collectedSources) do
        local ok, result = owner:CallSetPending(slotID, entry.sourceID)
        if result == "VERIFIED" then
            return true, "VERIFIED", entry.sourceID
        end
        -- If this source failed verification, try the next one
    end
    
    -- All collected sources failed verification - likely account-wide collection
    -- that this character can't use (wrong armor type, class restriction, etc.)
    return false, "COLLECTED_BUT_UNUSABLE", collectedSources[1].sourceID
end

-- Backward-compatible wrapper: returns sourceID, isCollected
-- Used by code paths that don't need the full TryApply flow
function Apply:GetCollectedSourceForItem(owner, itemID)
    local sources = owner:GetAllCollectedSources(itemID)
    if #sources > 0 then
        return sources[1].sourceID, true
    end
    return nil, false
end

-- Resolve sourceID from pending, ALWAYS preferring a collected source.
-- Returns sourceID, isCollected
function Apply:GetSourceIDForPending(owner, invSlot, pending)
    if not pending then return nil, false end
    local itemID = pending.itemID
    
    -- PRIORITY 1: Find a collected source for this item's appearance
    if itemID and type(itemID) == "number" then
        local collectedSrc, isCollected = owner:GetCollectedSourceForItem(itemID)
        if collectedSrc then
            owner.itemIDToSourceID[itemID] = collectedSrc
            return collectedSrc, true
        end
    end
    
    -- PRIORITY 2: If no collected source found via appearance lookup, try category scan
    -- This handles cases where GetItemInfo might not work in MoP Classic
    if itemID and type(itemID) == "number" then
        local categories = owner.INVSLOT_TO_CATEGORIES and owner.INVSLOT_TO_CATEGORIES[invSlot]
        if categories and C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances and C_TransmogCollection.GetAppearanceSources then
            for _, catID in ipairs(categories) do
                local ok, appearances = pcall(C_TransmogCollection.GetCategoryAppearances, catID)
                if ok and appearances then
                    for _, app in ipairs(appearances) do
                        if app.isCollected and app.visualID then
                            local ok2, sources = pcall(C_TransmogCollection.GetAppearanceSources, app.visualID)
                            if ok2 and sources then
                                for _, src in ipairs(sources) do
                                    local sid = type(src) == "number" and src or (src and src.sourceID)
                                    local srcItemID = type(src) == "table" and src.itemID or nil
                                    if not srcItemID and sid and C_TransmogCollection.GetSourceInfo then
                                        local si = C_TransmogCollection.GetSourceInfo(sid)
                                        srcItemID = si and si.itemID
                                    end
                                    if srcItemID == itemID and sid then
                                        local srcCollected = type(src) == "table" and src.isCollected
                                        if srcCollected then
                                            owner.itemIDToSourceID[itemID] = sid
                                            return sid, true
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
    end
    
    -- PRIORITY 3: Return uncollected sourceID as last resort
    if pending.sourceID and type(pending.sourceID) == "number" then 
        return pending.sourceID, false 
    end
    if itemID and type(itemID) == "number" and owner.itemIDToSourceID[itemID] then 
        return owner.itemIDToSourceID[itemID], false 
    end

    return nil, false
end


function Apply:UpdateCost(owner)
    if not owner.frame or not owner.frame.bottomBar then return end
    local count = 0
    for _ in pairs(owner.pendingTransmogs) do count = count + 1 end
    
    -- Use Blizzard API for accurate cost if available
    local totalCost = 0
    if C_Transmog and C_Transmog.GetCost then
        local ok, cost = pcall(C_Transmog.GetCost)
        if ok and cost then
            totalCost = math.floor(cost / 10000)  -- Convert copper to gold
        else
            totalCost = count * 10  -- Fallback: 10g per slot
        end
    else
        totalCost = count * 10
    end
    
    owner.frame.bottomBar.costValue:SetText(string.format("%d |TInterface\\MoneyFrame\\UI-GoldIcon:14:14:0:0|t", totalCost))
    owner.frame.bottomBar.pendingLabel:SetText(string.format("|cff888888%d change%s pending|r", count, count == 1 and "" or "s"))
end

-- (TUI:Reset omitted from extraction — it's a thin delegator to Frame:Reset
--  and stays in TransmogUI.lua proper, since Reset is a Frame-lifecycle
--  concern, not an Apply concern.)

-- =============================================================================
-- APPLY TRANSMOGS
-- =============================================================================

function Apply:ApplyTransmogs(owner)
    local count = 0; for _ in pairs(owner.pendingTransmogs) do count = count + 1 end
    if count == 0 then print("|cffff6600[ETB]|r No transmogs pending!"); return end
    
    local atNPC = owner:CanApplyTransmog()
    if not atNPC then print("|cffff6600[ETB]|r Must be at Transmogrifier NPC!"); return end
    if not C_Transmog or not C_Transmog.SetPending or not C_Transmog.ApplyAllPending then 
        print("|cffff6600[ETB]|r Transmog API not available."); return 
    end
    
    local verified = 0
    local notCollected = 0
    local unusable = 0
    local failed = 0
    local verifiedSlots = {}
    local notCollectedSlots = {}
    local unusableSlots = {}
    local failedSlots = {}
    
    for invSlot, pending in pairs(owner.pendingTransmogs) do
        local slotID = owner:GetTransmogSlotID(invSlot)
        local slotName = owner.INV_TO_SLOT_NAME[invSlot] or ("Slot " .. invSlot)
        local itemID = pending.itemID
        local itemName = pending.name or ("item " .. tostring(itemID))
        
        if pending.isHiddenSlot then
            -- v15.7.3: MoP 5.4.8 cannot hide Tabard (19) or Shirt (4) — the
            -- API has no hidden-appearance entries for those categories.
            -- If a stale outfit or pre-v15.7.3 SavedVariable queued one,
            -- skip it cleanly with a message instead of running through the
            -- doomed three-method fallback in CallSetPendingHidden.
            if invSlot == 19 or invSlot == 4 then
                unusable = unusable + 1
                table.insert(unusableSlots, slotName .. " (hide not supported)")
                print(string.format("|cffff9966[ETB]|r %s: HIDE NOT SUPPORTED in MoP 5.4.8 — unequip to remove", slotName))
            else
                -- Hidden slot: apply hidden-appearance sourceID
                local ok, result = owner:CallSetPendingHidden(slotID)
                if ok then
                    verified = verified + 1
                    table.insert(verifiedSlots, slotName .. " (hidden:" .. result .. ")")
                    print(string.format("|cff00ff00[ETB]|r %s: HIDDEN via %s", slotName, result))
                else
                    failed = failed + 1
                    table.insert(failedSlots, slotName)
                    print(string.format("|cffff6600[ETB]|r %s: HIDE FAILED - %s", slotName, result or "unknown"))
                end
            end
        elseif not itemID then
            -- No itemID at all - use legacy sourceID path
            local sourceID = pending.sourceID
            if sourceID then
                local ok, result = owner:CallSetPending(slotID, sourceID)
                if result == "VERIFIED" then
                    verified = verified + 1
                    table.insert(verifiedSlots, slotName)
                    print(string.format("|cff00ff00[ETB]|r %s: VERIFIED (src=%d)", slotName, sourceID))
                else
                    failed = failed + 1
                    table.insert(failedSlots, slotName)
                    print(string.format("|cffff6600[ETB]|r %s: FAILED - %s", slotName, result))
                end
            else
                failed = failed + 1
                table.insert(failedSlots, slotName .. "(no src)")
            end
        else
            -- Use the full collected-source-with-verification flow
            local ok, result, sourceID = owner:TryApplyWithCollectedSources(slotID, itemID)
            if result == "VERIFIED" then
                verified = verified + 1
                table.insert(verifiedSlots, slotName)
                print(string.format("|cff00ff00[ETB]|r %s: VERIFIED (src=%d)", slotName, sourceID))
            elseif result == "NOT_COLLECTED" then
                notCollected = notCollected + 1
                table.insert(notCollectedSlots, slotName)
                print(string.format("|cffff6600[ETB]|r %s: NOT COLLECTED - %s", slotName, itemName))
            elseif result == "COLLECTED_BUT_UNUSABLE" then
                unusable = unusable + 1
                table.insert(unusableSlots, slotName)
                print(string.format("|cffff9900[ETB]|r %s: COLLECTED but unusable on this character - %s", slotName, itemName))
            else
                failed = failed + 1
                table.insert(failedSlots, slotName)
                print(string.format("|cffff6600[ETB]|r %s: FAILED - %s", slotName, result))
            end
        end
    end
    
    -- Summary
    local summaryParts = {}
    if verified > 0 then table.insert(summaryParts, string.format("|cff00ff00%d applied|r", verified)) end
    if unusable > 0 then table.insert(summaryParts, string.format("|cffff9900%d unusable|r", unusable)) end
    if notCollected > 0 then table.insert(summaryParts, string.format("|cffff6600%d not collected|r", notCollected)) end
    if failed > 0 then table.insert(summaryParts, string.format("|cffff0000%d failed|r", failed)) end
    print(string.format("|cff00ffff[ETB Summary]|r %s (%d/%d)", table.concat(summaryParts, ", "), verified, count))
    
    -- Apply all pending
    if verified > 0 then
        local ok, err = pcall(C_Transmog.ApplyAllPending)
        if ok then
            print(string.format("|cff00ff00[ETB]|r Applied %d transmog(s): %s", 
                verified, table.concat(verifiedSlots, ", ")))
        else
            print(string.format("|cffff6600[ETB]|r ApplyAllPending failed: %s", tostring(err)))
        end
        owner:ClearPendingAfterApply()
    else
        if unusable > 0 then
            print("|cffff9900[ETB]|r Appearances are collected account-wide but not usable on this character (wrong armor type or class restriction).")
        elseif notCollected > 0 then
            print(string.format("|cffff6600[ETB]|r None of these appearances are in your collection yet."))
        else
            print("|cffff6600[ETB]|r No transmogs could be applied.")
        end
    end
end

function Apply:ApplyTransmogsArmor(owner)
    local count = 0; for invSlot in pairs(owner.pendingTransmogs) do if owner.ARMOR_SLOTS[invSlot] then count = count + 1 end end
    if count == 0 then print("|cffff6600[ETB]|r No armor pending!"); return end
    if not owner:CanApplyTransmog() then print("|cffff6600[ETB]|r Must be at NPC!"); return end
    
    local applied = 0
    local skipped = 0
    for invSlot, pending in pairs(owner.pendingTransmogs) do
        if owner.ARMOR_SLOTS[invSlot] then
            local itemID = pending.itemID
            if itemID then
                local ok, result, sourceID = owner:TryApplyWithCollectedSources(owner:GetTransmogSlotID(invSlot), itemID)
                if result == "VERIFIED" then applied = applied + 1 else skipped = skipped + 1 end
            else
                local sourceID = pending.sourceID
                if sourceID then
                    local ok = owner:CallSetPending(owner:GetTransmogSlotID(invSlot), sourceID)
                    if ok then applied = applied + 1 else skipped = skipped + 1 end
                else skipped = skipped + 1 end
            end
        end
    end
    if applied > 0 then 
        pcall(C_Transmog.ApplyAllPending)
        local msg = string.format("|cff00ff00[ETB]|r Applied %d armor!", applied)
        if skipped > 0 then msg = msg .. string.format(" (%d skipped - not usable)", skipped) end
        print(msg)
        owner:ClearPendingAfterApply() 
    elseif skipped > 0 then
        print(string.format("|cffff6600[ETB]|r No armor applied. %d piece(s) not usable on this character.", skipped))
    end
end

function Apply:ApplyTransmogsWeapons(owner)
    local count = 0; for invSlot in pairs(owner.pendingTransmogs) do if owner.WEAPON_SLOTS[invSlot] then count = count + 1 end end
    if count == 0 then print("|cffff6600[ETB]|r No weapons pending!"); return end
    if not owner:CanApplyTransmog() then print("|cffff6600[ETB]|r Must be at NPC!"); return end
    
    local applied = 0
    local skipped = 0
    for invSlot, pending in pairs(owner.pendingTransmogs) do
        if owner.WEAPON_SLOTS[invSlot] then
            local itemID = pending.itemID
            if itemID then
                local ok, result, sourceID = owner:TryApplyWithCollectedSources(owner:GetTransmogSlotID(invSlot), itemID)
                if result == "VERIFIED" then applied = applied + 1 else skipped = skipped + 1 end
            else
                local sourceID = pending.sourceID
                if sourceID then
                    local ok = owner:CallSetPending(owner:GetTransmogSlotID(invSlot), sourceID)
                    if ok then applied = applied + 1 else skipped = skipped + 1 end
                else skipped = skipped + 1 end
            end
        end
    end
    if applied > 0 then 
        pcall(C_Transmog.ApplyAllPending)
        local msg = string.format("|cff00ff00[ETB]|r Applied %d weapon(s)!", applied)
        if skipped > 0 then msg = msg .. string.format(" (%d skipped - not usable)", skipped) end
        print(msg)
        owner:ClearPendingAfterApply() 
    elseif skipped > 0 then
        print(string.format("|cffff6600[ETB]|r No weapons applied. %d not usable on this character.", skipped))
    end
end

function Apply:ClearPendingAfterApply(owner)
    wipe(owner.pendingTransmogs)
    for _, btn in pairs(owner.frame.slotButtons) do btn.pendingBorder:Hide(); btn.pendingInner:Hide() end
    
    -- DON'T reset model - keep showing what was just applied
    -- The TryOn() items will stay on the model
    
    owner:UpdateCost()
end

