-- 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

-- Transmoggin Source DB
-- Fast, local, itemID-keyed acquisition data. Static records can be generated
-- offline and shipped with the addon. Learned merchant records are stored in
-- SavedVariables so the addon never needs web access while WoW is running.
T.SourceDB=T.SourceDB or {}

local function trim(s)
    if type(s)~="string" then return s end
    return (s:gsub("^%s+",""):gsub("%s+$",""))
end

function T:GetSourceDBRecord(itemID,itemName)
    local static=itemID and self.SourceDB[itemID] or nil
    if not static and itemName and self.GetSourceNameRecord then static=self:GetSourceNameRecord(itemName) end
    local learned=itemID and self.db and self.db.sourceIntel and self.db.sourceIntel[itemID] or nil
    local allowGenerated=(not self.clientMode) or self.clientMode=="mop-classic"
    local generated=allowGenerated and itemID and self.GeneratedItemSourceDB and self.GeneratedItemSourceDB[itemID] or nil
    local aux=allowGenerated and itemID and self.GeneratedAuxSourceDB and self.GeneratedAuxSourceDB[itemID] or nil
    local meta=allowGenerated and itemID and self.GeneratedClientItemMeta and self.GeneratedClientItemMeta[itemID] or nil
    -- Set membership by itself is useful metadata but is not an acquisition source.
    -- Only promote generated client data when the Encounter Journal ties this exact
    -- itemID to one or more encounters.
    local hasGeneratedEncounter=generated and type(generated.e)=="table" and #generated.e>0
    local hasGeneratedMeta=generated~=nil
    if not static and not learned and not hasGeneratedMeta and not aux and not meta then return nil end
    local out={}
    if type(static)=="table" then for k,v in pairs(static) do out[k]=v end end
    if generated then
        if not out.itemName and self.GeneratedItemNames then out.itemName=self.GeneratedItemNames[itemID] end
        out.appearanceID=out.appearanceID or generated.a
        out.modifiedAppearanceID=out.modifiedAppearanceID or generated.m
        if hasGeneratedEncounter and self.GeneratedEncounterSources then
            out.kind=out.kind or "Boss Drop"
            out.encounters=out.encounters or {}
            for _,sourceIndex in ipairs(generated.e) do
                local row=self.GeneratedEncounterSources[sourceIndex]
                if row then
                    local d={encounter=row[1],instance=row[2]}
                    if row[3] and row[3]~="" then d.difficulty=row[3] end
                    out.encounters[#out.encounters+1]=d
                end
            end
            local first=out.encounters[1]
            if first then
                out.encounter=out.encounter or first.encounter
                out.instance=out.instance or first.instance
                out.difficulty=out.difficulty or first.difficulty
            end
        end
        if generated.s and self.GeneratedSetSources then
            out.sets=out.sets or {}
            for _,setID in ipairs(generated.s) do
                local row=self.GeneratedSetSources[setID]
                if row then out.sets[#out.sets+1]={name=row[1],expansionID=row[2]} end
            end
        end
        out.clientDB=true
    end
    if meta then
        -- Positional compact metadata generated from the complete local DB export:
        -- 1 source mask, 2 faction ID, 3 rep rank, 4 req level, 5 req skill,
        -- 6 req skill rank, 7 req ability, 8 base buy price, 9 currency-cost flag,
        -- 10 starts quest ID, 11 opposite-faction item ID.
        local mask=tonumber(meta[1]) or 0
        out.sourceTypes=out.sourceTypes or {}
        for sourceType=0,10 do
            local bit=2^sourceType
            if math.floor(mask/bit)%2==1 then out.sourceTypes[#out.sourceTypes+1]=sourceType end
        end
        local priority={1,8,2,3,4,7,10,6,5,9,0}
        if not out.kind and self.GeneratedSourceTypeNames then
            for _,sourceType in ipairs(priority) do
                for _,present in ipairs(out.sourceTypes) do
                    if present==sourceType then out.kind=self.GeneratedSourceTypeNames[sourceType]; break end
                end
                if out.kind then break end
            end
        end
        out.requiredFactionID=meta[2] or out.requiredFactionID
        if out.requiredFactionID and out.requiredFactionID~=0 and self.GeneratedFactionNames then out.requiredFactionName=self.GeneratedFactionNames[out.requiredFactionID] end
        out.requiredReputationRank=meta[3] or out.requiredReputationRank
        if out.requiredReputationRank and self.GeneratedRepNames then out.requiredReputationName=self.GeneratedRepNames[out.requiredReputationRank] end
        out.requiredLevel=meta[4] or out.requiredLevel
        out.requiredSkillID=meta[5] or out.requiredSkillID
        out.requiredSkillRank=meta[6] or out.requiredSkillRank
        out.requiredAbility=meta[7] or out.requiredAbility
        out.baseBuyPrice=meta[8] or out.baseBuyPrice
        out.hasCurrencyCost=(meta[9]==1) or out.hasCurrencyCost
        out.startsQuestID=meta[10] or out.startsQuestID
        out.oppositeFactionItemID=meta[11] or out.oppositeFactionItemID
        out.fullClientDB=true
    end
    if aux then
        if not out.itemName then out.itemName=aux.n end
        if aux.k==1 then
            out.kind=out.kind or "Crafted"
            out.profession=out.profession or aux.p
            out.recipe=out.recipe or aux.r
            out.recipeSpellID=out.recipeSpellID or aux.sp
            out.skillRank=out.skillRank or aux.rk
            if aux.g then
                out.reagents=out.reagents or {}
                for _,g in ipairs(aux.g) do
                    local reagentID,count=g[1],g[2]
                    local reagentName=self.GeneratedAuxReagentNames and self.GeneratedAuxReagentNames[reagentID] or nil
                    out.reagents[#out.reagents+1]={itemID=reagentID,count=count,name=reagentName}
                end
            end
            out.clientCraftDB=true
        end
        if aux.q then
            out.questPackages=out.questPackages or {}
            for _,q in ipairs(aux.q) do out.questPackages[#out.questPackages+1]={packageID=q[1],quantity=q[2]} end
            if not out.kind then out.kind="Quest Reward" end
            out.clientQuestPackageDB=true
        end
    end
    if type(learned)=="table" then for k,v in pairs(learned) do if out[k]==nil then out[k]=v end end end
    return out
end

local function moneyText(copper)
    copper=tonumber(copper) or 0
    if copper<=0 then return nil end
    local g=math.floor(copper/10000)
    local s=math.floor((copper%10000)/100)
    local c=copper%100
    local parts={}
    if g>0 then parts[#parts+1]=g.." Gold" end
    if s>0 then parts[#parts+1]=s.." Silver" end
    if c>0 then parts[#parts+1]=c.." Copper" end
    return table.concat(parts," ")
end
T.FormatMoneyPlain=moneyText

local function merchantName()
    local n=UnitName and UnitName("npc")
    return (n and n~="") and n or "Merchant"
end

local function currentZone()
    if GetRealZoneText then
        local z=GetRealZoneText()
        if z and z~="" then return z end
    end
    if GetZoneText then return GetZoneText() end
end

local function factionTag()
    local f=UnitFactionGroup and UnitFactionGroup("player")
    return (f=="Alliance" or f=="Horde") and f or nil
end

local function costParts(index,price,extended)
    local out={}
    if price and price>0 then out[#out+1]={type="money",amount=price,text=moneyText(price)} end
    if extended and GetMerchantItemCostInfo and GetMerchantItemCostItem then
        local ok,n=pcall(GetMerchantItemCostInfo,index)
        if ok and type(n)=="number" then
            for j=1,n do
                local ok2,texture,value,link,name=pcall(GetMerchantItemCostItem,index,j)
                if ok2 then
                    local id=link and tonumber((link:match("item:(%d+)"))) or nil
                    if not name and id then name=GetItemInfo(id) end
                    out[#out+1]={type="currency",itemID=id,amount=value or 0,name=name or "Currency",icon=texture}
                end
            end
        end
    end
    return out
end

function T:LearnCurrentMerchantSources()
    if not (self.db and GetMerchantNumItems and GetMerchantItemInfo) then return end
    local count=GetMerchantNumItems() or 0
    if count<=0 then return end
    self.db.sourceIntel=self.db.sourceIntel or {}
    local vendor=merchantName()
    local zone=currentZone()
    local faction=factionTag()
    for i=1,count do
        local name,texture,price,quantity,numAvailable,isUsable,extendedCost=GetMerchantItemInfo(i)
        local link=GetMerchantItemLink and GetMerchantItemLink(i)
        local itemID=link and tonumber(link:match("item:(%d+)"))
        if itemID then
            local rec=self.db.sourceIntel[itemID] or {}
            rec.itemName=name or rec.itemName
            rec.kind="Vendor"
            rec.vendor=vendor
            rec.zone=zone
            rec.faction=faction
            rec.costs=costParts(i,price,extendedCost)
            rec.learned=true
            rec.updated=time and time() or 0
            self.db.sourceIntel[itemID]=rec
        end
    end
    if self.state then self.state.acquisitionTextCache={} end
end

function T:FormatDBSource(rec)
    if not rec or not rec.itemID then return nil end
    local d=self:GetSourceDBRecord(rec.itemID,rec.itemName or rec.name)
    if not d then return nil end
    -- Merge a bundled in-game vendor guide when the client identifies a vendor
    -- source but does not ship the merchant inventory relationship.
    if (d.kind=="Vendor" or d.sourceKind=="Vendor") and self.GetVendorGuide then
        local guide=self:GetVendorGuide({itemID=rec.itemID,itemName=d.itemName or rec.itemName or rec.name,name=rec.name})
        if guide then
            if (not d.vendor) and (not d.vendors) and guide.vendors then d.vendors=guide.vendors end
            d.whereToGo=d.whereToGo or guide.whereToGo
            d.vendorGuideTitle=d.vendorGuideTitle or guide.title
            d.vendorGuideNote=d.vendorGuideNote or guide.note
        end
    end
    -- Item/appearance/set metadata alone does not explain how to obtain an item.
    -- Let the resolver continue to Blizzard's source type instead of presenting a
    -- misleading DB block when no acquisition relationship exists.
    local hasAcquisition=d.kind or d.sourceKind or d.vendor or d.vendors or d.quest or d.npcs or d.encounter or d.instance or d.profession or d.recipe or d.questPackages or d.fullClientDB or (type(d.encounters)=="table" and #d.encounters>0) or (type(d.costs)=="table" and #d.costs>0) or d.currency or d.amount or d.money
    if not hasAcquisition then return nil end
    local lines={}
    local function add(s) if s and s~="" then lines[#lines+1]=s end end
    add("HOW TO OBTAIN")
    add("Item: "..(d.itemName or rec.itemName or rec.name or "Unknown item"))
    local kind=d.kind or d.sourceKind
    if kind then add("Source: "..kind) end
    if kind=="Vendor" or d.vendor or d.vendors then
        if d.vendor then
            local where=d.zone and (" — "..d.zone) or ""
            local fac=d.faction and (" ["..d.faction.."]") or ""
            add("Vendor: "..d.vendor..where..fac)
        end
        if type(d.vendors)=="table" then
            add("Vendors:")
            for _,v in ipairs(d.vendors) do
                local where=v.zone and (" — "..v.zone) or ""
                local fac=v.faction and (" ["..v.faction.."]") or ""
                add("  "..(v.name or "Vendor")..where..fac)
            end
        end
    end
    if d.whereToGo then
        add("")
        add("WHERE TO GO")
        add(d.whereToGo)
        if d.vendorGuideNote then add(d.vendorGuideNote) end
    end
    if d.quest then add("Quest: "..d.quest) end
    if d.zone and kind~="Vendor" and not d.vendor then add("Zone: "..d.zone) end
    if d.npcs and #d.npcs>0 then
        add((kind=="World Boss" and "World boss:" or kind=="World Drop" and "Drops from:" or "NPC:").." "..table.concat(d.npcs,", "))
    end
    if type(d.encounters)=="table" and #d.encounters>0 then
        add("Drops from:")
        for i=1,math.min(#d.encounters,6) do
            local e=d.encounters[i]
            local line="  "..(e.encounter or "Encounter")
            if e.instance and e.instance~="" then line=line.." — "..e.instance end
            if e.difficulty and e.difficulty~="" then line=line.." ["..e.difficulty.."]" end
            add(line)
        end
        if #d.encounters>6 then add("  +"..tostring(#d.encounters-6).." additional encounter source(s)") end
    else
        if d.instance then add("Instance: "..d.instance) end
        if d.encounter then add("Boss: "..d.encounter) end
        if d.difficulty then add("Difficulty: "..d.difficulty) end
    end
    if type(d.sets)=="table" and #d.sets>0 then
        local names={}
        for i=1,math.min(#d.sets,3) do if d.sets[i].name then names[#names+1]=d.sets[i].name end end
        if #names>0 then add("Set: "..table.concat(names,", ")) end
    end
    if d.profession then add("Crafted by: "..d.profession) end
    if d.recipe then add("Recipe: "..d.recipe) end
    if d.skillRank and d.skillRank>0 then add("Required skill: "..tostring(d.skillRank)) end
    if type(d.reagents)=="table" and #d.reagents>0 then
        add("Materials:")
        for i=1,math.min(#d.reagents,8) do
            local r=d.reagents[i]
            local rn=r.name
            if (not rn or rn=="") and r.itemID and self.ResolveDisplayItemName then rn=self:ResolveDisplayItemName(r.itemID) end
            add("  "..tostring(r.count or 1).." x "..(rn or "Loading material name..."))
        end
        if #d.reagents>8 then add("  +"..tostring(#d.reagents-8).." additional material(s)") end
    end
    if type(d.questPackages)=="table" and #d.questPackages>0 then
        add("Quest reward data: this item is present in Blizzard quest reward package data.")
        if not d.quest then add("Exact quest title is not linked by the local client tables exported so far.") end
    end
    if type(d.costs)=="table" and #d.costs>0 then
        local parts={}
        for _,c in ipairs(d.costs) do
            if c.type=="money" and c.text then parts[#parts+1]=c.text
            elseif c.type=="currency" then
                local cn=c.name
                if (not cn or cn=="" or cn=="Currency") and c.itemID and self.ResolveDisplayItemName then cn=self:ResolveDisplayItemName(c.itemID,"Loading currency name...") end
                parts[#parts+1]=tostring(c.amount or 0).." "..(cn or "Currency")
            end
        end
        if #parts>0 then add("Cost: "..table.concat(parts," + ")) end
    elseif d.amount and d.currency then
        add("Cost: "..tostring(d.amount).." "..d.currency)
    end
    if d.reputation then add("Reputation: "..d.reputation) end
    if d.requiredFactionName and d.requiredFactionName~="" then
        local rep=d.requiredReputationName and (" - "..d.requiredReputationName) or ""
        add("Reputation requirement: "..d.requiredFactionName..rep)
    end
    if d.requiredLevel and d.requiredLevel>0 then add("Required level: "..tostring(d.requiredLevel)) end
    if d.requiredSkillRank and d.requiredSkillRank>0 and not d.profession then add("Required profession skill rank: "..tostring(d.requiredSkillRank)) end
    if d.startsQuestID and d.startsQuestID>0 then add("This item starts quest ID: "..tostring(d.startsQuestID)) end
    if kind=="Vendor" and type(d.costs)~="table" and not (d.amount and d.currency) then
        if d.hasCurrencyCost then
            add("Cost: special currency or token cost.")
            add("The exact price is shown by the vendor in-game. Transmoggin will remember the vendor and price after you open that merchant once.")
        elseif d.baseBuyPrice and d.baseBuyPrice>0 then
            local txt=self.FormatMoneyPlain and self.FormatMoneyPlain(d.baseBuyPrice) or nil
            if txt then add("Client base vendor price: "..txt) end
        end
    end
    if kind=="Quest Reward" and not d.quest and not d.questPackages then
        add("Quest source is verified by the client. Exact quest-to-reward linkage is not shipped in this client DB export.")
    elseif kind=="World Drop" and not d.npcs then
        add("World-drop source is verified by the client. Exact creature loot tables are not shipped in this client DB export.")
    end
    if type(d.sourceTypes)=="table" and #d.sourceTypes>1 and self.GeneratedSourceTypeNames then
        local labels={}
        for _,sourceType in ipairs(d.sourceTypes) do local label=self.GeneratedSourceTypeNames[sourceType]; if label and label~="Unknown / alternate source" then labels[#labels+1]=label end end
        if #labels>1 then add("Client source variants: "..table.concat(labels,", ")) end
    end
    if d.note then add(d.note) end
    if d.clientDB then add("Data: local MoP Classic client Encounter Journal DB") end
    if d.clientCraftDB then add("Data: local MoP Classic profession/recipe DB") end
    if d.clientQuestPackageDB and not d.clientDB and not d.clientCraftDB then add("Data: local MoP Classic quest package DB") end
    if d.fullClientDB and not d.clientDB and not d.clientCraftDB and not d.clientQuestPackageDB then add("Data: complete local MoP Classic client DB export") end
    return table.concat(lines,"\n")
end
