local _, ns = ...
local MythicTools = ns.MythicTools

local LFG_APPLICANT_HISTORY_BADGE = "Interface\\AddOns\\SkyMythicHistory\\Media\\smh.tga"
local LFG_ROLE_ICON_TEXTURE = "Interface\\LFGFrame\\UI-LFG-ICON-ROLES"

function MythicTools:ResolveTooltipHistoryPlayerName(playerName)
    if not (self.db and self.db.playersIndex) then
        return nil
    end

    if self:IsPlaceholderName(playerName) then
        return nil
    end

    local fullName = self:NormalizePlayerName(playerName) or self:TrimText(playerName)
    if not fullName or fullName == "" then
        return nil
    end
    if self.db.playersIndex[fullName] then
        return fullName
    end

    -- When the caller supplied an explicit realm (e.g. an LFG applicant
    -- "Name-Realm"), only an exact match counts. Falling back to short-name
    -- matching here would link a same-named player on a DIFFERENT realm, which
    -- caused history badges/tooltips to show for people you never grouped with.
    local hadExplicitRealm = type(playerName) == "string" and playerName:find("%-") ~= nil
    if hadExplicitRealm then
        return nil
    end

    local shortName = self:GetShortName(fullName)
    if shortName == "" then
        return nil
    end

    for indexedName in pairs(self.db.playersIndex) do
        if self:GetShortName(indexedName) == shortName then
            return indexedName
        end
    end

    return nil
end

function MythicTools:GetPlayerTooltipSummary(playerName)
    if self.db and self.db.settings and self.db.settings.showPlayerHistoryTooltips == false then
        return nil
    end

    local settings = self.db and self.db.settings or {}
    if settings.tooltipShowHeader == false
        and settings.tooltipShowRuns == false
        and settings.tooltipShowRating == false
        and settings.tooltipShowNotes == false then
        return nil
    end

    local fullName = self:ResolveTooltipHistoryPlayerName(playerName)
    if not fullName or fullName == "" then
        return nil
    end

    local playerEntry = self.db and self.db.playersIndex and self.db.playersIndex[fullName] or nil
    local totalRuns = tonumber(playerEntry and playerEntry.totalRuns) or 0
    local timedRuns = tonumber(playerEntry and playerEntry.timedRuns) or 0

    if totalRuns == 0 then
        for _, run in ipairs(self:GetRunsForPlayer(fullName) or {}) do
            totalRuns = totalRuns + 1
            if self:GetRunResult(run) == "timed" then
                timedRuns = timedRuns + 1
            end
        end
    end

    local noteText = self:GetPlayerTooltipNote(fullName)
    if not noteText or noteText == "" then
        noteText = nil
    end
    local averageRating = tonumber(playerEntry and playerEntry.averageRating) or 0
    local ratingCount = tonumber(playerEntry and playerEntry.ratingCount) or 0

    return {
        fullName = fullName,
        timedRuns = timedRuns,
        totalRuns = totalRuns,
        noteText = noteText,
        averageRating = averageRating,
        ratingCount = ratingCount,
    }
end

function MythicTools:GetPlayerTooltipNote(fullName)
    local noteText = self:GetPlayerNote(fullName)
    if noteText ~= "" then
        return noteText
    end

    if not (self.db and self.db.playerNotes) then
        return ""
    end

    local shortName = self:GetShortName(fullName)
    if shortName == "" then
        return ""
    end

    for notePlayer, note in pairs(self.db.playerNotes) do
        if type(note) == "string" and note ~= "" and self:GetShortName(notePlayer) == shortName then
            return note
        end
    end

    return ""
end

function MythicTools:AddPlayerHistoryTooltipLines(tooltip, summary)
    local settings = self.db and self.db.settings or {}
    local existingLines = 0
    if tooltip and tooltip.NumLines then
        existingLines = tooltip:NumLines() or 0
    end
    if existingLines > 0 then
        tooltip:AddLine(" ")
    end

    if settings.tooltipShowHeader ~= false then
        tooltip:AddLine(self:L("ADDON_TITLE"), 1, 0.82, 0)
    end
    if settings.tooltipShowRuns ~= false then
        tooltip:AddLine(self:L("PLAYER_HISTORY_TIMED", summary.timedRuns, summary.totalRuns), 1, 1, 1)
    end
    if settings.tooltipShowRating ~= false and (summary.ratingCount or 0) > 0 then
        tooltip:AddLine(("Rating: %.1f/5 (%d)"):format(summary.averageRating or 0, summary.ratingCount or 0), 1, 0.82, 0)
    end
    if settings.tooltipShowNotes ~= false and summary.noteText then
        tooltip:AddLine(self:L("PLAYER_HISTORY_NOTE", summary.noteText), 1, 1, 1, true)
    end
end

function MythicTools:ShowPlayerSummaryTooltip(owner, playerName, anchor)
    local summary = self:GetPlayerTooltipSummary(playerName)
    if not summary then
        return
    end

    GameTooltip:SetOwner(owner, anchor or "ANCHOR_TOPLEFT")
    GameTooltip:ClearLines()
    self:AddPlayerHistoryTooltipLines(GameTooltip, summary)
    GameTooltip._smhHistoryTooltipAdded = true
    GameTooltip:Show()
end

