-- Enhanced Raid Browser — Boss Tooltip v3
-- Shows BiS items that drop from a boss + alternatives per slot
-- Persists through Blizzard tooltip refreshes via OnTooltipSetUnit hook
local ADDON_NAME, NS = ...
local ETB = NS.ETB
local ERB = ETB.ERB

local BT = {}
ERB.BossTooltip = BT
BT.enabled = true
BT.lastNPCID = nil
BT._lastNPCIsBoss = false
BT._pendingItems = {}

local MAX_ALTS = 3
local S = (ETB and ETB.Style) or {}
local HEADER_COLOR = S.HEADER or "|cff00ccff"
local SLOT_COLOR = "|cffffff00"
local ICON_OWNED = S.MARK_OWNED or "|TInterface\\RaidFrame\\ReadyCheck-Ready:14:14:0:0|t"
local ICON_NEEDED = S.MARK_MISSING or "|TInterface\\RaidFrame\\ReadyCheck-NotReady:14:14:0:0|t"
local RANK_COLORS = {
    [1] = "|cffff8000",
    [2] = "|cffa335ee",
    [3] = "|cff0070dd",
}

-- ============================================================================
-- HELPERS
-- ============================================================================

local function PlayerOwnsItem(itemID)
    if not C_TransmogCollection or not C_TransmogCollection.GetItemInfo then return false end
    local ok, appearanceID = pcall(C_TransmogCollection.GetItemInfo, itemID)
    if not ok or not appearanceID then return false end
    if C_TransmogCollection.GetAllAppearanceSources then
        local sOK, allSources = pcall(C_TransmogCollection.GetAllAppearanceSources, appearanceID)
        if sOK and allSources and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
            for _, sid in ipairs(allSources) do
                local hOK, has = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, sid)
                if hOK and has then return true end
            end
        end
    end
    return false
end

local function GetItemDisplay(itemID)
    local name, _, quality, _, _, _, _, _, _, texture = GetItemInfo(itemID)
    if not name then
        GetItemInfo(itemID)
        name = "Loading..."
        quality = 4
    end
    local qColor = "|cffffffff"
    if quality and quality > 0 then
        local r, g, b = GetItemQualityColor(quality)
        qColor = string.format("|cff%02x%02x%02x", r*255, g*255, b*255)
    end
    return name, qColor, texture
end

-- ============================================================================
-- DIFFICULTY TIER
-- Tag each BiS drop with the difficulty it represents, Heroic shown as the
-- better one. JLoot diff bitmask: 1=Normal, 2=Heroic, 8=Thunderforged (a
-- Heroic-tier upgrade). The BiS lists pick a specific high-difficulty itemID
-- that often isn't in JLoot at all; those resolve only through BiSNpcBridge
-- and are the Heroic/Warforged Siege/ToT versions, so a bridge-only hit is
-- treated as Heroic. Items with no raid difficulty data (crafted, reputation,
-- world/pre-raid) get no tag.
-- ============================================================================

local TIER_BADGE = {
    heroic = " |cffa335ee[H]|r",   -- epic purple = the better (higher-ilvl) version
    normal = " |cff808080[N]|r",   -- grey = the accessible version
}
local TOKEN_BADGE = " |cffffd100[T]|r"   -- gold = drops as a class set token
local SHARED_BADGE = " |cff33d6ff[SL]|r" -- cyan = shared instance loot (any boss / trash)

local function GetItemDiffMask(itemID)
    if not ETB or not ETB.JLoot then return nil end
    local ld = ETB.JLoot[itemID]
    if not ld then return nil end
    if type(ld[1]) == "table" then
        local mask = 0
        for _, e in ipairs(ld) do mask = bit.bor(mask, e[2] or 15) end
        return mask
    end
    return ld[2] or 15
end

local function MaskToTier(mask)
    if not mask then return nil end
    if bit.band(mask, 2) > 0 or bit.band(mask, 8) > 0 then return "heroic" end
    if bit.band(mask, 1) > 0 then return "normal" end
    return nil
end

