-- Transmoggin - Copyright (c) 2026 Rooster_Roost. All Rights Reserved.
-- Proprietary software. Personal non-commercial use only; redistribution/repackaging/forks/reuse require permission.
-- Full license: Core.lua and LICENSE.txt

local T=Transmoggin

local function pickField(t,...)
    if type(t)~="table" then return nil end
    for i=1,select("#",...) do local k=select(i,...); if t[k]~=nil then return t[k] end end
end

function T:GetAppearanceSourcesSafe(visualID,categoryID,slotDef)
    if not (C_TransmogCollection and C_TransmogCollection.GetAppearanceSources and visualID) then return {} end
    self.state.sourceListCache=self.state.sourceListCache or {}
    local slotKey=(slotDef and slotDef.key) or (self.state and self.state.selectedSlot) or ""
    local cacheKey=tostring(visualID)..":"..tostring(categoryID or 0)..":"..tostring(slotKey)
    local cached=self.state.sourceListCache[cacheKey]
    if cached then return cached end
    local slot=slotDef or self:GetSlotDefForCategory(categoryID) or self:GetSlotDef(self.state.selectedSlot)
    local loc=slot and self:MakeLocation(slot) or nil
    local ok,res=pcall(C_TransmogCollection.GetAppearanceSources,visualID,categoryID,loc)
    if not ok or type(res)~="table" then ok,res=pcall(C_TransmogCollection.GetAppearanceSources,visualID,categoryID) end
    if not ok or type(res)~="table" then ok,res=pcall(C_TransmogCollection.GetAppearanceSources,visualID) end
    if ok and type(res)=="table" then self.state.sourceListCache[cacheKey]=res; return res end
    self.state.sourceListCache[cacheKey]={}
    return self.state.sourceListCache[cacheKey]
end

function T:SourceIsCollected(src)
    if type(src)=="table" and src.isCollected~=nil then return src.isCollected and true or false end
    local sid=type(src)=="table" and (src.itemModifiedAppearanceID or src.sourceID) or src
    if sid and C_TransmogCollection and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
        local ok,v=pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance,sid)
        if ok and v~=nil then return v and true or false end
    end
    if sid and C_TransmogCollection and C_TransmogCollection.GetSourceInfo then
        self.state.sourceInfoCache=self.state.sourceInfoCache or {}
        local info=self.state.sourceInfoCache[sid]
        if not info then
            local ok,res=pcall(C_TransmogCollection.GetSourceInfo,sid)
            if ok and type(res)=="table" then info=res; self.state.sourceInfoCache[sid]=res end
        end
        if type(info)=="table" and info.isCollected~=nil then return info.isCollected and true or false end
    end
    return false
end

-- Collection ownership is appearance-wide. A visual is owned when ANY valid
-- source that grants that visual is collected. This prevents false "Missing"
-- labels when the player owns a different item source for the same appearance.
function T:IsVisualCollected(visualID,categoryID,preferredSourceID)
    if not visualID then return false end
    self.state.visualCollectedCache=self.state.visualCollectedCache or {}
    local ck=tostring(visualID)..":"..tostring(categoryID or 0)
    local cv=self.state.visualCollectedCache[ck]
    if cv~=nil then return cv end
    local sources=self:GetAppearanceSourcesSafe(visualID,categoryID)
    for _,src in ipairs(sources) do if self:SourceIsCollected(src) then self.state.visualCollectedCache[ck]=true; return true end end
    if preferredSourceID and self:SourceIsCollected(preferredSourceID) then self.state.visualCollectedCache[ck]=true; return true end
    if C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances and categoryID then
        local slot=self:GetSlotDefForCategory(categoryID); local loc=slot and self:MakeLocation(slot) or nil
        local ok,apps=pcall(C_TransmogCollection.GetCategoryAppearances,categoryID,loc)
        if not ok or type(apps)~="table" then ok,apps=pcall(C_TransmogCollection.GetCategoryAppearances,categoryID) end
        if ok and type(apps)=="table" then
            for _,a in ipairs(apps) do if a.visualID==visualID then local v=a.isCollected and true or false; self.state.visualCollectedCache[ck]=v; return v end end
        end
    end
    self.state.visualCollectedCache[ck]=false
    return false