function MythicTools:SetTooltipOwnerSafely(tooltip, owner, anchor, xOffset, yOffset)
    if not (tooltip and owner and tooltip.SetOwner) then
        return false, false, false
    end

    local existingOwner = tooltip.GetOwner and tooltip:GetOwner() or nil
    if existingOwner == owner then
        return false, true, true
    end

    tooltip:SetOwner(owner, anchor or "ANCHOR_RIGHT", xOffset or 0, yOffset or 0)
    return true, existingOwner ~= nil, existingOwner == owner
end

function MythicTools:ShowPlayerContextMenu(playerName)
    playerName = self:NormalizePlayerName(playerName) or self:TrimText(playerName)
    if not playerName or playerName == "" then
        return
    end

    local shortName = self:GetShortName(playerName)
    local menu = {
        {
            text = shortName,
            isTitle = true,
            notCheckable = true,
        },
        {
            text = self:L("PLAYER_CONTEXT_VIEW_HISTORY"),
            notCheckable = true,
            func = function()
                MythicTools:OpenPlayerHistory(playerName)
            end,
        },
        {
            text = self:L("PLAYER_CONTEXT_EDIT_NOTE"),
            notCheckable = true,
            func = function()
                MythicTools:EditPlayerNote(playerName)
            end,
        },
        {
            text = self:L("PLAYER_CONTEXT_COPY_RAIDERIO"),
            notCheckable = true,
            func = function()
                MythicTools:ShowRaiderIOCopyPopup(playerName)
            end,
        },
    }

    if self.ShowCustomMenu then
        self:ShowCustomMenu(menu, {
            anchor = "cursor",
            width = 220,
            x = 6,
            y = -6,
        })
    end
end

function MythicTools:SaveSelectedPlayerNote()
    local playerName = self.db and self.db.ui and self.db.ui.selectedPlayer
    if not playerName then
        return
    end

    self:ShowPlayerNoteEditor(playerName)
end

function MythicTools:GetPlayerNameFromMenuContext(contextData)
    if not self:CanAccessTable(contextData) then
        return nil
    end

    local lfgPlayerName = self:GetPlayerNameFromLFGMenuContext(contextData)
    if lfgPlayerName then
        return lfgPlayerName
    end

    local unit = contextData.unit
    if self:CanAccessValue(unit) and type(unit) == "string" and unit ~= "" then
        local ok, fullName = pcall(function()
            if UnitExists(unit) then
                return self:GetUnitFullName(unit)
            end
        end)
        if ok and fullName then
            return fullName
        end
    end

    local accountInfo = contextData.accountInfo
    if self:CanAccessTable(accountInfo) and self:CanAccessTable(accountInfo.gameAccountInfo) then
        local gameAccountInfo = accountInfo.gameAccountInfo
        return self:NormalizePlayerName(gameAccountInfo.characterName, gameAccountInfo.realmName)
    end

    local name = contextData.name
    if self:CanAccessValue(name) and type(name) == "string" and name ~= "" then
        return self:NormalizePlayerName(name, contextData.server)
    end

    return nil
end

function MythicTools:GetPlayerNameFromLFGMenuContext(contextData)
    if not self:CanAccessTable(contextData) then
        return nil
    end

    local function resolveFromObject(object)
        if type(object) ~= "table" then
            return nil
        end

        local directName = self:NormalizeTooltipPlayerName(
            object.name or object.memberName or object.playerName or object.fullName or object.characterName,
            object.realmName or object.realm or object.server
        )
        if directName then
            return directName
        end

        local resultID = object.resultID or object.searchResultID or object.searchResultId
        local memberIndex = object.memberIdx or object.memberIndex or object.memberID or object.memberId or object.index
        if C_LFGList and C_LFGList.GetSearchResultPlayerInfo and resultID and memberIndex then
            local ok, infoOrName, realmName = pcall(C_LFGList.GetSearchResultPlayerInfo, resultID, memberIndex)
            if ok then
                if type(infoOrName) == "table" then
                    directName = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
                else
                    directName = self:NormalizeTooltipPlayerName(infoOrName, realmName)
                end
                if directName then
                    return directName
                end
            end
        end

        local applicantID = object.applicantID or object.applicantId or object.id
        if C_LFGList and C_LFGList.GetApplicantMemberInfo and applicantID and memberIndex then
            local ok, infoOrName, realmName = pcall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIndex)
            if ok then
                if type(infoOrName) == "table" then
                    directName = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
                else
                    directName = self:NormalizeTooltipPlayerName(infoOrName, realmName)
                end
                if directName then
                    return directName
                end
            end
        end

        return nil
    end

    local candidates = {
        contextData,
        contextData.data,
        contextData.elementData,
        contextData.memberInfo,
        contextData.applicantInfo,
        contextData.applicantMemberInfo,
        contextData.playerInfo,
    }

    for _, candidate in ipairs(candidates) do
        local playerName = resolveFromObject(candidate)
        if playerName then
            return playerName
        end
    end

    return nil
end

function MythicTools:GetUnitMenuSummaryText(playerName)
    local playerEntry = self.db and self.db.playersIndex and self.db.playersIndex[playerName]
    if not playerEntry then
        return nil
    end

    return self:L("PLAYER_HISTORY_TIMED", playerEntry.timedRuns or 0, playerEntry.totalRuns or 0)
end

function MythicTools:TooltipAlreadyHasHistoryLine(tooltip)
    return tooltip and tooltip._smhHistoryTooltipAdded and true or false
end

function MythicTools:NormalizeTooltipPlayerName(name, realm)
    if not self:CanAccessValue(name) or type(name) ~= "string" or name == "" then
        return nil
    end

    return self:NormalizePlayerName(name, realm)
