local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide
local CONFIG = privateDB.CONFIG

-- ============================================================================
-- VENDOR NAME RESOLVER MODULE
-- Runtime resolution of NPC names from "NPC #ID" fallbacks
-- Builds HDG_VendorByItem reverse index for O(1) item→vendor lookups
-- ============================================================================

HDG_VendorResolver = {}

-- Reverse index: [itemID] = { npcID1, npcID2, ... }
-- Built once from HDG_VendorDB (vendor-keyed), shared globally
HDG_VendorByItem = HDG_VendorByItem or {}

-- Reverse index: [uiMapID] = { npcID1, npcID2, ... }
HDG_VendorByMapID = HDG_VendorByMapID or {}

-- Cache of resolved NPC names (persisted in SavedVariables)
-- Format: { [npcID] = "NPC Name" }
local nameCache = {}

-- Cache of resolved currency costs (resolved from static VendorDB data)
local currencyCache = {}
local nameCacheDirty = false

-- Pending resolution requests (npcIDs we've tried to resolve this session)
local pendingResolution = {}

-- ============================================================================
-- REVERSE INDEX
-- ============================================================================

-- ============================================================================
-- SHOP BUNDLES (letter assignment for In-Game Shop display grouping)
-- ============================================================================

-- Letters are assigned alphabetically by bundle display name, stable within
-- a session. Computed lazily on first lookup. Item→bundle membership is
-- derived from bundle.contents (single source of truth).
local bundleLetters    -- { [bundleKey] = "a"|"b"|... }
local bundleMeta       -- ordered array: { { key, letter, name, cost, moneyCost }, ... }
local itemBundleKey    -- { [itemID] = bundleKey } first bundle wins for multi-bundle items

local function BuildBundleLetters()
    bundleLetters = {}
    bundleMeta = {}
    itemBundleKey = {}
    if not HDG_ShopDB or not HDG_ShopDB.bundles then return end
    local keys = {}
    for k in pairs(HDG_ShopDB.bundles) do table.insert(keys, k) end
    table.sort(keys, function(a, b)
        local na = HDG_ShopDB.bundles[a].name or tostring(a)
        local nb = HDG_ShopDB.bundles[b].name or tostring(b)
        return na < nb
    end)
    for i, k in ipairs(keys) do
        local letter = string.char(96 + i) -- 'a' = 97
        bundleLetters[k] = letter
        local b = HDG_ShopDB.bundles[k]
        bundleMeta[i] = {
            key = k, letter = letter,
            name = b.name or tostring(k),
            cost = b.cost, moneyCost = b.moneyCost,
        }
        if b.contents then
            for itemID in pairs(b.contents) do
                if not itemBundleKey[itemID] then
                    itemBundleKey[itemID] = k
                end
            end
        end
    end
end

function HDG_VendorResolver.GetShopBundleLetter(itemID)
    if not itemID then return nil end
    if not bundleLetters then BuildBundleLetters() end
    local key = itemBundleKey and itemBundleKey[itemID]
    return key and bundleLetters[key] or nil
end

-- Returns the bundle key (not letter) that an item belongs to, or nil for solo items.
function HDG_VendorResolver.GetShopBundleKey(itemID)
    if not itemID then return nil end
    if not bundleLetters then BuildBundleLetters() end
    return itemBundleKey and itemBundleKey[itemID] or nil
end

-- Returns an ordered array of bundle metadata for legend rendering:
--   { { key, letter, name, cost, moneyCost }, ... } sorted by letter ascending
function HDG_VendorResolver.GetShopBundleLegend()
    if not bundleMeta then BuildBundleLetters() end
    return bundleMeta or {}
end

-- Synthesize a HDG_VendorDB entry from HDG_ShopDB so the In-Game Shop renders
-- through the existing By Vendor pipeline. Shop-specific bundles/items stay in
-- HDG_ShopDB; only the flat vendor row is materialized here.
local function SynthesizeShopVendor()
    if not HDG_ShopDB or not HDG_ShopDB.vendor then return end
    local v = HDG_ShopDB.vendor
    local items = {}
    for itemID, item in pairs(HDG_ShopDB.items or {}) do
        local currCost
        if item.cost then
            currCost = {{"h", item.cost}}
        elseif item.bundleOnly then
            -- Amount 0 renders as "Bundle only". Non-nil currCost also prevents
            -- the zero-cost released-only filter from dropping these items.
            currCost = {{"h", 0}}
        end
        items[itemID] = {0, 0, 0, "", currCost}
    end
    HDG_VendorDB[v.npcID] = {
        v.name, v.zone, 0, 0, 0, v.faction,
        exp = v.expansion, added = v.added, note = v.note,
        items = items,
    }
end

local function BuildReverseIndex()
    wipe(HDG_VendorByItem)
    wipe(HDG_VendorByMapID)
    if not HDG_VendorDB then return end
    SynthesizeShopVendor()
    for npcID, vendorEntry in pairs(HDG_VendorDB) do
        if vendorEntry.items then
            for itemID, _ in pairs(vendorEntry.items) do
                if not HDG_VendorByItem[itemID] then
                    HDG_VendorByItem[itemID] = {}
                end
                table.insert(HDG_VendorByItem[itemID], npcID)
            end
        end
        local mapID = vendorEntry[3]
        if mapID and mapID > 0 then
            if not HDG_VendorByMapID[mapID] then
                HDG_VendorByMapID[mapID] = {}
            end
            table.insert(HDG_VendorByMapID[mapID], npcID)
        end
    end
end

-- ============================================================================
-- INITIALIZATION
-- ============================================================================

function HDG_VendorResolver.Initialize()
    -- Load cached names from SavedVariables
    if HDG and HDG.Store then
        local state = HDG.Store:GetState()
        if state.vendorNameCache then
            nameCache = state.vendorNameCache
        end
    elseif _G.HDG_DB and _G.HDG_DB.vendorNameCache then
        nameCache = _G.HDG_DB.vendorNameCache
    end
    -- Build reverse index
    BuildReverseIndex()
end

function HDG_VendorResolver.SaveCache()
    if not nameCacheDirty then return end
    if HDG and HDG.Store then
        HDG.Store:SetVendorNameCache(nameCache)
    end
    nameCacheDirty = false
end

-- Format an icon (fileID number or atlas string) into a WoW inline escape code.
-- Returns "" for empty/missing. Used by By Vendor cost rendering so the caller
-- does not need to branch on icon type.
function HDG_VendorResolver.FormatCurrencyIcon(icon, size)
    if not icon or icon == 0 or icon == "" then return "" end
    if type(icon) == "string" then
        return string.format("|A:%s:%d:%d|a", icon, size, size)
    end
    return string.format("|T%s:%d:%d|t", icon, size, size)
end

-- O(1) lookup: returns {{amount, name, icon}, ...} or nil
-- Resolves static VendorDB item field [5] currency/item tuples to display data
function HDG_VendorResolver.GetCurrencyCost(itemID)
    if not itemID then return nil end

    -- Check static ATT data via reverse index
    local npcIDs = HDG_VendorByItem[itemID]
    if npcIDs and npcIDs[1] and HDG_VendorDB then
        local vendorEntry = HDG_VendorDB[npcIDs[1]]
        local itemEntry = vendorEntry and vendorEntry.items and vendorEntry.items[itemID]
        local staticCosts = itemEntry and itemEntry[5]
        if staticCosts and type(staticCosts) == "table" then
            -- Resolve names/icons at runtime if not yet done
            if not currencyCache[itemID] then
                local resolved = {}
                for _, tuple in ipairs(staticCosts) do
                    local costType, costID, amount = tuple[1], tuple[2], tuple[3]
                    local name = ""
                    local icon ---@type number|string|nil
                    if costType == "c" then
                        local info = C_CurrencyInfo.GetCurrencyInfo(costID)
                        if info then
                            name = info.name or ""
                            icon = info.iconFileID or 0
                        end
                    elseif costType == "i" then
                        local itemName, _, _, _, _, _, _, _, _, itemIcon = C_Item.GetItemInfo(costID)
                        name = itemName or ""
                        icon = itemIcon or 0
                    elseif costType == "h" then
                        -- Hearthsteel uses the "h" marker; amount lives in tuple[2] (no costID).
                        amount = tuple[2]
                        if amount == 0 then
                            -- Sentinel for bundle-only items: no individual price.
                            name = "Bundle only"
                            icon = nil
                            amount = ""
                        else
                            name = "Hearthsteel"
                            icon = "hearthsteel-icon-32x32"
                        end
                    end
                    table.insert(resolved, { amount = amount, name = name, icon = icon })
                end
                if #resolved > 0 then
                    currencyCache[itemID] = resolved
                end
            end
            return currencyCache[itemID]
        end
    end

    return nil
end

-- ============================================================================
-- NAME RESOLUTION
-- ============================================================================

-- Get vendor name for a vendor entry from HDG_VendorDB
-- vendorEntry format (new): {name, zone, mapID, x, y, faction, items={...}}
function HDG_VendorResolver.GetVendorName(vendorEntry)
    if not vendorEntry then return "Unknown Vendor" end

    local storedName = vendorEntry[1]

    -- If stored name is a real name (not "NPC #ID"), use it
    if storedName and not storedName:match("^NPC #%d+$") then
        return storedName
    end

    -- Check our resolved name cache (need npcID — caller should pass it)
    -- This function is now primarily used via GetVendorsForItem which provides context
    return storedName or "Unknown Vendor"
end

-- Get vendor name by npcID (preferred method with new format)
function HDG_VendorResolver.GetVendorNameByID(npcID)
    if not npcID then return "Unknown Vendor" end

    -- Check resolved name cache first
    if nameCache[npcID] then
        return nameCache[npcID]
    end

    -- Try localized name from WoW API (works on non-English clients).
    -- 12.0.5: tooltipData.lines[i].leftText is a "secret string" under taint;
    -- direct comparison/:find fault when execution is tainted by an addon.
    -- pcall the whole probe so we fall through to the DB fallback cleanly.
    if C_TooltipInfo and C_TooltipInfo.GetHyperlink then
        local ok, resolved = pcall(function()
            local tooltipData = C_TooltipInfo.GetHyperlink(string.format("unit:Creature-0-0-0-0-%d-0000000000", npcID))
            if tooltipData and tooltipData.lines and tooltipData.lines[1] then
                local name = tooltipData.lines[1].leftText
                if name and name ~= "" and not name:find("Retrieving") then
                    return name
                end
            end
            return nil
        end)
        if ok and resolved then
            HDG_VendorResolver.CacheName(npcID, resolved)
            return resolved
        end
    end

    -- Fallback to DB entry (English)
    if HDG_VendorDB and HDG_VendorDB[npcID] then
        local storedName = HDG_VendorDB[npcID][1]
        if storedName and not storedName:match("^NPC #%d+$") then
            return storedName
        end
    end

    -- Try to resolve via other WoW APIs (target, npc unit)
    local resolvedName = HDG_VendorResolver.TryResolveNPCName(npcID)
    if resolvedName then return resolvedName end

    return "NPC #" .. tostring(npcID)
end

-- Attempt to resolve an NPC name using WoW APIs
function HDG_VendorResolver.TryResolveNPCName(npcID)
    if not npcID or npcID == 0 then return nil end

    -- Already resolved?
    if nameCache[npcID] then
        return nameCache[npcID]
    end

    -- Don't spam resolution attempts
    if pendingResolution[npcID] then
        return nil
    end
    pendingResolution[npcID] = true

    -- Method 1: Check if player's current target is this NPC
    if UnitExists("target") then
        local guid = UnitGUID("target")
        if guid then
            local unitType, _, _, _, _, targetNpcID = strsplit("-", guid)
            if unitType == "Creature" and tonumber(targetNpcID) == npcID then
                local name = UnitName("target")
                if name then
                    HDG_VendorResolver.CacheName(npcID, name)
                    return name
                end
            end
        end
    end

    -- Method 2: Check current NPC interaction (if player is interacting with a vendor)
    if UnitExists("npc") then
        local guid = UnitGUID("npc")
        if guid then
            local unitType, _, _, _, _, interactNpcID = strsplit("-", guid)
            if unitType == "Creature" and tonumber(interactNpcID) == npcID then
                local name = UnitName("npc")
                if name then
                    HDG_VendorResolver.CacheName(npcID, name)
                    return name
                end
            end
        end
    end

    -- Method 3: Try C_TooltipInfo with unit hyperlink (may not work for all NPCs).
    -- 12.0.5: pcall must wrap the comparison too — leftText is a tainted secret
    -- string and `name ~= ""` faults on read, not just on the GetHyperlink call.
    local ok, resolved = pcall(function()
        local hyperlink = string.format("unit:Creature-0-0-0-0-%d-0", npcID)
        local tooltipData = C_TooltipInfo.GetHyperlink(hyperlink)
        if tooltipData and tooltipData.lines and tooltipData.lines[1] then
            local name = tooltipData.lines[1].leftText
            if name and name ~= "" and not name:match("^Creature") then
                return name
            end
        end
        return nil
    end)
    if ok and resolved then
        HDG_VendorResolver.CacheName(npcID, resolved)
        return resolved
    end

    return nil
end

-- Cache a resolved NPC name
function HDG_VendorResolver.CacheName(npcID, name)
    if not npcID or not name or name == "" then return end
    if name:match("^NPC #") or name:match("^Creature") then return end

    nameCache[npcID] = name
    pendingResolution[npcID] = nil -- allow future re-resolution
    nameCacheDirty = true
    HDG_VendorResolver.SaveCache()
end

-- ============================================================================
-- UTILITY: Get all vendors for a decoration item
-- ============================================================================

-- Get formatted vendor list for a decoration item
-- Returns: table of { npcID, name, zone, mapID, x, y, goldCost, factionID, minRep, factionName, currencyCost } with resolved names
function HDG_VendorResolver.GetVendorsForItem(itemID)
    if not HDG_VendorDB or not itemID then return nil end

    local npcIDs = HDG_VendorByItem[itemID]
    if not npcIDs or #npcIDs == 0 then return nil end

    local result = {}
    for _, npcID in ipairs(npcIDs) do
        local vendorEntry = HDG_VendorDB[npcID]
        if vendorEntry and vendorEntry.items then
            local itemEntry = vendorEntry.items[itemID]
            if itemEntry then
                table.insert(result, {
                    npcID = npcID,
                    name = HDG_VendorResolver.GetVendorNameByID(npcID),
                    zone = vendorEntry[2] or "Unknown",
                    mapID = vendorEntry[3] or 0,
                    x = vendorEntry[4] or 0,
                    y = vendorEntry[5] or 0,
                    vendorFaction = vendorEntry[6] or "N",
                    race = vendorEntry.race,
                    note = vendorEntry.note,
                    goldCost = itemEntry[1] or 0,
                    factionID = itemEntry[2] or 0,
                    minRep = itemEntry[3] or 0,
                    factionName = itemEntry[4] or "",
                    currencyCost = itemEntry[5], -- raw ATT format or nil
                    questID = itemEntry[6] or 0,
                    achieveID = itemEntry[7] or 0,
                })
            end
        end
    end

    return #result > 0 and result or nil
end

-- Standing level names (0=Hated through 7=Exalted)
local STANDING_NAMES = { -- Blizzard reaction IDs (1-based)
    [1] = "Hated", [2] = "Hostile", [3] = "Unfriendly", [4] = "Neutral",
    [5] = "Friendly", [6] = "Honored", [7] = "Revered", [8] = "Exalted",
}

-- Format reputation requirement as "FactionName — Standing" or nil
function HDG_VendorResolver.FormatRepRequirement(vendor)
    if not vendor or not vendor.factionID or vendor.factionID == 0 then return nil end
    local factionLabel = vendor.factionName ~= "" and vendor.factionName or ("Faction #" .. vendor.factionID)
    local standingLabel
    if CONFIG.RENOWN_FACTIONS and CONFIG.RENOWN_FACTIONS[vendor.factionID] then
        standingLabel = "Renown " .. vendor.minRep
    else
        standingLabel = STANDING_NAMES[vendor.minRep] or ("Standing " .. vendor.minRep)
    end
    return factionLabel .. " -- " .. standingLabel
end

-- Check if player meets vendor's reputation requirement
function HDG_VendorResolver.MeetsRepRequirement(vendor)
    if not vendor or not vendor.factionID or vendor.factionID == 0 then return true end
    -- Renown factions (use RENOWN_FACTIONS lookup table)
    if CONFIG.RENOWN_FACTIONS and CONFIG.RENOWN_FACTIONS[vendor.factionID] and C_MajorFactions and C_MajorFactions.GetMajorFactionData then
        local major = C_MajorFactions.GetMajorFactionData(vendor.factionID)
        if major then return (major.renownLevel or 0) >= vendor.minRep end
    end
    -- Traditional reputation: both data.reaction and minRep use Blizzard 1-based IDs (1=Hated..8=Exalted)
    if C_Reputation and C_Reputation.GetFactionDataByID then
        local data = C_Reputation.GetFactionDataByID(vendor.factionID)
        if data then return (data.reaction or 0) >= vendor.minRep end
    end
    return false
end

-- Format quest requirement as quest title or nil
function HDG_VendorResolver.FormatQuestRequirement(vendor)
    if not vendor or not vendor.questID or vendor.questID == 0 then return nil end
    local title = C_QuestLog.GetTitleForQuestID(vendor.questID)
    return title or ("Quest #" .. vendor.questID)
end

-- Check if player has completed the quest gate
function HDG_VendorResolver.MeetsQuestRequirement(vendor)
    if not vendor or not vendor.questID or vendor.questID == 0 then return true end
    return C_QuestLog.IsQuestFlaggedCompleted(vendor.questID)
end

-- Format achievement requirement as achievement name or nil
function HDG_VendorResolver.FormatAchievementRequirement(vendor)
    if not vendor or not vendor.achieveID or vendor.achieveID == 0 then return nil end
    local _, name = GetAchievementInfo(vendor.achieveID)
    return name or ("Achievement #" .. vendor.achieveID)
end

-- Check if player has earned the achievement gate
function HDG_VendorResolver.MeetsAchievementRequirement(vendor)
    if not vendor or not vendor.achieveID or vendor.achieveID == 0 then return true end
    local _, _, _, completed = GetAchievementInfo(vendor.achieveID)
    return completed
end

-- Format race name for display (e.g. "DarkIronDwarf" -> "Dark Iron Dwarf")
local function FormatRaceName(fileName)
    return fileName:gsub("(%l)(%u)", "%1 %2")
end

-- Get atlas name for race headshot icon
local function GetRaceAtlas(fileName)
    return "raceicon128-" .. fileName:lower() .. "-male"
end

-- Format race requirement as icon+name string, or nil if no race gate
function HDG_VendorResolver.FormatRaceRequirement(vendor)
    if not vendor or not vendor.race or #vendor.race == 0 then return nil end
    local parts = {}
    for _, r in ipairs(vendor.race) do
        local icon = CreateAtlasMarkup(GetRaceAtlas(r), 14, 14)
        parts[#parts + 1] = icon .. " " .. FormatRaceName(r)
    end
    return table.concat(parts, " or ")
end

-- Check if player's race meets vendor requirement
function HDG_VendorResolver.MeetsRaceRequirement(vendor)
    if not vendor or not vendor.race or #vendor.race == 0 then return true end
    local _, playerRace = UnitRace("player")
    for _, r in ipairs(vendor.race) do
        if r == playerRace then return true end
    end
    return false
end

function HDG_VendorResolver.FormatGoldCost(copper)
    if not copper or copper == 0 then
        return nil -- Currency cost, not gold
    end

    local gold = math.floor(copper / 10000)
    local silver = math.floor((copper % 10000) / 100)
    local copperRemainder = copper % 100
    local goldStr = privateDB.UIHelpers.FormatWithCommas(gold)

    if gold > 0 then
        if silver > 0 or copperRemainder > 0 then
            return string.format("%s|cFFFFD700g|r %d|cFFC0C0C0s|r %d|cFFB87333c|r", goldStr, silver, copperRemainder)
        else
            return string.format("%s|cFFFFD700g|r", goldStr)
        end
    elseif silver > 0 then
        if copperRemainder > 0 then
            return string.format("%d|cFFC0C0C0s|r %d|cFFB87333c|r", silver, copperRemainder)
        else
            return string.format("%d|cFFC0C0C0s|r", silver)
        end
    else
        return string.format("%d|cFFB87333c|r", copperRemainder)
    end
end

-- ============================================================================
-- ZONE QUERIES
-- ============================================================================

-- Convert zone-percentage coords (0-100) to map coords (0-1) with tolerance
function HDG_VendorResolver.ZonePctToMap(mapID, pctX, pctY)
    if not mapID or mapID == 0 then return nil end
    if not C_Map.GetMapInfo(mapID) then return nil end -- unknown/invalid map
    if not pctX or not pctY or (pctX == 0 and pctY == 0) then return nil end
    if pctX < -5 or pctX > 105 or pctY < -5 or pctY > 105 then return nil end
    local mapX = math.max(0, math.min(1, pctX / 100))
    local mapY = math.max(0, math.min(1, pctY / 100))
    return mapID, mapX, mapY
end

-- Player faction tag for filtering ("A" or "H")
local function GetPlayerFactionTag()
    local faction = UnitFactionGroup("player")
    return faction == "Alliance" and "A" or "H"
end

-- Quick check: does this zone have any vendors with uncollected items?
-- Exits early at first uncollected item for fast zone-change checks
function HDG_VendorResolver.ZoneHasUncollectedVendorItems(uiMapID)
    if not uiMapID or not HDG_VendorDB or not HDG_VendorByMapID[uiMapID] then return false end
    if not HDG_Data or not HDG_Data.IsItemCollected then return false end
    local pTag = GetPlayerFactionTag()
    for _, npcID in ipairs(HDG_VendorByMapID[uiMapID]) do
        local ve = HDG_VendorDB[npcID]
        if ve and ve.items then
            local vFaction = ve[6] or "N"
            if vFaction == "N" or vFaction == pTag then
                for itemID, _ in pairs(ve.items) do
                    if not HDG_Data.IsItemCollected(itemID) then return true end
                end
            end
        end
    end
    return false
end

-- Get all vendors in zone with their uncollected items
-- Returns: { {npcID, name, zone, mapID, x, y, uncollectedItems={{itemID, goldCost},...}} } or nil
function HDG_VendorResolver.GetVendorsInZoneWithUncollected(uiMapID)
    if not uiMapID or not HDG_VendorDB or not HDG_VendorByMapID[uiMapID] then return nil end
    if not HDG_Data or not HDG_Data.IsItemCollected then return nil end
    local pTag = GetPlayerFactionTag()
    local result = {}
    for _, npcID in ipairs(HDG_VendorByMapID[uiMapID]) do
        local ve = HDG_VendorDB[npcID]
        if ve and ve.items then
            local vFaction = ve[6] or "N"
            if vFaction == "N" or vFaction == pTag then
                local uncollected = {}
                for itemID, itemEntry in pairs(ve.items) do
                    if not HDG_Data.IsItemCollected(itemID) then
                        table.insert(uncollected, { itemID = itemID, goldCost = itemEntry[1] or 0 })
                    end
                end
                if #uncollected > 0 then
                    table.insert(result, {
                        npcID = npcID,
                        name = HDG_VendorResolver.GetVendorNameByID(npcID),
                        zone = ve[2] or "Unknown",
                        mapID = ve[3] or 0,
                        x = ve[4] or 0,
                        y = ve[5] or 0,
                        uncollectedItems = uncollected,
                    })
                end
            end
        end
    end
    return #result > 0 and result or nil
end

-- ============================================================================
-- DEBUG / STATS
-- ============================================================================

function HDG_VendorResolver.GetStats()
    local cachedCount = 0
    for _ in pairs(nameCache) do
        cachedCount = cachedCount + 1
    end

    local totalVendorNPCs = 0
    if HDG_VendorDB then
        for _ in pairs(HDG_VendorDB) do
            totalVendorNPCs = totalVendorNPCs + 1
        end
    end

    local unresolvedCount = 0
    if HDG_VendorDB then
        for npcID, vendorEntry in pairs(HDG_VendorDB) do
            local name = vendorEntry[1]
            if name and name:match("^NPC #%d+$") and not nameCache[npcID] then
                unresolvedCount = unresolvedCount + 1
            end
        end
    end

    return {
        cachedNames = cachedCount,
        totalVendorNPCs = totalVendorNPCs,
        unresolvedEntries = unresolvedCount,
    }
end

-- Initialize on addon load
HDG_VendorResolver.Initialize()