end

local function sourceIDOf(src)
    return type(src)=="table" and (src.sourceID or src.itemModifiedAppearanceID) or src
end

function T:GetGeneratedItemIDForModifiedAppearance(modID)
    if self.clientMode and self.clientMode~="mop-classic" then return nil end
    modID=tonumber(modID)
    if not modID then return nil end
    -- v0.10.9 ships a direct reverse index generated from the complete local
    -- ItemModifiedAppearance table. This avoids a linear scan/stutter the first
    -- time an old-client source ID needs to be resolved.
    if self.GeneratedModToItem then
        local itemID=self.GeneratedModToItem[modID]
        if itemID and itemID~=0 then return itemID end
    end
    if not self.GeneratedItemSourceDB then return nil end
    self.state.generatedModToItem=self.state.generatedModToItem or {}
    local cached=self.state.generatedModToItem[modID]
    if cached~=nil then return cached~=false and cached or nil end
    for itemID,row in pairs(self.GeneratedItemSourceDB) do
        if row and tonumber(row.m)==modID then self.state.generatedModToItem[modID]=itemID; return itemID end
    end
    self.state.generatedModToItem[modID]=false
    return nil
end

function T:GetSourceRecord(visualID,categoryID,preferredSourceID,slotDef)
    if not visualID then return nil end
    local slotKey=(slotDef and slotDef.key) or (self.state and self.state.selectedSlot) or ""
    local key=tostring(visualID)..":"..tostring(categoryID or 0)..":"..tostring(preferredSourceID or 0)..":"..tostring(slotKey)
    local cached=self.state.sourceCache[key]; if cached then cached.collected=self:IsVisualCollected(visualID,categoryID,cached.itemModID or cached.sourceID); return cached end

    local sources=self:GetAppearanceSourcesSafe(visualID,categoryID,slotDef)
    local chosen=nil
    -- Prefer the exact requested source only if it is collected/usable, otherwise
    -- prefer any collected usable source so names/tooltips match what the player owns.
    if preferredSourceID then
        for _,src in ipairs(sources) do if sourceIDOf(src)==preferredSourceID and self:SourceIsCollected(src) then chosen=src break end end
    end
    if not chosen then for _,src in ipairs(sources) do if self:SourceIsCollected(src) and (type(src)~="table" or src.isUsable~=false) then chosen=src break end end end
    if not chosen and preferredSourceID then for _,src in ipairs(sources) do if sourceIDOf(src)==preferredSourceID then chosen=src break end end end
    if not chosen then for _,src in ipairs(sources) do if type(src)~="table" or src.isUsable~=false then chosen=src break end end end
    chosen=chosen or sources[1]

    local info={}; local sourceID=sourceIDOf(chosen)
    if type(chosen)=="table" then for k,v in pairs(chosen) do info[k]=v end end
    if sourceID and C_TransmogCollection and C_TransmogCollection.GetSourceInfo then
        self.state.sourceInfoCache=self.state.sourceInfoCache or {}
        local res=self.state.sourceInfoCache[sourceID]
        if not res then
            local ok,r=pcall(C_TransmogCollection.GetSourceInfo,sourceID)
            if ok and type(r)=="table" then res=r; self.state.sourceInfoCache[sourceID]=r end
        end
        if type(res)=="table" then for k,v in pairs(res) do if info[k]==nil then info[k]=v end end end
    end

    sourceID=sourceID or info.sourceID
    local itemModID=pickField(info,"itemModifiedAppearanceID","itemModID","sourceID") or sourceID
    local itemID=pickField(info,"itemID")
    if not itemID and itemModID and C_TransmogCollection and C_TransmogCollection.GetSourceItemID then
        local ok,res=pcall(C_TransmogCollection.GetSourceItemID,itemModID); if ok and res and res~=0 then itemID=res end
    end
    if not itemID and itemModID and self.GetGeneratedItemIDForModifiedAppearance then
        itemID=self:GetGeneratedItemIDForModifiedAppearance(itemModID)
    end

    local itemName,itemLink,icon
    if itemID then
        itemName,itemLink=GetItemInfo(itemID)
        if not itemName and C_Item and C_Item.GetItemNameByID then local ok,res=pcall(C_Item.GetItemNameByID,itemID); if ok then itemName=res end end
        if GetItemInfoInstant then icon=select(5,GetItemInfoInstant(itemID)) end
    end
    local appearanceName=info.name or ("Appearance "..tostring(visualID))
    local rec={visualID=visualID,sourceID=sourceID,itemID=itemID,itemModID=itemModID,categoryID=categoryID,
        name=itemName or appearanceName,itemName=itemName,appearanceName=appearanceName,itemLink=itemLink,
        icon=icon or info.icon or 134400,isHideVisual=info.isHideVisual,allSources=sources,
        sourceType=info.sourceType,useError=info.useError,useErrorType=info.useErrorType}
    rec.collected=self:IsVisualCollected(visualID,categoryID,itemModID)
    -- Acquisition intelligence can involve several boss/source queries. Compute it
    -- only when the player selects/hovers/tracks the piece, never for every grid tile.
    rec.sourceText=nil
    self.state.sourceCache[key]=rec
    return rec