end

function MythicTools:GetPlayerNameFromLFGTooltipOwner(tooltip)
    local owner = tooltip and tooltip.GetOwner and tooltip:GetOwner() or nil
    if type(owner) ~= "table" then
        return nil
    end

    local directName = self:NormalizeTooltipPlayerName(owner.name or owner.memberName or owner.playerName or owner.fullName, owner.realmName or owner.realm)
    if directName then
        return directName
    end

    local resultID = owner.resultID or owner.searchResultID or owner.id
    local memberIndex = owner.memberIdx or owner.memberIndex or owner.memberID or owner.index
    if C_LFGList and C_LFGList.GetSearchResultPlayerInfo and resultID and memberIndex then
        local ok, infoOrName, realmName = pcall(C_LFGList.GetSearchResultPlayerInfo, resultID, memberIndex)
        if ok then
            if type(infoOrName) == "table" then
                local name = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
                if name then
                    return name
                end
            else
                local name = self:NormalizeTooltipPlayerName(infoOrName, realmName)
                if name then
                    return name
                end
            end
        end
    end

    local applicantID = owner.applicantID or owner.applicantId or owner.id
    if C_LFGList and C_LFGList.GetApplicantMemberInfo and applicantID and memberIndex then
        local ok, infoOrName, realmName = pcall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIndex)
        if ok then
            if type(infoOrName) == "table" then
                local name = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
                if name then
                    return name
                end
            else
                local name = self:NormalizeTooltipPlayerName(infoOrName, realmName)
                if name then
                    return name
                end
            end
        end
    end

    return nil
end

function MythicTools:GetPlayerNameFromFriendObject(object)
    if type(object) ~= "table" then
        return nil
    end

    local data = type(object.elementData) == "table" and object.elementData or object
    local directName = self:NormalizeTooltipPlayerName(data.name or data.characterName or data.playerName or data.fullName, data.realmName or data.realm)
    if directName then
        return directName
    end

    local gameAccountInfo = data.gameAccountInfo
    if type(gameAccountInfo) == "table" and gameAccountInfo.characterName then
        return self:NormalizeTooltipPlayerName(gameAccountInfo.characterName, gameAccountInfo.realmName)
    end

    local buttonType = data.buttonType or data.type
    local buttonID = data.id or data.index or data.friendIndex
    local isWowFriend = buttonType == "FRIEND" or buttonType == "WOW" or buttonType == FRIENDS_BUTTON_TYPE_WOW or buttonType == 1
    local isBattleNetFriend = buttonType == "BN_FRIEND" or buttonType == "BNET" or buttonType == FRIENDS_BUTTON_TYPE_BNET or buttonType == 2
    if isWowFriend and buttonID then
        if C_FriendList and C_FriendList.GetFriendInfoByIndex then
            local ok, info = pcall(C_FriendList.GetFriendInfoByIndex, buttonID)
            if ok and type(info) == "table" and info.name then
                local name = self:NormalizeTooltipPlayerName(info.name)
                if name then
                    return name
                end
            end
        end
        if GetFriendInfo then
            local ok, name = pcall(GetFriendInfo, buttonID)
            if ok then
                local normalized = self:NormalizeTooltipPlayerName(name)
                if normalized then
                    return normalized
                end
            end
        end
    end

    if isBattleNetFriend and buttonID then
        if C_BattleNet and C_BattleNet.GetFriendAccountInfo then
            local ok, accountInfo = pcall(C_BattleNet.GetFriendAccountInfo, buttonID)
            local info = ok and accountInfo and accountInfo.gameAccountInfo
            if type(info) == "table" and info.characterName and (not BNET_CLIENT_WOW or info.clientProgram == BNET_CLIENT_WOW or info.clientProgram == "WoW") then
                local name = self:NormalizeTooltipPlayerName(info.characterName, info.realmName)
                if name then
                    return name
                end
            end
        end
        if BNGetFriendInfo then
            local ok, _, _, _, _, _, client, _, _, _, _, _, _, _, _, _, gameAccountInfo = pcall(BNGetFriendInfo, buttonID)
            if ok and client == BNET_CLIENT_WOW and type(gameAccountInfo) == "table" and gameAccountInfo.characterName then
                local name = self:NormalizeTooltipPlayerName(gameAccountInfo.characterName, gameAccountInfo.realmName)
                if name then
                    return name
                end
            end
        end
    end

    return nil
end

function MythicTools:GetPlayerNameFromFriendTooltipOwner(tooltip)
    local owner = tooltip and tooltip.GetOwner and tooltip:GetOwner() or nil
    return self:GetPlayerNameFromFriendObject(owner)
end

function MythicTools:GetPlayerNameFromTooltip(tooltip, tooltipData)
    local guid = tooltipData and tooltipData.guid
    if guid and C_PlayerInfo and C_PlayerInfo.GetPlayerInfoByGUID then
        local playerInfo = C_PlayerInfo.GetPlayerInfoByGUID(guid)
        if playerInfo and playerInfo.name then
            return self:NormalizePlayerName(playerInfo.name, playerInfo.realmName)
        end
    end

    local lfgPlayerName = self:GetPlayerNameFromLFGTooltipOwner(tooltip)
    if lfgPlayerName then
        return lfgPlayerName
    end

    local friendPlayerName = self:GetPlayerNameFromFriendTooltipOwner(tooltip)
    if friendPlayerName then
        return friendPlayerName
    end

    if tooltip and tooltip.GetUnit then
        -- Blizzard can expose protected unit tokens on some tooltips.
        -- Resolve them defensively so bad tokens simply produce no result.
        local ok, fullName = pcall(function()
            local _, unit = tooltip:GetUnit()
            if unit and UnitExists(unit) and UnitIsPlayer(unit) then
                return self:GetUnitFullName(unit)
            end
        end)
        if ok and fullName then
            return fullName
        end
    end

    return nil