local function GetBiSItemTier(itemID)
    local tier = MaskToTier(GetItemDiffMask(itemID))
    if tier then return tier end
    if ERB.BiSNpcBridge and ERB.BiSNpcBridge[itemID] then return "heroic" end
    -- Tier set piece: it isn't a direct boss drop, so derive its difficulty from
    -- the token that grants it (the token is the actual drop and carries the diff).
    local token = ETB.TierPieceToToken and ETB.TierPieceToToken[itemID]
    if token then
        return MaskToTier(GetItemDiffMask(token))
    end
    return nil
end

-- True when this BiS item is a tier set piece obtained from a class token drop.
local function IsTierToken(itemID)
    return (ETB.TierPieceToToken and ETB.TierPieceToToken[itemID]) ~= nil
end

-- True when this BiS item is shared instance loot (a "Shared Boss Loot" or
-- instance "Trash Drops" pseudo-encounter, idx >= 9000) rather than the drop of
-- one specific boss. Such items are spread across every boss in the instance.
local function IsSharedLoot(itemID)
    if not ETB or not ETB.JLoot then return false end
    local ld = ETB.JLoot[itemID]
    if not ld then return false end
    local function pseudo(enc)
        local d = ETB.JEncounters and ETB.JEncounters[enc]
        return d and d[3] and d[3] >= 9000
    end
    if type(ld[1]) == "table" then
        for _, e in ipairs(ld) do if pseudo(e[1]) then return true end end
        return false
    end
    return pseudo(ld[1]) or false
end

-- ============================================================================
-- BOSS → PHASE MAPPING
-- Determines which BiS phase a boss belongs to based on instance.
-- PR = Dungeons, T14 raids (MSV, HoF, ToES), Sha of Anger, Galleon
-- P3 = Throne of Thunder, Nalak, Oondasta
-- P5 = Siege of Orgrimmar, Ordos
-- ============================================================================

local INSTANCE_PHASE = {
    -- Dungeons
    [246]="PR", [302]="PR", [303]="PR", [311]="PR", [312]="PR",
    [313]="PR", [316]="PR", [321]="PR", [324]="PR",
    -- T14 Raids
    [317]="PR", [330]="PR", [320]="PR",
    -- World bosses (default PR, overridden per-encounter below)
    [322]="PR",
    -- T15
    [362]="P3",
    -- T16 (Siege of Orgrimmar)
    [369]="P5",
}

-- World bosses by encounter ID (instance 322 has mixed phases)
local ENCOUNTER_PHASE = {
    [814]="P3",  -- Nalak
    [826]="P3",  -- Oondasta
    [861]="P5",  -- Ordos, Fire-God of the Yaungol (Timeless Isle, patch 5.4)
}

local function GetBossPhase(npcID)
    if not ETB then return nil end
    if ETB._BossLootLoader then
        local inCombat = (InCombatLockdown and InCombatLockdown())
            or (UnitAffectingCombat and UnitAffectingCombat("player"))
        if inCombat or (ETB.IsLowMemoryMode and ETB:IsLowMemoryMode()) then return nil end
        ETB._BossLootLoader(); ETB._BossLootLoader = nil
    end
    if not ETB.JNpcToEnc or not ETB.JEncounters then return nil end

    local encID = ETB.JNpcToEnc[npcID]
    if not encID then return nil end

    -- Check encounter-level override first (world bosses)
    if ENCOUNTER_PHASE[encID] then return ENCOUNTER_PHASE[encID] end

    -- Check instance-level mapping
    local encData = ETB.JEncounters[encID]
    if encData and encData[2] then
        return INSTANCE_PHASE[encData[2]]
    end

    return nil
end