end

-- Resolve a player-facing item name without ever exposing a raw itemID as the
-- label. Retail item records are loaded lazily, so request missing data and let
-- GET_ITEM_INFO_RECEIVED rebuild the currently visible HOW TO OBTAIN panel.
function T:ResolveDisplayItemName(itemID, fallback)
    itemID=tonumber(itemID)
    if not itemID or itemID<=0 then return fallback end
    local name
    if GetItemInfo then name=GetItemInfo(itemID) end
    if (not name or name=="") and C_Item and C_Item.GetItemNameByID then
        local ok,res=pcall(C_Item.GetItemNameByID,itemID)
        if ok and res and res~="" then name=res end
    end
    if (not name or name=="") and self.GeneratedItemNames then name=self.GeneratedItemNames[itemID] end
    if (not name or name=="") and self.GeneratedAuxSourceDB and self.GeneratedAuxSourceDB[itemID] then
        name=self.GeneratedAuxSourceDB[itemID].n
    end
    if name and name~="" then return name end
    if C_Item and C_Item.RequestLoadItemDataByID then pcall(C_Item.RequestLoadItemDataByID,itemID) end
    self.state.sourceResolvePending=true
    return fallback or "Loading item name..."
end

function T:EnsureSourceText(rec)
    if not rec then return "Source unavailable." end
    self.state.acquisitionTextCache=self.state.acquisitionTextCache or {}
    local k=tostring(rec.itemID or 0)..":"..tostring(rec.itemModID or rec.sourceID or 0)..":"..tostring(rec.visualID or 0)
    if self.state.acquisitionTextCache[k] then return self.state.acquisitionTextCache[k] end
    if rec.sourceText and rec.sourceText~="" and not rec.sourceTextPending then
        self.state.acquisitionTextCache[k]=rec.sourceText
        return rec.sourceText
    end
    self.state.sourceResolvePending=false
    local text=self:GetAcquisitionText(rec)
    local pending=self.state.sourceResolvePending and true or false
    self.state.sourceResolvePending=false
    rec.sourceText=text
    rec.sourceTextPending=pending
    if not pending then self.state.acquisitionTextCache[k]=text end
    return text
end