end

function MythicTools:AddHistoryToBlizzardTooltip(tooltip, tooltipData)
    if not (tooltip and tooltip.AddLine) or self:TooltipAlreadyHasHistoryLine(tooltip) then
        return
    end

    local playerName = self:GetPlayerNameFromTooltip(tooltip, tooltipData)
    local summary = self:GetPlayerTooltipSummary(playerName)
    if not summary then
        return
    end

    self:AddPlayerHistoryTooltipLines(tooltip, summary)
    tooltip._smhHistoryTooltipAdded = true
    tooltip:Show()
end

function MythicTools:AddHistoryToTooltipForPlayer(tooltip, playerName)
    if not (tooltip and tooltip.AddLine) or self:TooltipAlreadyHasHistoryLine(tooltip) then
        return false
    end

    local summary = self:GetPlayerTooltipSummary(playerName)
    if not summary then
        return false
    end

    self:AddPlayerHistoryTooltipLines(tooltip, summary)
    tooltip._smhHistoryTooltipAdded = true
    tooltip:Show()
    return true
end

function MythicTools:AddHistoryToLFGSearchTooltip(tooltip, resultID)
    if not (C_LFGList and C_LFGList.GetSearchResultInfo and resultID) then
        return
    end

    local searchResultInfo = C_LFGList.GetSearchResultInfo(resultID)
    local leaderName = searchResultInfo and searchResultInfo.leaderName
    if not self:CanAccessValue(leaderName) or type(leaderName) ~= "string" or leaderName == "" then
        return
    end

    tooltip = tooltip or GameTooltip
    if C_Timer then
        C_Timer.After(0, function()
            MythicTools:AddHistoryToTooltipForPlayer(tooltip, leaderName)
        end)
    else
        self:AddHistoryToTooltipForPlayer(tooltip, leaderName)
    end
end

function MythicTools:GetLFGApplicantMemberContext(button)
    if not (button and C_LFGList and C_LFGList.GetApplicantMemberInfo) then
        return nil
    end

    local parent = button
    local applicantID = button.applicantID
    while not applicantID and parent and parent.GetParent do
        parent = parent:GetParent()
        applicantID = parent and parent.applicantID
    end

    local memberIndex = button.memberIdx or button.memberIndex
    if not memberIndex and parent and parent.Members then
        for index, memberButton in pairs(parent.Members) do
            if memberButton == button then
                memberIndex = index
                break
            end
        end
    end
    if not (applicantID and memberIndex) then
        return nil
    end

    local ok, infoOrName = pcall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIndex)
    if not ok then
        return nil
    end

    local fullName
    if type(infoOrName) == "table" then
        fullName = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
    else
        fullName = self:NormalizeTooltipPlayerName(infoOrName)
    end

    if not fullName then
        return nil
    end

    return {
        applicantID = applicantID,
        memberIndex = memberIndex,
        fullName = fullName,
    }
end

function MythicTools:GetKnownLFGApplicantPlayer(button)
    local context = self:GetLFGApplicantMemberContext(button)
    if not context then
        return nil
    end

    return self:ResolveTooltipHistoryPlayerName(context.fullName)
end

-- Resolve a known player directly from applicant/member IDs (used by the
-- Blizzard update hook), bypassing button parent-walking which is unreliable on
-- recycled scroll frames.
function MythicTools:GetKnownLFGApplicantPlayerByIDs(applicantID, memberIdx)
    if not (applicantID and memberIdx and C_LFGList and C_LFGList.GetApplicantMemberInfo) then
        return nil
    end

    local ok, infoOrName = pcall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIdx)
    if not ok then
        return nil
    end

    local fullName
    if type(infoOrName) == "table" then
        fullName = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
    else
        fullName = self:NormalizeTooltipPlayerName(infoOrName)
    end

    if not fullName then
        return nil
    end

    return self:ResolveTooltipHistoryPlayerName(fullName)
end

function MythicTools:AddHistoryToLFGApplicantTooltip(button)
    local fullName = self:GetKnownLFGApplicantPlayer(button)
    if not fullName then
        return
    end

    if GameTooltip and not GameTooltip:GetOwner() then
        GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
    end
    self:AddHistoryToTooltipForPlayer(GameTooltip, fullName)
end

function MythicTools:EnsureLFGApplicantHistoryBadge(button)
    if not button or button._smhHistoryBadge then
        return button and button._smhHistoryBadge or nil
    end

    local badge = button:CreateTexture(nil, "OVERLAY")
    badge:SetTexture(LFG_APPLICANT_HISTORY_BADGE)
    badge:SetBlendMode("BLEND")
    badge:SetTexCoord(0, 1, 0, 1)
    badge:SetSize(15, 15)
    badge:Hide()
    button._smhHistoryBadge = badge
    return badge
end

