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 vendor currency costs (persisted in SavedVariables)
-- Format: { [itemID] = { {amount=N, name="Currency", icon=fileID}, ... } }
local currencyCache = {}
local currencyDirty = false

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

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

local function BuildReverseIndex()
    wipe(HDG_VendorByItem)
    wipe(HDG_VendorByMapID)
    if not HDG_VendorDB then return end
    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
    -- Load currency cache
    if _G.HDG_DB and _G.HDG_DB.vendorCurrencyCache then
        currencyCache = _G.HDG_DB.vendorCurrencyCache
    end
    -- Build reverse index
    BuildReverseIndex()
end

function HDG_VendorResolver.SaveCache()
    -- Save cache to SavedVariables
    if HDG and HDG.Store then
        HDG.Store.state.vendorNameCache = nameCache
        HDG.Store:QueueSave()
    elseif _G.HDG_DB then
        _G.HDG_DB.vendorNameCache = nameCache
    end
end

function HDG_VendorResolver.SaveCurrencyCache()
    if not currencyDirty then return end
    _G.HDG_DB = _G.HDG_DB or {}
    _G.HDG_DB.vendorCurrencyCache = currencyCache
    currencyDirty = false
    if HDG and HDG.Store then
        HDG.Store:QueueSave()
    end
end

-- ============================================================================
-- CURRENCY SCANNING
-- Scan merchant items for currency costs when player opens a vendor
-- ============================================================================

function HDG_VendorResolver.ScanMerchantCurrencies()
    local numItems = GetMerchantNumItems()
    if not numItems or numItems == 0 then return end

    for i = 1, numItems do
        local link = GetMerchantItemLink(i)
        if link then
            local itemID = tonumber(link:match("item:(%d+)"))
            if itemID and not currencyCache[itemID] then
                local numCosts = GetMerchantItemCostInfo(i)
                if numCosts and numCosts > 0 then
                    local costs = {}
                    for j = 1, numCosts do
                        local texture, amount, itemLink, currencyName = GetMerchantItemCostItem(i, j)
                        if amount and amount > 0 then
                            -- Resolve name: currency link name > itemLink name > fallback
                            local name = currencyName or ""
                            if (name == "" or name == nil) and itemLink then
                                name = itemLink:match("%[(.-)%]") or ""
                            end
                            table.insert(costs, {
                                amount = amount,
                                name = name,
                                icon = texture or 0,
                            })
                        end
                    end
                    if #costs > 0 then
                        currencyCache[itemID] = costs
                        currencyDirty = true
                    end
                end
            end
        end
    end

    HDG_VendorResolver.SaveCurrencyCache()
end

-- O(1) lookup: returns {{amount, name, icon}, ...} or nil
-- Checks static VendorDB item field [5] first, falls back to runtime currencyCache
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 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]
                    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
                    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

    -- Fall back to runtime merchant scanning cache
    return currencyCache[itemID]
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)
    if C_TooltipInfo and C_TooltipInfo.GetHyperlink then
        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
                HDG_VendorResolver.CacheName(npcID, name)
                return name
            end
        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)
    local success, tooltipData = pcall(function()
        local hyperlink = string.format("unit:Creature-0-0-0-0-%d-0", npcID)
        return C_TooltipInfo.GetHyperlink(hyperlink)
    end)

    if success and 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
            HDG_VendorResolver.CacheName(npcID, name)
            return name
        end
    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
    HDG_VendorResolver.SaveCache()
end

-- ============================================================================
-- EVENT-BASED RESOLUTION
-- Opportunistically cache NPC names when player interacts with them
-- ============================================================================

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
eventFrame:RegisterEvent("GOSSIP_SHOW")
eventFrame:RegisterEvent("MERCHANT_SHOW")
eventFrame:RegisterEvent("QUEST_GREETING")

local function ProcessTargetEvent(unit, event)
    if not UnitExists(unit) then return end

    local guid = UnitGUID(unit)
    if not guid then return end

    -- UnitGUID can return a "secret value" in tainted contexts (TWW 12.0+)
    local ok, unitType, _, _, _, npcID = pcall(strsplit, "-", guid)
    if not ok or unitType ~= "Creature" then return end

    npcID = tonumber(npcID)
    if not npcID or npcID == 0 then return end

    -- Check if this NPC is in our vendor database — O(1) lookup
    if not HDG_VendorDB or not HDG_VendorDB[npcID] then return end

    local name = UnitName(unit)
    if name and name ~= "" then
        HDG_VendorResolver.CacheName(npcID, name)
    end

    -- Scan currency costs when opening a merchant
    if event == "MERCHANT_SHOW" then
        HDG_VendorResolver.ScanMerchantCurrencies()
    end
end

eventFrame:SetScript("OnEvent", function(self, event, ...)
    local unit = (event == "PLAYER_TARGET_CHANGED") and "target" or "npc"
    if event == "PLAYER_TARGET_CHANGED" then
        -- Defer to next frame to break taint chain from hardware input;
        -- UnitGUID returns a "secret value" in the direct click call path
        C_Timer.After(0, function() ProcessTargetEvent(unit, event) end)
    else
        ProcessTargetEvent(unit, event)
    end
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",
                    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 gold cost (copper to gold/silver/copper display)
local function FormatWithCommas(n)
    local s = tostring(n)
    local pos = #s % 3
    if pos == 0 then pos = 3 end
    local parts = { s:sub(1, pos) }
    for i = pos + 1, #s, 3 do
        parts[#parts + 1] = s:sub(i, i + 2)
    end
    return table.concat(parts, ",")
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 = 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
    -- Invalidate stale negative cache (catalog may not have been ready earlier)
    if HDG_Data.InvalidateCollectionCache then HDG_Data.InvalidateCollectionCache() 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
    -- Invalidate stale negative cache (catalog may not have been ready earlier)
    if HDG_Data.InvalidateCollectionCache then HDG_Data.InvalidateCollectionCache() 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()