function T:GetAcquisitionText(rec)
    if not rec then return "Source unavailable." end
    -- Resolve every item that can unlock this appearance, not only the source tile
    -- currently selected. This lets an appearance expose boss/craft/quest alternatives.
    if self.FormatDBSource then
        local itemIDs,seenItems={},{}
        local function addItem(id)
            id=tonumber(id)
            if id and id>0 and not seenItems[id] then seenItems[id]=true; itemIDs[#itemIDs+1]=id end
        end
        addItem(rec.itemID)
        if C_TransmogCollection and C_TransmogCollection.GetSourceItemID then
            for _,src in ipairs(rec.allSources or {}) do
                local sid=sourceIDOf(src)
                if sid then
                    local ok,id=pcall(C_TransmogCollection.GetSourceItemID,sid)
                    if ok then addItem(id) end
                    if (not ok or not id or id==0) and self.GetGeneratedItemIDForModifiedAppearance then
                        addItem(self:GetGeneratedItemIDForModifiedAppearance(sid))
                    end
                end
            end
        end
        local blocks={}
        for _,itemID in ipairs(itemIDs) do
            local itemName=self:ResolveDisplayItemName(itemID)
            local text=self:FormatDBSource({itemID=itemID,itemName=itemName,name=itemName})
            if text and text~="" then blocks[#blocks+1]=text end
            if #blocks>=4 then break end
        end
        if #blocks==1 then return blocks[1] end
        if #blocks>1 then
            local out={}
            for i,text in ipairs(blocks) do
                out[#out+1]="OPTION "..tostring(i)
                out[#out+1]=text
                if i<#blocks then out[#out+1]="" end
            end
            return table.concat(out,"\n")
        end
    end
    -- Curated source intelligence fills gaps Blizzard does not expose (vendor names,
    -- faction routing, and currencies). It wins when a verified record exists.
    if self.FormatSourceIntel then
        local enriched=self:FormatSourceIntel(rec)
        if enriched and enriched~="" then return enriched end
    end
    local ids={}
    if rec.itemModID then ids[#ids+1]=rec.itemModID end
    for _,src in ipairs(rec.allSources or {}) do local sid=sourceIDOf(src); if sid then ids[#ids+1]=sid end end

    local lines={}
    local seen={}
    local function add(line)
        if line and line~="" and not seen[line] then seen[line]=true; lines[#lines+1]=line end
    end

    -- Instance/boss sources. Blizzard exposes the exact encounter, instance and
    -- difficulties for these sources, so show them as structured farming intel.
    if self.cap.sourceDrops and C_TransmogCollection and C_TransmogCollection.GetAppearanceSourceDrops then
        local dropRows={}
        local dropSeen={}
        for _,sid in ipairs(ids) do
            local ok,drops=pcall(C_TransmogCollection.GetAppearanceSourceDrops,sid)
            if ok and type(drops)=="table" then
                for _,d in ipairs(drops) do
                    local encounter=d.encounter or ""
                    local instance=d.instance or ""
                    local diffs=(type(d.difficulties)=="table" and #d.difficulties>0) and table.concat(d.difficulties,", ") or ""
                    local tiers=(type(d.tiers)=="table" and #d.tiers>0) and table.concat(d.tiers,", ") or ""
                    local key=instance.."|"..encounter.."|"..diffs
                    if key~="||" and not dropSeen[key] then
                        dropSeen[key]=true
                        dropRows[#dropRows+1]={instance=instance,encounter=encounter,diffs=diffs,tiers=tiers,instanceType=d.instanceType}
                    end
                end
            end
        end
        if #dropRows>0 then
            add("HOW TO OBTAIN")
            for i=1,math.min(#dropRows,5) do
                local d=dropRows[i]
                local kind="BOSS DROP"
                local inst=(d.instance or ""):lower()
                if inst:find("world",1,true) or inst:find("outdoor",1,true) then kind="WORLD BOSS" end
                add(kind..": "..((d.encounter~="" and d.encounter) or "Unknown encounter"))
                if d.instance~="" then add("Location: "..d.instance) end
                if d.diffs~="" then add("Difficulty: "..d.diffs) end
                if d.tiers~="" then add("Expansion/Tier: "..d.tiers) end
                if i<#dropRows and i<5 then add(" ") end
            end
            if #dropRows>5 then add("+ "..(#dropRows-5).." more boss source(s)") end
            if rec.itemName then add("Item: "..rec.itemName) end
            return table.concat(lines,"\n")
        end
    end


    -- Retail has a much richer appearance/source graph than older clients.  Build
    -- a detailed source summary from every known source for this appearance before
    -- falling back to a generic source-type label.  This is deliberately lazy: it
    -- only runs for the selected/hovered appearance and is cached by EnsureSourceText.
    if self.clientMode=="retail" and C_TransmogCollection then
        local retailSources={}
        local retailSeen={}
        local function addRetailSource(src)
            local sid=sourceIDOf(src)
            if not sid or retailSeen[sid] then return end
            retailSeen[sid]=true
            local info=type(src)=="table" and src or nil
            if C_TransmogCollection.GetSourceInfo then
                local ok,full=pcall(C_TransmogCollection.GetSourceInfo,sid)
                if ok and type(full)=="table" then
                    if not info then info={} end
                    for k,v in pairs(full) do if info[k]==nil then info[k]=v end end
                end
            end
            info=info or {sourceID=sid}
            info.sourceID=info.sourceID or sid
            retailSources[#retailSources+1]=info
        end
        for _,src in ipairs(rec.allSources or {}) do addRetailSource(src) end
        if C_TransmogCollection.GetAllAppearanceSources and rec.visualID then
            local ok,all=pcall(C_TransmogCollection.GetAllAppearanceSources,rec.visualID)
            if ok and type(all)=="table" then for _,src in ipairs(all) do addRetailSource(src) end end
        end
        if #retailSources>0 then
            local sourceTypeNames={
                [1]="Dungeon / Raid",[2]="Quest",[3]="Vendor",[4]="World Drop",
                [5]="Hidden",[6]="Unavailable",[7]="Achievement",[8]="Profession / Crafted",
                [9]="Invalid",[10]="Trading Post",
            }
            local byType, typeOrder={},{}
            local itemNames,itemSeen={},{}
            local setNames,setSeen={},{}
            local variantSeen,variants={},{}
            local reqSeen,requirements={},{}
            for _,info in ipairs(retailSources) do
                local st=tonumber(info.sourceType) or 0
                if not byType[st] then byType[st]={}; typeOrder[#typeOrder+1]=st end
                byType[st][#byType[st]+1]=info
                local iid=tonumber(info.itemID)
                if iid and iid>0 and not itemSeen[iid] then
                    itemSeen[iid]=true
                    local nm=self:ResolveDisplayItemName(iid)
                    itemNames[#itemNames+1]={id=iid,name=nm or "Loading item name...",sourceType=st,sourceID=info.sourceID}
                end
                local mod=tonumber(info.itemModID)
                if mod and not variantSeen[mod] then
                    variantSeen[mod]=true
                    local labels={[0]="Normal / Base",[1]="Heroic",[3]="Mythic / Elite",[4]="Raid Finder"}
                    variants[#variants+1]=labels[mod] or ("Item modifier "..mod)
                end
                if info.useError and info.useError~="" and not reqSeen[info.useError] then reqSeen[info.useError]=true; requirements[#requirements+1]=info.useError end
                if C_TransmogSets and C_TransmogSets.GetSetsContainingSourceID and info.sourceID then
                    local ok,sets=pcall(C_TransmogSets.GetSetsContainingSourceID,info.sourceID)
                    if ok and type(sets)=="table" then
                        for _,setID in ipairs(sets) do
                            if not setSeen[setID] then
                                setSeen[setID]=true
                                local setInfo
                                if C_TransmogSets.GetSetInfo then local ok2,v=pcall(C_TransmogSets.GetSetInfo,setID); if ok2 then setInfo=v end end
                                if type(setInfo)=="table" and setInfo.name then
                                    local label=setInfo.name
                                    if setInfo.description and setInfo.description~="" then label=label.." ("..setInfo.description..")" end
                                    setNames[#setNames+1]=label
                                end
                            end
                        end
                    end
                end
            end
            table.sort(typeOrder)
            add("HOW TO OBTAIN")
            if rec.itemName then add("Selected item: "..rec.itemName) end
            if #typeOrder>0 then
                local labels={}
                for _,st in ipairs(typeOrder) do labels[#labels+1]=sourceTypeNames[st] or ("Source type "..st) end
                add("Source type(s): "..table.concat(labels,", "))
            end
            if #setNames>0 then
                add("Set: "..table.concat(setNames,", ",1,math.min(#setNames,3)))
                if #setNames>3 then add("+ "..tostring(#setNames-3).." additional set association(s)") end
            end
            if #variants>1 then add("Appearance variants: "..table.concat(variants,", ")) end
            if #itemNames>0 then
                add("")
                add("ITEMS THAT UNLOCK THIS APPEARANCE")
                for i=1,math.min(#itemNames,8) do
                    local it=itemNames[i]
                    local kind=sourceTypeNames[it.sourceType] or "Source"
                    add("  "..it.name.." — "..kind)
                end
                if #itemNames>8 then add("  +"..tostring(#itemNames-8).." additional item source(s)") end
            end
            if #requirements>0 then
                add("")
                add("REQUIREMENTS")
                for i=1,math.min(#requirements,3) do add("  "..requirements[i]) end
            end
            local holidaySeen=false
            if C_TransmogCollection.GetSourceRequiredHoliday then
                for _,info in ipairs(retailSources) do
                    if info.sourceID then
                        local ok,h=pcall(C_TransmogCollection.GetSourceRequiredHoliday,info.sourceID)
                        if ok and h and h~="" then add("Available during: "..h); holidaySeen=true; break end
                    end
                end
            end
            -- If Blizzard exposes structured encounter drops, append them even when
            -- another source type also exists for the same appearance.
            if C_TransmogCollection.GetAppearanceSourceDrops then
                local seenDrop,dropCount={},0
                for _,info in ipairs(retailSources) do
                    if info.sourceID then
                        local ok,drops=pcall(C_TransmogCollection.GetAppearanceSourceDrops,info.sourceID)
                        if ok and type(drops)=="table" then
                            for _,d in ipairs(drops) do
                                local key=tostring(d.instance or "").."|"..tostring(d.encounter or "")
                                if key~="|" and not seenDrop[key] then
                                    if dropCount==0 then add(""); add("ENCOUNTER SOURCES") end
                                    seenDrop[key]=true; dropCount=dropCount+1
                                    local line="  "..(d.encounter or "Encounter")
                                    if d.instance and d.instance~="" then line=line.." — "..d.instance end
                                    if type(d.difficulties)=="table" and #d.difficulties>0 then line=line.." ["..table.concat(d.difficulties,", ").."]" end
                                    add(line)
                                    if dropCount>=6 then break end
                                end
                            end
                        end
                    end
                    if dropCount>=6 then break end
                end
            end
            add("")
            add("Retail source graph: Blizzard Wardrobe APIs + Transmoggin local data")
            return table.concat(lines,"\n")
        end
    end

    local typeNames={
        [1]="Dungeon / raid encounter", [2]="Quest reward", [3]="Vendor",
        [4]="World drop", [5]="Hidden until collected", [6]="Not collectible on this character",
        [7]="Achievement reward", [8]="Profession / crafted", [9]="Not valid for transmog",
        [10]="Trading Post",
    }
    local found={}
    local function addType(v) if v and typeNames[v] then found[v]=true end end
    addType(rec.sourceType)
    for _,src in ipairs(rec.allSources or {}) do
        if type(src)=="table" then addType(src.sourceType) end
        local sid=sourceIDOf(src)
        if sid and C_TransmogCollection and C_TransmogCollection.GetSourceInfo then
            local ok,inf=pcall(C_TransmogCollection.GetSourceInfo,sid)
            if ok and type(inf)=="table" then
                addType(inf.sourceType)
                if not rec.useError and inf.useError then rec.useError=inf.useError end
            end
        end
    end

    add("HOW TO OBTAIN")
    if found[2] then
        add("Source: Quest reward")
        add("Exact quest: not present in the local client DB2 export for this item yet.")
    elseif found[4] then
        add("Source: World drop")
        add("Exact mob / elite: not present in the local client DB2 export for this item yet.")
    elseif found[3] then
        add("Source: Vendor")
        add("Vendor inventory / currency: not present in the local client DB2 export for this item yet.")
    elseif found[8] then
        add("Source: Profession / crafted")
        add("Profession / recipe: not present in the local client DB2 export for this item yet.")
    elseif found[7] then
        add("Source: Achievement reward")
    elseif found[10] then
        add("Source: Trading Post")
    elseif found[1] then
        add("Source: Dungeon / raid encounter")
    else
        local kinds={}; for id in pairs(found) do kinds[#kinds+1]=typeNames[id] end; table.sort(kinds)
        if #kinds>0 then add("Source: "..table.concat(kinds,", ")) end
    end

    if rec.itemName then add("Item: "..rec.itemName) end
    if rec.itemID and self.GeneratedItemSourceDB then
        local meta=self.GeneratedItemSourceDB[rec.itemID]
        if meta and meta.s and self.GeneratedSetSources then
            local names={}
            for i=1,math.min(#meta.s,3) do
                local row=self.GeneratedSetSources[meta.s[i]]
                if row and row[1] then names[#names+1]=row[1] end
            end
            if #names>0 then add("Appearance set: "..table.concat(names,", ")) end
        end
    end
    if rec.useError and rec.useError~="" then add("Requirement: "..rec.useError) end
    if C_TransmogCollection and C_TransmogCollection.GetSourceRequiredHoliday and rec.itemModID then
        local ok,h=pcall(C_TransmogCollection.GetSourceRequiredHoliday,rec.itemModID)
        if ok and h and h~="" then add("Available during: "..h) end
    end
    if #lines>1 then return table.concat(lines,"\n") end
    return rec.collected and "Collected - detailed source data is not exposed by this client." or "Uncollected - detailed source data is not exposed by this client."
end

function T:GetWowheadURL(rec)
    -- Kept only for backward compatibility with older saved UI references.
    -- Transmoggin now displays bundled source intelligence directly in the salon.
    return nil
end

function T:ShowWowheadLink(rec)
    self:Print("Source information is now displayed directly in the item panel.")
end

function T:ShowAppearanceTooltip(owner,rec)
    if not rec then return end
    rec.collected=self:IsPieceCollected(rec)
    GameTooltip:SetOwner(owner,"ANCHOR_RIGHT"); GameTooltip:ClearLines()
    GameTooltip:SetText(rec.itemName or rec.name or "Item",1,0.82,0.30)
    local d=(rec.itemID and self.GetSourceDBRecord) and self:GetSourceDBRecord(rec.itemID,rec.itemName or rec.name) or nil
    local kind=d and (d.kind or d.sourceKind) or nil
    if kind then GameTooltip:AddLine("Source: "..kind,0.90,0.90,0.90,true) end
    if d and d.vendor then
        GameTooltip:AddLine("Vendor: "..d.vendor..(d.zone and (" - "..d.zone) or ""),0.75,0.85,1,true)
    elseif d and (d.kind=="Vendor" or d.sourceKind=="Vendor") and self.GetVendorGuide then
        local guide=self:GetVendorGuide(rec)
        if guide and guide.whereToGo then GameTooltip:AddLine(guide.whereToGo,0.75,0.85,1,true) end
    elseif d and d.encounter then
        GameTooltip:AddLine("Drops from: "..d.encounter..(d.instance and (" - "..d.instance) or ""),0.75,0.85,1,true)
    elseif d and d.profession then
        GameTooltip:AddLine("Crafted by: "..d.profession,0.75,0.85,1,true)
    end
    GameTooltip:AddLine("See HOW TO OBTAIN below for full details.",0.55,0.75,0.95,true)
    GameTooltip:Show()
end