function MythicTools:GetLFGApplicantRoleRegion(button)
    if type(button) ~= "table" then
        return nil
    end

    local namedRegion = button.RoleIcon or button.roleIcon or button.Role or button.role
    if namedRegion and namedRegion.GetObjectType then
        return namedRegion
    end

    if button.GetName then
        local name = button:GetName()
        if type(name) == "string" and name ~= "" then
            for _, suffix in ipairs({"RoleIcon", "Role", "RoleTexture"}) do
                local region = _G[name .. suffix]
                if region and region.GetObjectType then
                    return region
                end
            end
        end
    end

    if button.GetRegions then
        local regions = {button:GetRegions()}
        for _, region in ipairs(regions) do
            if region and region.GetObjectType and region:GetObjectType() == "Texture" and region.GetTexture then
                local texture = region:GetTexture()
                local atlas = region.GetAtlas and region:GetAtlas() or nil
                if texture == LFG_ROLE_ICON_TEXTURE
                    or tostring(texture or ""):find("UI%-LFG%-ICON%-ROLES")
                    or tostring(atlas or ""):find("roleicon") then
                    return region
                end
            end
        end
    end

    return nil
end

function MythicTools:PositionLFGApplicantHistoryBadge(button, badge)
    if not (button and badge) then
        return
    end

    badge:ClearAllPoints()
    local roleRegion = self:GetLFGApplicantRoleRegion(button)
    if roleRegion then
        badge:SetPoint("LEFT", roleRegion, "RIGHT", 7, 0)
    else
        badge:SetPoint("LEFT", button, "LEFT", 124, 0)
    end
end

function MythicTools:UpdateLFGApplicantHistoryBadge(button, applicantID, memberIdx)
    if not button then
        return
    end

    local badge = self:EnsureLFGApplicantHistoryBadge(button)
    if not badge then
        return
    end

    if not (self.db and self.db.settings and self.db.settings.showLFGApplicantHistoryBadge ~= false) then
        badge.playerName = nil
        badge:Hide()
        return
    end

    -- Prefer the explicit IDs passed by the Blizzard update hook; only fall back
    -- to button inspection when they are absent. This keeps the badge in sync as
    -- scroll frames are recycled for different applicants.
    local fullName
    if applicantID and memberIdx then
        fullName = self:GetKnownLFGApplicantPlayerByIDs(applicantID, memberIdx)
    else
        fullName = self:GetKnownLFGApplicantPlayer(button)
    end
    if not fullName then
        badge.playerName = nil
        badge:Hide()
        return
    end

    self:PositionLFGApplicantHistoryBadge(button, badge)
    badge.playerName = fullName
    badge:Show()
end

function MythicTools:RefreshVisibleLFGApplicantHistoryBadges()
    if not (LFGListFrame and LFGListFrame.ApplicationViewer and LFGListFrame.ApplicationViewer.ScrollBox) then
        return
    end

    self:HookVisibleLFGTooltipButtons(LFGListFrame.ApplicationViewer.ScrollBox)
end

function MythicTools:HookLFGTooltipButton(button)
    if not (button and button.HookScript) then
        return
    end

    if button.Members then
        for _, memberButton in pairs(button.Members) do
            self:HookLFGTooltipButton(memberButton)
        end
    end

    self.lfgTooltipHookedButtons = self.lfgTooltipHookedButtons or {}
    if self.lfgTooltipHookedButtons[button] then
        self:UpdateLFGApplicantHistoryBadge(button)
        return
    end

    self.lfgTooltipHookedButtons[button] = true
    if button.RegisterForClicks then
        pcall(button.RegisterForClicks, button, "LeftButtonUp", "RightButtonUp")
    end
    self:UpdateLFGApplicantHistoryBadge(button)
    button:HookScript("OnEnter", function(frame)
        if frame.applicantID and frame.Members then
            for _, memberButton in pairs(frame.Members) do
                MythicTools:HookLFGTooltipButton(memberButton)
            end
            return
        end

        C_Timer.After(0, function()
            MythicTools:AddHistoryToLFGApplicantTooltip(frame)
        end)
    end)
    button:HookScript("OnShow", function(frame)
        if frame.applicantID and frame.Members then
            for _, memberButton in pairs(frame.Members) do
                MythicTools:HookLFGTooltipButton(memberButton)
                MythicTools:UpdateLFGApplicantHistoryBadge(memberButton)
            end
            return
        end

        MythicTools:UpdateLFGApplicantHistoryBadge(frame)
    end)
    button:HookScript("OnMouseUp", function(frame, mouseButton)
        if mouseButton ~= "RightButton" then
            return
        end

        if GameTooltip then
            GameTooltip._smhHistoryTooltipAdded = nil
            GameTooltip:Hide()
        end
    end)
end

function MythicTools:HookLFGTooltipButtons(buttons)
    for _, button in pairs(buttons or {}) do
        self:HookLFGTooltipButton(button)
        self:UpdateLFGApplicantHistoryBadge(button)
    end
end

function MythicTools:HookVisibleLFGTooltipButtons(scrollBox)
    if not scrollBox then
        return
    end

    if scrollBox.GetFrames then
        local ok, frames = pcall(scrollBox.GetFrames, scrollBox)
        if ok and type(frames) == "table" then
            self:HookLFGTooltipButtons(frames)
        end
    end

    if scrollBox.ForEachFrame then
        pcall(scrollBox.ForEachFrame, scrollBox, function(button)
            MythicTools:HookLFGTooltipButton(button)
        end)
    end
end

