--[[
============================================================================
Enhanced Toy Tracker  -  EToy/Modules/SourceInfo.lua
============================================================================
Turns a ToyData record into the human-readable "how do I get this toy" shown
in the detail panel and the row's source tag -- the same role EMT/EPT's
SourceInfo plays for mounts and pets.

ToyData now carries ATT-derived provider detail in addition to the category
code (src). Optional detail keys, all resolved to names at RUNTIME so nothing
locale-specific is baked on disk:
  v   vendor npcID         + c/cn currency item & count   (cg = gold copper)
  dn  drop NPC id  /  dc = {creature ids}
  q   quest id  (title via C_QuestLog.GetTitleForQuestID)
  a   achievement id (name via GetAchievementInfo)
  sk  profession name (literal)   ev = holiday/event name (literal)

When a toy has no stored vendor/quest/etc but the shared rare roster knows a
creature that drops it, we still surface "Drops from <Rare> in <Zone>" exactly
as before. No fabrication -- if nothing links it, we fall back to the
category-level sentence (honest gaps over wrong data).

SourceInfo:Describe(toyID, info) -> table:
  headline    short source label
  howto       source-aware action sentence
  dropSource  "Drops from <npc> in <zone>" or nil
  event       holiday/event name or nil
  skill       profession name or nil
  removed     boolean (true-unobtainable)  -- ToyData ships obtainable only
  removedText string or nil
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETT = NS.ETT
if not ETT then return end

ETT.SourceInfo = ETT.SourceInfo or {}
local SI = ETT.SourceInfo

local HEADLINE = {
    V = "Vendor",            D = "Drop / Object",   Q = "Quest",
    A = "Achievement",       H = "Holiday",         E = "World Event",
    P = "Profession",        X = "PvP",             B = "Black Market",
    F = "Expansion Feature", M = "Promotion",       I = "Instance",
    C = "Crafted",           U = "Other",
}

-- ----------------------------------------------------------------------------
-- RUNTIME NAME RESOLUTION
-- These mirror how the mount/pet panels resolve names: nothing is stored on
-- disk, everything comes from the live client or the shared caches that ETB /
-- EMT build as you play. Every call is guarded so a missing API never errors.
-- ----------------------------------------------------------------------------

-- npcID -> name. Prefers EMT's resolver (boss names + caches) when the mount
-- tracker is loaded in the same addon, then ETB's account-wide runtime cache.
local function npcName(npcID)
    if not npcID then return nil end
    local EMT = NS.EMT
    if EMT and EMT.NPCNames and EMT.NPCNames.Get then
        local ok, n = pcall(function() return EMT.NPCNames:Get(npcID) end)
        if ok and n then return n end
    end
    local tbl = _G.ETB_RuntimeNPCNames
    if tbl then
        local e = tbl[npcID]
        if type(e) == "table" then return e.n end
        if type(e) == "string" then return e end
    end
    return nil
end

-- Resolve a name, preferring a baked English name (info.dnm / info.vn) when the
-- live client cache hasn't seen this creature yet. Returns nil (never a raw id)
-- so the caller can fall back to an honest category sentence instead of printing
-- "creature #54321", which is what users were seeing on a cold cache.
local function npcDisplay(npcID, baked)
    if baked and baked ~= "" then return baked end
    return npcName(npcID)  -- nil when unresolved; caller handles the fallback
end

-- achievementID -> name (2nd return of GetAchievementInfo)
local function achName(achID)
    if not achID or not GetAchievementInfo then return nil end
    local ok, _, name = pcall(GetAchievementInfo, achID)
    if ok and type(name) == "string" and name ~= "" then return name end
    return nil
end

-- questID -> title (retail-engine API; nil until cached)
local function questTitle(questID)
    if not questID then return nil end
    if C_QuestLog and C_QuestLog.GetTitleForQuestID then
        local ok, t = pcall(C_QuestLog.GetTitleForQuestID, questID)
        if ok and type(t) == "string" and t ~= "" then return t end
    end
    return nil
end

-- Known currency/token names (AtlasLoot-confirmed) so the cost reads as a name
-- even before the client item cache warms up. Anything not listed resolves via
-- GetItemInfo at runtime, falling back to a generic word rather than an id.
local CURRENCY = {
    [21100] = "Coin of Ancestry",
    [23247] = "Burning Blossom",
    [33226] = "Tricky Treat",
    [37829] = "Brewfest Prize Token",
}

-- currency cost -> " for 350x Burning Blossom" (item name async via GetItemInfo)
local function costText(info)
    if info.cg then
        if GetCoinTextureString then
            local ok, s = pcall(GetCoinTextureString, info.cg)
            if ok and s then return " for " .. s end
        end
        return " for " .. tostring(math.floor(info.cg / 10000)) .. "g"
    end
    if info.c then
        local n = info.cn or 1
        local cur = CURRENCY[info.c] or GetItemInfo(info.c)
        if cur then
            return string.format(" for %d\195\151 %s", n, cur)  -- "10x Name"
        end
        return string.format(" for %d token%s", n, n == 1 and "" or "s")
    end
    return ""
end

-- ----------------------------------------------------------------------------
-- DROP ENRICHMENT (reverse index off the shared rare roster) -- unchanged
-- ETB.ERT.RareRoster is keyed [npcID]={n=name, L={{uiMapID,x,y},...}, ty={toys}}
-- We invert ty once to toyItemID -> {npc, map}. Built lazily on first lookup.
-- ----------------------------------------------------------------------------
local _toyDrop
local function buildDropIndex()
    _toyDrop = {}
    local ETB = NS.ETB
    local roster = ETB and ETB.ERT and ETB.ERT.RareRoster
    if type(roster) ~= "table" then return end
    for _, rec in pairs(roster) do
        local toys = rec and rec.ty
        if type(toys) == "table" then
            local map = rec.L and rec.L[1] and rec.L[1][1]
            for _, itemID in ipairs(toys) do
                if _toyDrop[itemID] == nil then
                    _toyDrop[itemID] = { npc = rec.n, map = map }
                end
            end
        end
    end
end

local function zoneName(mapID)
    if not mapID then return nil end
    if C_Map and C_Map.GetMapInfo then
        local info = C_Map.GetMapInfo(mapID)
        if info and info.name and info.name ~= "" then return info.name end
    end
    return nil
end

local function rosterDropLine(toyID)
    if not _toyDrop then buildDropIndex() end
    local hit = _toyDrop[toyID]
    if not hit or not hit.npc then return nil end
    local zone = zoneName(hit.map)
    if zone then return "Drops from " .. hit.npc .. " in " .. zone end
    return "Drops from " .. hit.npc
end

-- ----------------------------------------------------------------------------
-- CATEGORY-LEVEL HOW-TO  (fallback when no specific detail is stored)
-- ----------------------------------------------------------------------------
local HOWTO = {
    V = "Purchased from a vendor",
    D = "Drops from a creature or found on a world object",
    Q = "Earned by completing a quest",
    A = "Awarded for completing an achievement",
    H = "Obtained during a holiday or seasonal world event",
    E = "Obtained from a world event",
    P = "Made by a profession",
    X = "Earned through PvP",
    B = "Bought from the Black Market Auction House",
    F = "Tied to an expansion feature (garrison, farm, scenario, ...)",
    M = "From a promotion, the in-game shop, or the TCG",
    I = "Found inside a dungeon or raid",
    C = "Crafted by a profession",
    U = "Source varies",
}

-- Build the primary action sentence from the most specific detail available.
local function buildHowto(toyID, info)
    -- 1. Vendor purchase (we know who + how much). Name resolves at runtime; if
    --    it can't, say "Purchased from a vendor" rather than printing an id.
    if info.v then
        local who = npcDisplay(info.v, info.vn)
        if who then return "Sold by " .. who .. costText(info) end
        return "Purchased from a vendor" .. costText(info)
    end
    -- 2. Currency purchase with no stored vendor (e.g. event token store)
    if info.c or info.cg then
        if info.ev then return "Purchased during " .. info.ev .. costText(info) end
        return "Purchased from a vendor" .. costText(info)
    end
    -- 3. Quest reward
    if info.q then
        local t = questTitle(info.q)
        return t and ("Quest reward: " .. t) or "Quest reward"
    end
    -- 4. Achievement reward
    if info.a then
        local n = achName(info.a)
        return n and ("Reward for the achievement \"" .. n .. "\"")
            or "Awarded for an achievement"
    end
    -- 5. Drop from a specific encounter/rare NPC. Prefer the baked name so a
    --    real name shows even before the rare roster / client cache populates.
    if info.dn then
        local who = npcDisplay(info.dn, info.dnm)
        if who then return "Drops from " .. who end
        -- no name yet: lean on the event, then the rare roster, then category
        if info.ev then return "Drops during " .. info.ev end
        return rosterDropLine(toyID) or HOWTO[info.src] or HOWTO.U
    end
    -- 6. Drop from listed creatures (first one named via the cache)
    if type(info.dc) == "table" and info.dc[1] then
        local who = npcDisplay(info.dc[1], info.dcm)
        if who then return "Drops from " .. who end
        return rosterDropLine(toyID) or HOWTO[info.src] or HOWTO.U
    end
    -- 7. Profession craft -- but ONLY when a profession is genuinely the source
    --    (crafted toys). Drop/quest/vendor toys no longer claim a profession.
    if info.sk and (info.src == "P" or info.src == "C") then
        if info.sk == "Archaeology" then
            return "Solved through Archaeology"  -- dug up, not crafted
        end
        return info.sk .. " crafted"
    end
    -- 8. Fall back to the rare roster, then to the category sentence
    return rosterDropLine(toyID) or HOWTO[info.src] or HOWTO.U
end

function SI:Describe(toyID, info)
    info = info or {}
    local src = info.src or "U"
    -- Only consult the roster for the generic drop categories so a vendor/quest
    -- toy never gets a misleading "Drops from ..." line appended.
    local drop = (src == "D" or src == "I" or src == "U")
                 and not (info.v or info.q or info.a or info.dn)
                 and rosterDropLine(toyID) or nil
    return {
        headline    = HEADLINE[src] or "Other",
        howto       = buildHowto(toyID, info),
        dropSource  = drop,
        event       = info.ev,
        -- A profession is shown only when the toy is actually made by one.
        -- Engineering/Fishing/etc. tags that merely described an ATT tree branch
        -- are no longer surfaced on drops, quests, vendor buys or achievements.
        skill       = (src == "P" or src == "C") and info.sk or nil,
        removed     = false,
        removedText = nil,
    }
end

-- ----------------------------------------------------------------------------
-- ROW SOURCE TAG (short, colour-coded -- shown on the right of every row)
-- ----------------------------------------------------------------------------
local TAG = {
    V = "|cffffd100Vendor|r",  D = "|cffff8040Drop|r",    Q = "|cff40ff40Quest|r",
    A = "|cffffcc00Achiev|r",  H = "|cffff60ffHoliday|r", E = "|cff66ccffEvent|r",
    P = "|cffd0a060Prof|r",    X = "|cffff4040PvP|r",     B = "|cff999999BMAH|r",
    F = "|cff60d0ffFeature|r", M = "|cffcc99ffPromo|r",   I = "|cffff8888Instance|r",
    C = "|cffffaa44Craft|r",   U = "|cffaaaaaaOther|r",
}
function SI:SourceTag(info)
    return TAG[(info and info.src) or "U"] or TAG.U
end