function BT:AddBiSToTooltip(tooltip, npcID)
    if not tooltip or not npcID then return end
    if not ERB.DataBridge then return end

    local bisItems = ERB.DataBridge:GetBiSForNPC(npcID)
    if not bisItems or not next(bisItems) then return end

    local info = ERB:GetPlayerInfo()
    self._pendingItems = {}

    -- Determine which phase this boss belongs to, scoped to the ACTIVE source.
    -- The two curated sources cover different tiers (WoWHead = PR+P5 Siege,
    -- WoWTBC = PR+P3 ToT), so flipping the source toggle re-renders this tooltip
    -- against that source: a Siege boss lights up on WoWHead, a ToT boss on
    -- WoWTBC. We only ever search phases the active source actually has.
    local availPhases = ERB.DataBridge:GetAvailablePhases()
    local availSet = {}
    for _, p in ipairs(availPhases) do availSet[p] = true end

    local bossPhase = GetBossPhase(npcID)
    local phasesToSearch
    if bossPhase and availSet[bossPhase] then
        -- Active source covers this boss's tier — restrict to it (clean, no
        -- cross-tier alt clutter).
        phasesToSearch = { bossPhase }
    else
        -- Unknown boss, or the active source doesn't cover this boss's tier.
        -- Search the active source's phases; the NPC index gate above already
        -- limits us to items this source ranks for this boss.
        phasesToSearch = availPhases
    end

    -- Collect boss drops from matching phases only
    local slotDrops = {}

    for itemID in pairs(bisItems) do
        for _, slot in ipairs(ERB.DataBridge.SLOT_ORDER) do
            for _, phase in ipairs(phasesToSearch) do
                local slotItems = ERB.DataBridge:GetItemsForSlot(slot, phase)
                for _, item in ipairs(slotItems) do
                    if item.itemID == itemID then
                        if not slotDrops[slot] then slotDrops[slot] = {} end
                        local found = false
                        for _, d in ipairs(slotDrops[slot]) do
                            if d.itemID == itemID then found = true; break end
                        end
                        if not found then
                            slotDrops[slot][#slotDrops[slot] + 1] = {
                                itemID = itemID,
                                rank = item.rank,
                                label = item.label,
                                isBossDrop = true,
                                phase = phase,
                            }
                        end
                    end
                end
            end
        end
    end

    -- Safety net: npcIndex already confirmed this boss drops BiS for the player,
    -- so an empty slotDrops here means the phase scoping missed (e.g. a boss
    -- whose tier wasn't mapped, or future phase-detection drift). Rather than
    -- return silently and show nothing, retry across every phase the active
    -- source actually has — the npcIndex gate keeps this to real hits.
    if not next(slotDrops) and #phasesToSearch < #availPhases then
        phasesToSearch = availPhases
        for itemID in pairs(bisItems) do
            for _, slot in ipairs(ERB.DataBridge.SLOT_ORDER) do
                for _, phase in ipairs(phasesToSearch) do
                    local slotItems = ERB.DataBridge:GetItemsForSlot(slot, phase)
                    for _, item in ipairs(slotItems) do
                        if item.itemID == itemID then
                            if not slotDrops[slot] then slotDrops[slot] = {} end
                            local found = false
                            for _, d in ipairs(slotDrops[slot]) do
                                if d.itemID == itemID then found = true; break end
                            end
                            if not found then
                                slotDrops[slot][#slotDrops[slot] + 1] = {
                                    itemID = itemID,
                                    rank = item.rank,
                                    label = item.label,
                                    isBossDrop = true,
                                    phase = phase,
                                }
                            end
                        end
                    end
                end
            end
        end
    end

    if not next(slotDrops) then return end
    for slot, drops in pairs(slotDrops) do
        local dropIDs = {}
        for _, d in ipairs(drops) do dropIDs[d.itemID] = true end
        local altCount = 0
        for _, phase in ipairs(phasesToSearch) do
            local slotItems = ERB.DataBridge:GetItemsForSlot(slot, phase)
            for _, item in ipairs(slotItems) do
                if not dropIDs[item.itemID] and altCount < MAX_ALTS then
                    local already = false
                    for _, d in ipairs(drops) do
                        if d.itemID == item.itemID then already = true; break end
                    end
                    if not already then
                        drops[#drops + 1] = {
                            itemID = item.itemID,
                            rank = item.rank,
                            label = item.label,
                            isBossDrop = false,
                            phase = phase,
                        }
                        dropIDs[item.itemID] = true
                        altCount = altCount + 1
                    end
                end
            end
        end
        table.sort(drops, function(a, b)
            if a.isBossDrop ~= b.isBossDrop then return a.isBossDrop end
            return (a.rank or 99) < (b.rank or 99)
        end)
    end

    -- Count needed
    local totalNeeded = 0
    for _, drops in pairs(slotDrops) do
        for _, item in ipairs(drops) do
            if item.isBossDrop and not PlayerOwnsItem(item.itemID) then
                totalNeeded = totalNeeded + 1
            end
        end
    end

    -- Header
    tooltip:AddLine(" ")
    local classColor = info.classColor or "|cffffffff"
    local specName = ERB.DataBridge.playerSpecName or info.specName or "?"
    local srcLabel = ERB.DataBridge:GetSourceLabel(ERB.DataBridge.activeSource) or ""
    local phaseLabel = (bossPhase and availSet[bossPhase]) and ERB.DataBridge:GetPhaseName(bossPhase) or ""

    tooltip:AddDoubleLine(
        HEADER_COLOR .. "BiS: |r" .. classColor .. (info.className or "?") .. "|r " .. specName,
        HEADER_COLOR .. totalNeeded .. " needed|r |cff888888(" .. srcLabel .. (phaseLabel ~= "" and (", " .. phaseLabel) or "") .. ")|r"
    )

    -- Render each slot

    local anyTier = false
    local anyToken = false
    local anyShared = false
    for _, slot in ipairs(ERB.DataBridge.SLOT_ORDER) do
        local drops = slotDrops[slot]
        if drops then
            tooltip:AddLine(SLOT_COLOR .. slot .. ":|r")
            for _, item in ipairs(drops) do
                local name, qColor, texture = GetItemDisplay(item.itemID)
                if name == "Loading..." then
                    self._pendingItems[item.itemID] = npcID
                end
                local owned = PlayerOwnsItem(item.itemID)
                local icon = texture and ("|T" .. texture .. ":14:14:0:0|t ") or ""
                local rc = RANK_COLORS[item.rank] or "|cff888888"
                local rankTag = rc .. "#" .. item.rank .. " " .. item.label .. "|r"
                local status = owned and ICON_OWNED or ICON_NEEDED

                local prefix = item.isBossDrop and (" " .. icon) or ("   ")
                local nameStr = item.isBossDrop
                    and (qColor .. name .. "|r")
                    or ("|cff666666" .. name .. "|r")

                local tier = GetBiSItemTier(item.itemID)
                if tier then
                    nameStr = nameStr .. TIER_BADGE[tier]
                    anyTier = true
                end
                if IsTierToken(item.itemID) then
                    nameStr = nameStr .. TOKEN_BADGE
                    anyToken = true
                end
                if IsSharedLoot(item.itemID) then
                    nameStr = nameStr .. SHARED_BADGE
                    anyShared = true
                end

                if not item.isBossDrop then
                    rankTag = "|cff555555#" .. item.rank .. " " .. item.label .. "|r"
                    local altBoss = ERB.DataBridge:GetBossName(item.itemID)
                    if altBoss then
                        nameStr = nameStr .. " |cff444444(" .. altBoss .. ")|r"
                    end
                end

                tooltip:AddDoubleLine(
                    prefix .. nameStr,
                    rankTag .. " " .. status
                )
            end
        end
    end

    if anyTier or anyToken or anyShared then
        local legend = "|cffa335ee[H]|r |cff888888Heroic, higher ilvl|r   |cff808080[N]|r |cff888888Normal|r"
        if anyToken then
            legend = legend .. "   |cffffd100[T]|r |cff888888Tier token|r"
        end
        if anyShared then
            legend = legend .. "   |cff33d6ff[SL]|r |cff888888Shared loot|r"
        end
        tooltip:AddLine(legend)
    end

    tooltip:Show()
end

function BT:AddNoBiSMessage(tooltip, npcID)
    if not tooltip or not npcID then return end
    local info = ERB:GetPlayerInfo()
    local classColor = info.classColor or "|cffffffff"
    local specName = ERB.DataBridge.playerSpecName or info.specName or "?"
    tooltip:AddLine(" ")
    tooltip:AddLine(
        HEADER_COLOR .. "BiS: |r" .. classColor .. (info.className or "?") .. "|r " .. specName ..
        " — |cff888888No BiS loot from this boss|r"
    )
    tooltip:Show()
end

-- ============================================================================
-- BOSS DETECTION
-- ============================================================================

function BT:ShouldSuppress()
    if UnitAffectingCombat("player") then return true end
    if UnitIsDeadOrGhost("player") then return true end
    return false
end

function BT:IsBossNPC(npcID)
    if ETB then
        if ETB._BossLootLoader then
            local inCombat = (InCombatLockdown and InCombatLockdown())
                or (UnitAffectingCombat and UnitAffectingCombat("player"))
            if inCombat or (ETB.IsLowMemoryMode and ETB:IsLowMemoryMode()) then return false end
            ETB._BossLootLoader(); ETB._BossLootLoader = nil
        end
        if ETB.JNpcToEnc and ETB.JNpcToEnc[npcID] then
            return true
        end
    end
    local classification = UnitClassification("mouseover")
    return classification == "worldboss"
end

-- ============================================================================
-- EVENT HANDLING
-- ============================================================================

function BT:OnMouseoverUnit()
    if not self.enabled then return end
    if ETB.IsLowMemoryMode and ETB:IsLowMemoryMode() then return end
    if self:ShouldSuppress() then return end
    if not ERB.DataBridge or not ERB.BiSData then return end

    local guid = UnitGUID("mouseover")
    if not guid then return end

    local npcID = ERB:GetNPCIDFromGUID(guid)
    if not npcID then return end

    if npcID == self.lastNPCID then return end
    self.lastNPCID = npcID
    self._lastNPCIsBoss = false

    local bisItems = ERB.DataBridge:GetBiSForNPC(npcID)
    if bisItems and next(bisItems) then
        self._lastNPCIsBoss = true
        self:AddBiSToTooltip(GameTooltip, npcID)
    elseif self:IsBossNPC(npcID) then
        self._lastNPCIsBoss = true
        self:AddNoBiSMessage(GameTooltip, npcID)
    end
end

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

function BT:Initialize()
    if self._initialized then return end

    local frame = CreateFrame("Frame")
    frame:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
    frame:RegisterEvent("PLAYER_REGEN_DISABLED")
    frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")

    frame:SetScript("OnEvent", function(_, event, arg1)
        if event == "UPDATE_MOUSEOVER_UNIT" then
            local guid = UnitGUID("mouseover")
            local currentNPC = guid and ERB:GetNPCIDFromGUID(guid)
            if currentNPC ~= BT.lastNPCID then
                BT.lastNPCID = nil
                BT._lastNPCIsBoss = false
                BT:OnMouseoverUnit()
            end
        elseif event == "PLAYER_REGEN_DISABLED" then
            BT.lastNPCID = nil
            BT._lastNPCIsBoss = false
        elseif event == "GET_ITEM_INFO_RECEIVED" then
            local itemID = arg1
            if itemID and BT._pendingItems and BT._pendingItems[itemID] then
                BT._pendingItems[itemID] = nil
                if not BT._refreshThrottle then
                    BT._refreshThrottle = true
                    C_Timer.After(0.3, function()
                        BT._refreshThrottle = false
                        if UnitExists("mouseover") and GameTooltip:IsShown() and BT.lastNPCID then
                            BT:AddBiSToTooltip(GameTooltip, BT.lastNPCID)
                        end
                    end)
                end
            end
        end
    end)

    self._initialized = true

    -- Persist through Blizzard's internal tooltip refreshes
    GameTooltip:HookScript("OnTooltipSetUnit", function(tooltip)
        if not BT.lastNPCID or BT:ShouldSuppress() then return end
        if not ERB.DataBridge or not ERB.BiSData then return end

        local bisItems = ERB.DataBridge:GetBiSForNPC(BT.lastNPCID)
        if bisItems and next(bisItems) then
            BT:AddBiSToTooltip(tooltip, BT.lastNPCID)
        elseif BT._lastNPCIsBoss then
            BT:AddNoBiSMessage(tooltip, BT.lastNPCID)
        end
    end)
end