function MythicTools:AddHistoryToFriendTooltip(button)
    local fullName = self:GetPlayerNameFromFriendObject(button)
    if not fullName then
        return
    end

    if FriendsTooltip and FriendsTooltip:IsShown() and GameTooltip then
        local xOffset = FriendsTooltip.GetWidth and -FriendsTooltip:GetWidth() or 0
        local ownerSet, ownerExisted, ownerSetSame = self:SetTooltipOwnerSafely(GameTooltip, FriendsTooltip, "ANCHOR_BOTTOMRIGHT", xOffset, -4)
        if self:AddHistoryToTooltipForPlayer(GameTooltip, fullName) then
            return
        end
        if ownerSet and not ownerExisted then
            GameTooltip:Hide()
        end
        return
    end

    self:SetTooltipOwnerSafely(GameTooltip, button, "ANCHOR_RIGHT", 0, 0)
    self:AddHistoryToTooltipForPlayer(GameTooltip, fullName)
end

function MythicTools:AddHistoryToFriendsTooltip()
    local button = FriendsTooltip and FriendsTooltip.button
    if button then
        self:AddHistoryToFriendTooltip(button)
    end
end

function MythicTools:HideFriendHistoryTooltip()
    if GameTooltip then
        GameTooltip:Hide()
    end
end

function MythicTools:HookFriendTooltipButton(button)
    if not (button and button.HookScript) or (self.friendTooltipHookedButtons and self.friendTooltipHookedButtons[button]) then
        return
    end

    self.friendTooltipHookedButtons = self.friendTooltipHookedButtons or {}
    self.friendTooltipHookedButtons[button] = true
    button:HookScript("OnEnter", function(frame)
        C_Timer.After(0, function()
            MythicTools:AddHistoryToFriendTooltip(frame)
        end)
    end)
end

function MythicTools:HookVisibleFriendTooltipButtons()
    if FriendsFrameFriendsScrollFrame and FriendsFrameFriendsScrollFrame.buttons then
        for _, button in pairs(FriendsFrameFriendsScrollFrame.buttons) do
            self:HookFriendTooltipButton(button)
        end
    end

    if FriendsListFrame and FriendsListFrame.ScrollBox then
        if FriendsListFrame.ScrollBox.GetFrames then
            local ok, frames = pcall(FriendsListFrame.ScrollBox.GetFrames, FriendsListFrame.ScrollBox)
            if ok and type(frames) == "table" then
                for _, button in pairs(frames) do
                    self:HookFriendTooltipButton(button)
                end
            end
        end
        if FriendsListFrame.ScrollBox.ForEachFrame then
            pcall(FriendsListFrame.ScrollBox.ForEachFrame, FriendsListFrame.ScrollBox, function(button)
                MythicTools:HookFriendTooltipButton(button)
            end)
        end
    end
end

function MythicTools:InitializeFriendTooltips()
    if self.friendTooltipsInitialized then
        return
    end

    if not FriendsFrame and C_AddOns and C_AddOns.LoadAddOn then
        pcall(C_AddOns.LoadAddOn, "Blizzard_Friends")
    end

    if not FriendsFrame then
        self.friendTooltipHookAttempts = (self.friendTooltipHookAttempts or 0) + 1
        if self.friendTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeFriendTooltips()
            end)
        end
        return
    end

    if not FriendsTooltip then
        self.friendTooltipHookAttempts = (self.friendTooltipHookAttempts or 0) + 1
        if self.friendTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeFriendTooltips()
            end)
        end
        return
    end

    self:HookVisibleFriendTooltipButtons()
    if FriendsFrame.HookScript then
        FriendsFrame:HookScript("OnShow", function()
            MythicTools:HookVisibleFriendTooltipButtons()
        end)
    end
    if FriendsFrameFriendsScrollFrame and FriendsFrameFriendsScrollFrame.HookScript then
        FriendsFrameFriendsScrollFrame:HookScript("OnShow", function()
            MythicTools:HookVisibleFriendTooltipButtons()
        end)
    end
    if hooksecurefunc and not self.friendTooltipShowHooked then
        hooksecurefunc(FriendsTooltip, "Show", function()
            if C_Timer then
                C_Timer.After(0, function()
                    MythicTools:AddHistoryToFriendsTooltip()
                end)
            else
                MythicTools:AddHistoryToFriendsTooltip()
            end
        end)
        hooksecurefunc(FriendsTooltip, "Hide", function()
            MythicTools:HideFriendHistoryTooltip()
        end)
        self.friendTooltipShowHooked = true
    end
    self.friendTooltipsInitialized = true
end

function MythicTools:InitializeLFGTooltips()
    if self.lfgTooltipsInitialized then
        return
    end

    if not self.lfgTooltipWatcher and CreateFrame then
        self.lfgTooltipWatcher = CreateFrame("Frame")
        self.lfgTooltipWatcher:SetScript("OnEvent", function()
            MythicTools:InitializeLFGTooltips()
        end)
        self.lfgTooltipWatcher:RegisterEvent("ADDON_LOADED")
        self.lfgTooltipWatcher:RegisterEvent("LFG_LIST_SEARCH_RESULTS_RECEIVED")
        self.lfgTooltipWatcher:RegisterEvent("LFG_LIST_APPLICANT_LIST_UPDATED")
    end

    if not LFGListFrame and C_AddOns and C_AddOns.LoadAddOn then
        pcall(C_AddOns.LoadAddOn, "Blizzard_GroupFinder")
    end

    if not (LFGListFrame and LFGListFrame.SearchPanel and LFGListFrame.ApplicationViewer) then
        self.lfgTooltipHookAttempts = (self.lfgTooltipHookAttempts or 0) + 1
        if self.lfgTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeLFGTooltips()
            end)
        end
        return
    end

    if not LFGListUtil_SetSearchEntryTooltip then
        self.lfgTooltipHookAttempts = (self.lfgTooltipHookAttempts or 0) + 1
        if self.lfgTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeLFGTooltips()
            end)
        end
        return
    end

    if hooksecurefunc and not self.lfgSearchTooltipHooked then
        hooksecurefunc("LFGListUtil_SetSearchEntryTooltip", function(tooltip, resultID)
            MythicTools:AddHistoryToLFGSearchTooltip(tooltip, resultID)
        end)
        self.lfgSearchTooltipHooked = true
    end

    -- Refresh the badge whenever Blizzard repopulates an applicant member row.
    -- This is the authoritative per-row update and fixes the badge "jumping" to
    -- the wrong applicant as scroll frames are recycled.
    if hooksecurefunc and not self.lfgApplicantMemberHooked
        and type(_G.LFGListApplicationViewer_UpdateApplicantMember) == "function" then
        hooksecurefunc("LFGListApplicationViewer_UpdateApplicantMember", function(member, applicantID, memberIdx)
            MythicTools:UpdateLFGApplicantHistoryBadge(member, applicantID, memberIdx)
        end)
        self.lfgApplicantMemberHooked = true
    end

    if ScrollBoxUtil and ScrollBoxUtil.OnViewFramesChanged then
        ScrollBoxUtil:OnViewFramesChanged(LFGListFrame.SearchPanel.ScrollBox, function(buttons)
            MythicTools:HookLFGTooltipButtons(buttons)
        end)
        ScrollBoxUtil:OnViewFramesChanged(LFGListFrame.ApplicationViewer.ScrollBox, function(buttons)
            MythicTools:HookLFGTooltipButtons(buttons)
        end)
    end
    self:HookVisibleLFGTooltipButtons(LFGListFrame.SearchPanel.ScrollBox)
    self:HookVisibleLFGTooltipButtons(LFGListFrame.ApplicationViewer.ScrollBox)

    if LFGListFrame.SearchPanel.HookScript then
        LFGListFrame.SearchPanel:HookScript("OnShow", function()
            MythicTools:HookVisibleLFGTooltipButtons(LFGListFrame.SearchPanel.ScrollBox)
        end)
    end
    if LFGListFrame.ApplicationViewer.HookScript then
        LFGListFrame.ApplicationViewer:HookScript("OnShow", function()
            MythicTools:HookVisibleLFGTooltipButtons(LFGListFrame.ApplicationViewer.ScrollBox)
        end)
    end
    if not self.lfgApplicantBadgeWatcher and CreateFrame then
        self.lfgApplicantBadgeWatcher = CreateFrame("Frame")
        self.lfgApplicantBadgeWatcher:SetScript("OnEvent", function()
            if C_Timer then
                C_Timer.After(0, function()
                    MythicTools:RefreshVisibleLFGApplicantHistoryBadges()
                end)
            else
                MythicTools:RefreshVisibleLFGApplicantHistoryBadges()
            end
        end)
        self.lfgApplicantBadgeWatcher:RegisterEvent("LFG_LIST_APPLICANT_LIST_UPDATED")
    end
    -- Blizzard_GroupFinder is loaded by this point, so (re)register the LFG
    -- unit-popup menu tags that may have failed at PLAYER_LOGIN.
    if self.RegisterUnitPopupMenuTags then
        self:RegisterUnitPopupMenuTags()
    end

    if self.lfgTooltipWatcher then
        self.lfgTooltipWatcher:UnregisterAllEvents()
    end
    self.lfgTooltipsInitialized = true
end

function MythicTools:InitializeBlizzardUnitTooltip()
    if self.blizzardUnitTooltipInitialized then
        return
    end

    if TooltipDataProcessor and TooltipDataProcessor.AddTooltipPostCall and Enum and Enum.TooltipDataType and Enum.TooltipDataType.Unit then
        TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, function(tooltip, tooltipData)
            MythicTools:AddHistoryToBlizzardTooltip(tooltip, tooltipData)
        end)
    end

    if GameTooltip and GameTooltip.HookScript then
        if GameTooltip.HasScript and GameTooltip:HasScript("OnTooltipSetUnit") then
            GameTooltip:HookScript("OnTooltipSetUnit", function(tooltip)
                MythicTools:AddHistoryToBlizzardTooltip(tooltip)
            end)
        end
        GameTooltip:HookScript("OnShow", function(tooltip)
            MythicTools:AddHistoryToBlizzardTooltip(tooltip)
        end)
        GameTooltip:HookScript("OnTooltipCleared", function(tooltip)
            tooltip._smhHistoryTooltipAdded = nil
        end)
        GameTooltip:HookScript("OnHide", function(tooltip)
            tooltip._smhHistoryTooltipAdded = nil
        end)
    else
        return
    end

    self:InitializeLFGTooltips()
    self:InitializeFriendTooltips()
    self.blizzardUnitTooltipInitialized = true
end

function MythicTools:HandleUnitPopupMenu(owner, rootDescription, contextData)
    if self.db and self.db.settings and self.db.settings.showUnitContextMenuEntry == false then
        return
    end

    if rootDescription and rootDescription._smhUnitEntryAdded then
        return
    end

    local contextPlayerName = self:GetPlayerNameFromMenuContext(contextData)
        or self:GetPlayerNameFromLFGMenuContext(owner)
        or self:GetKnownLFGApplicantPlayer(owner)
    local playerName = self:ResolveTooltipHistoryPlayerName(contextPlayerName)

    if self.PushRuntimeDebug and self:IsDebugLoggingEnabled() then
        self:PushRuntimeDebug(("UnitPopupMenu fired: context=%s resolved=%s history=%s"):format(
            tostring(contextPlayerName or "nil"),
            tostring(playerName or "nil"),
            tostring(playerName ~= nil and self:HasPlayerHistory(playerName) or false)))
    end

    if not playerName or not self:HasPlayerHistory(playerName) then
        return
    end

    local summaryText = self:GetUnitMenuSummaryText(playerName)
    if not summaryText then
        return
    end

    if rootDescription then
        rootDescription._smhUnitEntryAdded = true
    end
    rootDescription:CreateDivider()
    rootDescription:CreateTitle(self:L("ADDON_TITLE"))
    rootDescription:CreateButton(summaryText, function()
        if C_Timer then
            C_Timer.After(0, function()
                MythicTools:OpenPlayerHistory(playerName)
            end)
        else
            MythicTools:OpenPlayerHistory(playerName)
        end
    end)
end

function MythicTools:RegisterUnitPopupMenuTag(tag, modifyMenu)
    if type(tag) ~= "string" or tag == "" then
        return
    end

    self.unitPopupRegisteredTags = self.unitPopupRegisteredTags or {}
    if self.unitPopupRegisteredTags[tag] then
        return
    end

    local ok = pcall(modifyMenu, tag, function(owner, rootDescription, contextData)
        MythicTools:HandleUnitPopupMenu(owner, rootDescription, contextData)
    end)
    if ok then
        self.unitPopupRegisteredTags[tag] = true
    end
end

function MythicTools:IsLFGRelatedMenuTag(tag)
    if type(tag) ~= "string" then
        return false
    end

    return tag:find("LFG", 1, true) ~= nil
        or tag:find("GROUP_FINDER", 1, true) ~= nil
        or tag:find("APPLICANT", 1, true) ~= nil
end

function MythicTools:GetUnitPopupMenuTags()
    return {
        "MENU_UNIT_PARTY",
        "MENU_UNIT_PLAYER",
        "MENU_UNIT_RAID",
        "MENU_UNIT_RAID_PLAYER",
        "MENU_UNIT_TARGET",
        "MENU_UNIT_FOCUS",
        "MENU_UNIT_SELF",
        "MENU_UNIT_FRIEND",
        "MENU_UNIT_ENEMY_PLAYER",
        "MENU_UNIT_GUILD",
        "MENU_UNIT_GUILD_OFFLINE",
        -- Real tag used by Blizzard_GroupFinder/LFGList.lua for the context
        -- menu that opens when clicking an applicant member while your group
        -- is listed (LFGListApplicantMember_OnMouseDown -> SetTag).
        "MENU_LFG_FRAME_MEMBER_APPLY",
        "MENU_LFG_APPLICANT",
        "MENU_LFG_APPLICANT_MEMBER",
        "MENU_LFG_LIST_APPLICANT",
        "MENU_LFG_LIST_APPLICANT_MEMBER",
        "MENU_LFG_LIST_APPLICATION",
        "MENU_LFG_LIST_APPLICATION_PLAYER",
        "MENU_LFG_LIST_APPLICATION_VIEWER",
        "MENU_LFG_LIST_SEARCH_ENTRY",
        "MENU_LFG_LIST_SEARCH_RESULT",
        "MENU_LFG_LIST_SEARCH_RESULT_MEMBER",
        "MENU_LFG_LIST_SEARCH_RESULT_PLAYER",
        "MENU_LFG_SEARCH_ENTRY",
        "MENU_LFG_SEARCH_RESULT",
        "MENU_LFG_SEARCH_RESULT_MEMBER",
        "MENU_LFG_SEARCH_RESULT_PLAYER",
    }
end

-- Re-attempt registration of the unit-popup menu tags. Menu.ModifyMenu can fail
-- for LFG menu tags at PLAYER_LOGIN because Blizzard_GroupFinder is not loaded
-- yet; tags that fail are NOT marked registered, so calling this again later
-- (e.g. once the Group Finder UI is ready) completes them. Safe to call repeatedly.
function MythicTools:RegisterUnitPopupMenuTags()
    local modifyMenu = Menu and Menu.ModifyMenu
    if not modifyMenu then
        return
    end

    for _, tag in ipairs(self:GetUnitPopupMenuTags()) do
        self:RegisterUnitPopupMenuTag(tag, modifyMenu)
    end
end

function MythicTools:InitializeUnitPopupMenu()
    if self.unitPopupMenuInitialized then
        return
    end

    local modifyMenu = Menu and Menu.ModifyMenu
    if not modifyMenu then
        return
    end

    self:RegisterUnitPopupMenuTags()

    if hooksecurefunc and Menu and Menu.OpenMenuTag and not self.unitPopupDynamicMenuHooked then
        hooksecurefunc(Menu, "OpenMenuTag", function(tag)
            if MythicTools:IsLFGRelatedMenuTag(tag) then
                MythicTools:RegisterUnitPopupMenuTag(tag, modifyMenu)
            end
        end)
        self.unitPopupDynamicMenuHooked = true
    end

    self.unitPopupMenuInitialized = true
end
