local addonName, addonTable = ...
---@type MK
local MK = addonTable.MK
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end

core.FindGroupTab = core.FindGroupTab or {}
local FindGroupTab = core.FindGroupTab
core.FindGroupList = core.FindGroupList or {}
local FindGroupList = core.FindGroupList
local FindGroupSearch = core.FindGroupSearch or {}
local MeetingStoneConstants = core.MeetingStoneConstants or {}
local MeetingStoneForm = core.MeetingStoneForm or {}

local TrySetAtlas = MeetingStoneForm.TrySetAtlas or function() return false end
local CreateSystemButton = MeetingStoneForm.CreateSystemButton

local ROLE_ATLAS_BY_KEY = core.GROUP_LIST_ROLE_ATLAS_BY_KEY or {
    TANK = "UI-LFG-RoleIcon-Tank",
    HEAL = "UI-LFG-RoleIcon-Healer",
    DPS = "UI-LFG-RoleIcon-DPS",
}

local ROLE_TEXT_KEY_BY_ROLE = {
    TANK = "坦克",
    HEAL = "治疗",
    DPS = "输出",
    UNKNOWN = "未知",
}

local MK_ICON_PATH = core.MK_ICON_PATH or ("Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Icon\\")
local REFRESH_BUTTON_TEXTURE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Refresh.png"
local ROW_TEXTURE_NORMAL = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowNormal.png"
local ROW_TEXTURE_GREEN = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowGreen.png"
local ROW_TEXTURE_RED = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowRed.png"
local ROW_TEXTURE_BLUE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowBlue.png"
local ROW_TEXTURE_GREY = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowGrey.png"
local LEAVER_ICON_TEXTURE = MK_ICON_PATH .. "isLeaver.png"
local NEWBIE_ICON_TEXTURE = MK_ICON_PATH .. "Newbie.png"
local LEADER_ICON_TEXTURE = MK_ICON_PATH .. "Leader.png"
local STAR_ICON_TEXTURE = MK_ICON_PATH .. "Star.png"
local BNET_ICON_TEXTURE = MK_ICON_PATH .. "BattleNet.png"
local APPLICATION_ALERT_ICON_TEXTURE = "Interface\\DialogFrame\\UI-Dialog-Icon-AlertNew.blp"
local BLACKLIST_MENU_ICON_TEXTURE = MK_ICON_PATH .. "Blacklist.png"
local BLACKLIST_MENU_ICON_MARKUP = "|T" .. BLACKLIST_MENU_ICON_TEXTURE .. ":16:16:0:0|t "

local RIGHT_PANEL_OVERLAP = MeetingStoneConstants.MEETINGSTONE_RIGHT_PANEL_OVERLAP or -4

local HEADER_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_HEADER_TOP_OFFSET or -20
local HEADER_BOTTOM_OFFSET = MeetingStoneConstants.MEETINGSTONE_HEADER_BOTTOM_OFFSET or -46
local HEADER_CONTENT_INSET_X = MeetingStoneConstants.MEETINGSTONE_HEADER_CONTENT_INSET_X or 4
local HEADER_CONTENT_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_HEADER_CONTENT_OFFSET_Y or 4

local TOP_RIGHT_FILIGREE_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_X or 0
local TOP_RIGHT_FILIGREE_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_Y or -3
local BOTTOM_RIGHT_FILIGREE_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_X or 0
local BOTTOM_RIGHT_FILIGREE_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_Y or 0
local APPLICANT_SCROLLBAR_WIDTH = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLLBAR_WIDTH or 17
local APPLICANT_SCROLLBAR_GAP = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLLBAR_GAP or 2
local APPLICANT_SCROLLBAR_RIGHT_INSET = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLLBAR_RIGHT_INSET or 10
local APPLICANT_SCROLL_BOTTOM_INSET = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLL_BOTTOM_INSET or 2

local HEADER_HEIGHT = math.abs(HEADER_BOTTOM_OFFSET - HEADER_TOP_OFFSET)
local ROW_HEIGHT = 36
local FIND_GROUP_VISIBLE_DETAIL_ROWS = 10

local ROW_SIDE_INSET = 4
local ROW_GAP = 1
local ROLE_ICON_SIZE = core.GROUP_LIST_ROLE_ICON_SIZE or core.GROUP_LIST_STATUS_ICON_SIZE or 18
local TOOLTIP_ICON_SIZE = core.GROUP_LIST_TOOLTIP_ICON_SIZE or ROLE_ICON_SIZE
local TOOLTIP_ICON_TEXT_GAP = core.GROUP_LIST_TOOLTIP_ICON_TEXT_GAP or "  "
local TOOLTIP_GROUP_LEADER_ICON_MARKUP = string.format(
    "|A:%s:%d:%d:0:0|a",
    core.GROUP_LIST_LEADER_ATLAS or "UI-LFG-RoleIcon-Leader",
    TOOLTIP_ICON_SIZE,
    TOOLTIP_ICON_SIZE
)
local TYPE_ICON_SIZE = 16
local TYPE_ICON_LEFT_OFFSET = 10
local TYPE_TEXT_MIN_WIDTH = 54
local TYPE_COLUMN_MIN_WIDTH = TYPE_ICON_LEFT_OFFSET + TYPE_ICON_SIZE + 4 + TYPE_TEXT_MIN_WIDTH + 12
local TITLE_COLUMN_MIN_WIDTH = 86
local HEADER_TEXT_SIZE = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_SIZE or 14
local ApplyHeaderTextStyle = MeetingStoneConstants.ApplyMeetingStoneHeaderTextStyle
local HEADER_ACCENT_WIDTH = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_WIDTH or 1.5
local HEADER_ACCENT_HEIGHT = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_HEIGHT or 22
local HEADER_ACCENT_COLOR = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_COLOR or { 126 / 255, 112 / 255, 82 / 255 }
local HEADER_ACCENT_ALPHA = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_ALPHA or 0.6
local GetHeaderAccentPixelWidth = MeetingStoneConstants.GetMeetingStoneHeaderAccentPixelWidth or function()
    return HEADER_ACCENT_WIDTH
end
local SnapHeaderAccentOffset = MeetingStoneConstants.SnapMeetingStoneHeaderAccentOffset or function(_, value)
    return math.floor((tonumber(value) or 0) + 0.5)
end
local COMMENT_TEXT_COLOR = { 1, 1, 1, 1 }
local APPLICATION_PENDING_TEXT_COLOR = { 0.12, 1, 0.25, 1 }
local APPLICATION_DECLINED_TEXT_COLOR = { 1, 0.18, 0.12, 1 }
local APPLICATION_CANCELLED_TEXT_COLOR = { 0.55, 0.55, 0.55, 1 }
local APPLICATION_MENU_APPLY_COLOR_CODE = "|cff1eff3f"
local BLACKLIST_MENU_COLOR_CODE = "|cffff2020"
local REQUIREMENT_TEXT_COLOR = { 0.12, 1, 0.25, 1 }
local REQUIREMENT_EMPTY_TEXT_COLOR = { 0.5, 0.5, 0.5, 1 }
local EMPTY_TEXT_COLOR = core.PANEL_EMPTY_PROMPT_TEXT_COLOR or { 1, 0.82, 0, 1 }
local EMPTY_TEAMUP_TEXTURE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\TeamUp.png"
local EMPTY_TEAMUP_ICON_COUNT = 3
local EMPTY_TEAMUP_ICON_WIDTH = 27
local EMPTY_TEAMUP_ICON_HEIGHT = 25
local EMPTY_TEAMUP_ICON_GAP = 6
local EMPTY_TEAMUP_TEXT_GAP = 7
local EMPTY_TEAMUP_STEP_SECONDS = 0.28
local EMPTY_TEAMUP_FADE_IN_SECONDS = 0.16
local EMPTY_TEAMUP_HOLD_SECONDS = 0.4
local EMPTY_TEAMUP_FADE_OUT_SECONDS = 0.45
local ROW_FADE_IN_SECONDS = 0.18
local ROW_FADE_OUT_SECONDS = 0.18
local ROW_BACKGROUND_FADE_OUT_SECONDS = 0.45
local ROW_HOVER_HEIGHT = core.GROUP_LIST_ROW_HOVER_HEIGHT or 28
local ROW_HOVER_INSET_X = core.GROUP_LIST_ROW_HOVER_INSET_X or 2
local ROW_HOVER_OFFSET_Y = core.GROUP_LIST_ROW_HOVER_OFFSET_Y or 0
local ROW_HOVER_FADE_WIDTH = core.GROUP_LIST_ROW_HOVER_FADE_WIDTH or 96
local APPLICATION_CANCEL_BUTTON_SIZE = 32
local APPLICATION_CANCEL_BUTTON_SCALE = 0.7
local APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE = APPLICATION_CANCEL_BUTTON_SIZE * APPLICATION_CANCEL_BUTTON_SCALE
local APPLICATION_CANCEL_BUTTON_ICON_SIZE = 16
local APPLICATION_CANCEL_BUTTON_GAP = 4
local APPLICATION_CANCEL_BUTTON_ATLAS = "UI-LFG-DeclineMark"
local APPLICATION_STATUS_ICON_SIZE = 16
local APPLICATION_STATUS_ICON_GAP = 3
local APPLICATION_PENDING_SPINNER_SIZE = 18
local HEADER_REFRESH_BUTTON_SIZE = 35
local HEADER_REFRESH_ICON_SIZE = 21
local HEADER_REFRESH_ICON_PRESSED_SIZE = 19
local HEADER_REFRESH_BUTTON_OFFSET_Y = 6

local COLUMN_WIDTHS = {
    title = 108,
    dungeon = 108,
    type = 96,
    members = 100,
    requirement = 56,
    leader = 92,
    score = 54,
    comment = 130,
}

local COLUMN_ORDER = {
    "title",
    "dungeon",
    "type",
    "members",
    "requirement",
    "leader",
    "score",
    "comment",
}

local HEADER_LABELS = {
    title = "队伍标题",
    dungeon = "副本名称",
    type = "类型",
    members = "队伍成员列",
    requirement = "要求",
    leader = "队长",
    score = "分数",
    comment = "说明",
}

local SORTABLE_HEADER_KEYS = {
    dungeon = true,
    members = true,
    requirement = true,
    score = true,
}

local function ColorText(text, color)
    if color and color.GenerateHexColorMarkup then
        return color:GenerateHexColorMarkup() .. tostring(text or "") .. "|r"
    end
    if type(color) == "table" and color.colorStr then
        return "|c" .. color.colorStr .. tostring(text or "") .. "|r"
    end
    return tostring(text or "")
end

local GetClassColor

local function IsUnknownMemberName(name)
    name = strtrim and strtrim(tostring(name or "")) or tostring(name or "")
    if name == "" or name == "-" or name == "未知目标" or name == "Unknown" or name == "Unknown Target" then
        return true
    end
    if _G.UNKNOWNOBJECT and name == _G.UNKNOWNOBJECT then
        return true
    end
    if _G.UNKNOWN and name == _G.UNKNOWN then
        return true
    end
    if _G.UNKNOWNBEING and name == _G.UNKNOWNBEING then
        return true
    end
    return false
end

local function GetTooltipMemberName(member)
    local memberName = member and (member.displayName or member.name)
    if IsUnknownMemberName(memberName) then
        return nil
    end
    return ColorText(memberName, GetClassColor(member.classFile))
end

local function GetInlineTexture(texture, size)
    size = size or 16
    return string.format("|T%s:%d:%d:0:0|t", tostring(texture or ""), size, size)
end

local function GetInlineAtlas(atlasName, size)
    if type(atlasName) ~= "string" or atlasName == "" then
        return nil
    end
    size = size or 16
    return string.format("|A:%s:%d:%d|a", atlasName, size, size)
end

local function NormalizeTooltipRoleKey(role)
    if role == "HEALER" then
        return "HEAL"
    end
    if role == "DAMAGER" then
        return "DPS"
    end
    return role
end

local function GetTooltipRoleIconMarkup(role)
    local roleKey = NormalizeTooltipRoleKey(role)
    return GetInlineAtlas(ROLE_ATLAS_BY_KEY[roleKey] or ROLE_ATLAS_BY_KEY.EMPTY, TOOLTIP_ICON_SIZE)
end

local function IsFindGroupLeaderMember(result, member)
    if not member then
        return false
    end
    if member.isLeader == true then
        return true
    end
    local leaderName = result and result.leaderName
    if type(leaderName) ~= "string" or leaderName == "" then
        return false
    end
    if member.name == leaderName then
        return true
    end
    local leaderShortName = leaderName:match("^([^-]+)%-.+$")
    return leaderShortName and member.name == leaderShortName
end

local function FormatTooltipTeamLeaderName(name, classColor)
    local leaderName = ColorText(name or "-", classColor)
    if IsUnknownMemberName(name) then
        return leaderName
    end
    return string.format("%s%s%s", leaderName, TOOLTIP_ICON_TEXT_GAP, TOOLTIP_GROUP_LEADER_ICON_MARKUP)
end

local function AddTypeMemberTooltipLine(tooltip, texture, label, names, r, g, b)
    if not (tooltip and type(names) == "table" and #names > 0) then
        return false
    end
    local text = string.format(
        TRANSLATE("%s：%s"),
        label,
        table.concat(names, TRANSLATE("、"))
    )
    tooltip:AddLine(GetInlineTexture(texture, 16) .. " " .. text, r, g, b, true)
    return true
end

GetClassColor = function(classFile)
    local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    return classFile and colors and colors[classFile] or nil
end

local function GetScoreColor(score)
    score = tonumber(score) or 0
    if C_ChallengeMode and C_ChallengeMode.GetDungeonScoreRarityColor then
        local color = C_ChallengeMode.GetDungeonScoreRarityColor(score)
        if color then
            return color
        end
    end
    return HIGHLIGHT_FONT_COLOR
end

local function GetColumnLayout(availableWidth)
    local layout = {}
    local widths = {}
    local baseTotal = ROW_SIDE_INSET * 2
    for _, key in ipairs(COLUMN_ORDER) do
        baseTotal = baseTotal + (COLUMN_WIDTHS[key] or 0)
    end
    baseTotal = baseTotal + math.max(0, #COLUMN_ORDER - 1) * ROW_GAP

    local scale = 1
    local extraCommentWidth = 0
    if availableWidth and availableWidth > 0 then
        if availableWidth < baseTotal then
            scale = math.max(0.76, availableWidth / baseTotal)
        elseif availableWidth > baseTotal then
            extraCommentWidth = availableWidth - baseTotal
        end
    end

    for _, key in ipairs(COLUMN_ORDER) do
        widths[key] = (COLUMN_WIDTHS[key] or 0) * scale
    end

    local typeDeficit = TYPE_COLUMN_MIN_WIDTH - (widths.type or 0)
    if typeDeficit > 0 and (widths.title or 0) > TITLE_COLUMN_MIN_WIDTH then
        local transfer = math.min(typeDeficit, (widths.title or 0) - TITLE_COLUMN_MIN_WIDTH)
        widths.title = widths.title - transfer
        widths.type = widths.type + transfer
    end

    local x = ROW_SIDE_INSET * scale
    for index, key in ipairs(COLUMN_ORDER) do
        local width = widths[key] or 0
        if key == "comment" then
            width = width + extraCommentWidth
        end
        layout[key] = { x = x, width = math.max(1, width) }
        x = x + width
        if index < #COLUMN_ORDER then
            x = x + ROW_GAP
        end
    end
    layout.totalWidth = x + ROW_SIDE_INSET * scale
    layout.scale = scale
    return layout
end

local function CreatePlainLabel(parent, text, size, justify)
    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetText(text or "")
    label:SetJustifyH(justify or "LEFT")
    label:SetJustifyV("MIDDLE")
    label:SetWordWrap(false)
    if core.ApplyFont then
        core.ApplyFont(label, size or 12, "")
    end
    return label
end

local function CreateFindGroupFrameWithTemplateOptions(frameType, name, parent, templates)
    for _, template in ipairs(templates or {}) do
        local ok, frame = pcall(CreateFrame, frameType, name, parent, template)
        if ok and frame then
            return frame
        end
    end
    return CreateFrame(frameType, name, parent)
end

local function CreateFindGroupScrollContainer(parent)
    local scrollFrame = CreateFrame("ScrollFrame", nil, parent)
    scrollFrame:EnableMouseWheel(true)

    local content = CreateFrame("Frame", nil, scrollFrame)
    content:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
    content:SetWidth(1)
    content:SetHeight(1)
    scrollFrame:SetScrollChild(content)

    local scrollBar = CreateFindGroupFrameWithTemplateOptions("EventFrame", nil, parent, { "MinimalScrollBar" })
    if not scrollBar or (not scrollBar.SetValue and not scrollBar.SetScrollPercentage) then
        scrollBar = CreateFrame("Slider", nil, parent)
        scrollBar:SetOrientation("VERTICAL")
        scrollBar:SetValueStep(1)
    end
    scrollBar:SetWidth(APPLICANT_SCROLLBAR_WIDTH)
    if scrollBar.SetMinMaxValues then
        scrollBar:SetMinMaxValues(0, 0)
    end
    scrollBar:Hide()

    local useNativeMinimalScrollBar = scrollBar.Init and ScrollUtil and ScrollUtil.InitScrollFrameWithScrollBar
    if useNativeMinimalScrollBar then
        ScrollUtil.InitScrollFrameWithScrollBar(scrollFrame, scrollBar)
    end

    local isSyncingScroll = false
    local scrollMinValue = 0
    local scrollMaxValue = 0

    local function setScrollBarRange(minValue, maxValue)
        scrollMinValue = minValue or 0
        scrollMaxValue = maxValue or 0
        if scrollBar.SetMinMaxValues then
            scrollBar:SetMinMaxValues(scrollMinValue, scrollMaxValue)
        elseif scrollBar.SetScrollPercentage then
            scrollBar:SetScrollPercentage(0)
        end
    end
    scrollBar.mkSetScrollRange = setScrollBarRange

    local function getScrollBarValue()
        if useNativeMinimalScrollBar and scrollBar.GetScrollPercentage then
            return (scrollBar:GetScrollPercentage() or 0) * scrollMaxValue
        end
        if scrollBar.GetValue then
            return scrollBar:GetValue() or 0
        end
        if scrollBar.GetScrollPercentage then
            return (scrollBar:GetScrollPercentage() or 0) * scrollMaxValue
        end
        return 0
    end
    scrollBar.mkGetScrollValue = getScrollBarValue

    local function setScrollBarValue(value)
        if useNativeMinimalScrollBar and scrollBar.SetScrollPercentage then
            local percentage = scrollMaxValue > 0 and ((value or 0) / scrollMaxValue) or 0
            scrollBar:SetScrollPercentage(math.min(math.max(percentage, 0), 1), ScrollBoxConstants and ScrollBoxConstants.NoScrollInterpolation)
        elseif scrollBar.SetValue then
            scrollBar:SetValue(value or 0)
        elseif scrollBar.SetScrollPercentage then
            local percentage = scrollMaxValue > 0 and ((value or 0) / scrollMaxValue) or 0
            scrollBar:SetScrollPercentage(math.min(math.max(percentage, 0), 1))
        end
    end
    scrollBar.mkSetScrollValue = setScrollBarValue

    local function applyScrollValue(value)
        local nextValue = math.min(math.max(value or 0, scrollMinValue), scrollMaxValue)
        isSyncingScroll = true
        scrollFrame:SetVerticalScroll(nextValue)
        if math.abs(getScrollBarValue() - nextValue) > 0.5 then
            setScrollBarValue(nextValue)
        end
        isSyncingScroll = false
        if scrollFrame.mkOnScrollValueChanged then
            scrollFrame.mkOnScrollValueChanged(scrollFrame, nextValue)
        end
    end

    if useNativeMinimalScrollBar then
        -- ScrollUtil.InitScrollFrameWithScrollBar wires the arrows, thumb, mouse wheel, and ScrollFrame sync.
    elseif scrollBar.SetScrollPercentage and scrollBar.RegisterCallback and scrollBar.Event and scrollBar.Event.OnScroll then
        scrollBar:RegisterCallback(scrollBar.Event.OnScroll, function(_, scrollPercentage)
            if not isSyncingScroll then
                applyScrollValue((scrollPercentage or 0) * scrollMaxValue)
            end
        end)
    elseif scrollBar.SetScript and scrollBar.SetValue then
        scrollBar:SetScript("OnValueChanged", function(_, value)
            if not isSyncingScroll then
                applyScrollValue(value or getScrollBarValue())
            end
        end)
    end
    setScrollBarValue(0)

    if not useNativeMinimalScrollBar then
        scrollFrame:SetScript("OnVerticalScroll", function(_, offset)
            applyScrollValue(offset)
        end)
        scrollFrame:SetScript("OnMouseWheel", function(_, delta)
            applyScrollValue(getScrollBarValue() - (delta or 0) * ROW_HEIGHT)
        end)
    end
    scrollFrame:HookScript("OnVerticalScroll", function(frame, offset)
        if frame.mkOnScrollValueChanged then
            frame.mkOnScrollValueChanged(frame, offset or frame:GetVerticalScroll() or 0)
        end
    end)

    scrollFrame:SetScript("OnSizeChanged", function(frame)
        content:SetWidth(math.max(frame:GetWidth() or 0, 1))
    end)

    return scrollFrame, content, scrollBar
end

local function SetFindGroupScrollValue(scrollFrame, scrollBar, value)
    local nextValue = value or 0
    if scrollFrame and scrollFrame.SetVerticalScroll then
        scrollFrame:SetVerticalScroll(nextValue)
    end
    if scrollBar and scrollBar.mkSetScrollValue then
        scrollBar.mkSetScrollValue(nextValue)
    elseif scrollBar and scrollBar.SetValue then
        scrollBar:SetValue(nextValue)
    end
end

local function GetFindGroupLoadingCycleSeconds()
    return (EMPTY_TEAMUP_STEP_SECONDS * EMPTY_TEAMUP_ICON_COUNT)
        + EMPTY_TEAMUP_HOLD_SECONDS
        + EMPTY_TEAMUP_FADE_OUT_SECONDS
end

local function GetFindGroupLoadingIconAlpha(elapsed, iconIndex)
    local appearAt = (iconIndex - 1) * EMPTY_TEAMUP_STEP_SECONDS
    if elapsed < appearAt then
        return 0
    end

    local fadeInEnd = appearAt + EMPTY_TEAMUP_FADE_IN_SECONDS
    if elapsed < fadeInEnd then
        return math.max(0, math.min(1, (elapsed - appearAt) / EMPTY_TEAMUP_FADE_IN_SECONDS))
    end

    local fadeOutStart = (EMPTY_TEAMUP_STEP_SECONDS * EMPTY_TEAMUP_ICON_COUNT) + EMPTY_TEAMUP_HOLD_SECONDS
    if elapsed < fadeOutStart then
        return 1
    end

    local fadeOutEnd = fadeOutStart + EMPTY_TEAMUP_FADE_OUT_SECONDS
    if elapsed < fadeOutEnd then
        return math.max(0, math.min(1, 1 - ((elapsed - fadeOutStart) / EMPTY_TEAMUP_FADE_OUT_SECONDS)))
    end

    return 0
end

local function RefreshFindGroupLoadingAnimation(animationFrame)
    if not (animationFrame and animationFrame.icons) then
        return
    end

    local elapsed = animationFrame.elapsed or 0
    for index, icon in ipairs(animationFrame.icons) do
        local alpha = GetFindGroupLoadingIconAlpha(elapsed, index)
        icon:SetAlpha(alpha)
        if alpha > 0.02 then
            icon:Show()
        else
            icon:Hide()
        end
    end
end

local function EnsureFindGroupLoadingAnimation(widgets)
    if not widgets then
        return nil
    end
    if widgets.findGroupLoadingAnimation then
        return widgets.findGroupLoadingAnimation
    end

    local parent = widgets.findGroupScrollFrame or widgets.findGroupRowsFrame
    if not parent then
        return nil
    end

    local frame = CreateFrame("Frame", nil, parent)
    frame:SetFrameLevel((parent.GetFrameLevel and parent:GetFrameLevel() or 0) + 5)
    local width = (EMPTY_TEAMUP_ICON_WIDTH * EMPTY_TEAMUP_ICON_COUNT)
        + (EMPTY_TEAMUP_ICON_GAP * (EMPTY_TEAMUP_ICON_COUNT - 1))
    frame:SetSize(width, EMPTY_TEAMUP_ICON_HEIGHT)
    frame.icons = {}

    for index = 1, EMPTY_TEAMUP_ICON_COUNT do
        local icon = frame:CreateTexture(nil, "ARTWORK")
        icon:SetTexture(EMPTY_TEAMUP_TEXTURE)
        icon:SetTexCoord((index - 1) / EMPTY_TEAMUP_ICON_COUNT, index / EMPTY_TEAMUP_ICON_COUNT, 0, 1)
        icon:SetSize(EMPTY_TEAMUP_ICON_WIDTH, EMPTY_TEAMUP_ICON_HEIGHT)
        icon:SetPoint("LEFT", frame, "LEFT", (index - 1) * (EMPTY_TEAMUP_ICON_WIDTH + EMPTY_TEAMUP_ICON_GAP), 0)
        icon:SetAlpha(0)
        icon:Hide()
        frame.icons[index] = icon
    end

    frame:SetScript("OnShow", function(animation)
        animation.elapsed = 0
        RefreshFindGroupLoadingAnimation(animation)
    end)
    frame:SetScript("OnUpdate", function(animation, elapsed)
        animation.elapsed = ((animation.elapsed or 0) + (elapsed or 0)) % GetFindGroupLoadingCycleSeconds()
        RefreshFindGroupLoadingAnimation(animation)
    end)
    frame:Hide()

    widgets.findGroupLoadingAnimation = frame
    return frame
end

local function SetFindGroupLoadingAnimationShown(widgets, shown, attachToEmpty)
    local animation = shown and EnsureFindGroupLoadingAnimation(widgets) or widgets and widgets.findGroupLoadingAnimation
    if not animation then
        return
    end

    animation:ClearAllPoints()
    if attachToEmpty and widgets and widgets.findGroupEmpty then
        animation:SetPoint("TOP", widgets.findGroupEmpty, "BOTTOM", 0, -EMPTY_TEAMUP_TEXT_GAP)
    elseif widgets and widgets.findGroupScrollFrame then
        animation:SetPoint("BOTTOM", widgets.findGroupScrollFrame, "BOTTOM", 0, 8)
    elseif widgets and widgets.findGroupRowsFrame then
        animation:SetPoint("BOTTOM", widgets.findGroupRowsFrame, "BOTTOM", 0, 8)
    end

    if shown then
        local wasShown = animation.IsShown and animation:IsShown()
        if not wasShown then
            animation.elapsed = 0
            RefreshFindGroupLoadingAnimation(animation)
        end
        animation:Show()
    else
        animation:Hide()
    end
end

local function UpdateFindGroupEmptyLayout(widgets, showAnimation)
    local empty = widgets and widgets.findGroupEmpty
    local rowsFrame = widgets and widgets.findGroupRowsFrame
    if not (empty and rowsFrame) then
        return
    end

    local offsetY = showAnimation and ((EMPTY_TEAMUP_ICON_HEIGHT + EMPTY_TEAMUP_TEXT_GAP) / 2) or 0
    empty:ClearAllPoints()
    empty:SetPoint("CENTER", rowsFrame, "CENTER", 0, offsetY)
end

local function SetHeaderTextPressed(label, pressed)
    local text = label and label.text
    if not text then
        return
    end
    label.mkHeaderTextPressed = pressed == true
    if ApplyHeaderTextStyle then
        ApplyHeaderTextStyle(text, label, {
            pressed = pressed,
            hovered = label.mkHeaderTextHovered == true,
            enabled = not (label.IsEnabled and not label:IsEnabled()),
            applyFont = core.ApplyFont,
        })
    end
end

local function SetHeaderTextHovered(label, hovered)
    if not label then
        return
    end
    label.mkHeaderTextHovered = hovered == true
    SetHeaderTextPressed(label, label.mkHeaderTextPressed == true)
end

local function RefreshHeaderSortState(header, owner)
    if not header then
        return
    end
    local sortKey = ""
    if FindGroupSearch.GetSort then
        sortKey = FindGroupSearch.GetSort(owner)
    end
    for _, key in ipairs(COLUMN_ORDER) do
        local label = header[key]
        if label and label.text then
            local text = TRANSLATE(HEADER_LABELS[key])
            label.text:SetText(text)
            SetHeaderTextPressed(label, label.mkHeaderTextPressed == true)
        end
    end
end

local function CreateHeader(parent, owner)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetHeight(HEADER_HEIGHT)
    for _, key in ipairs(COLUMN_ORDER) do
        local columnKey = key
        local label = CreateFrame(SORTABLE_HEADER_KEYS[columnKey] and "Button" or "Frame", nil, frame)
        local text = CreatePlainLabel(label, TRANSLATE(HEADER_LABELS[key]), HEADER_TEXT_SIZE, "CENTER")
        label.text = text
        SetHeaderTextPressed(label, false)
        if SORTABLE_HEADER_KEYS[columnKey] then
            label:RegisterForClicks("LeftButtonUp")
            label:SetScript("OnEnter", function(self)
                SetHeaderTextHovered(self, true)
            end)
            label:SetScript("OnMouseDown", function(self)
                SetHeaderTextPressed(self, true)
            end)
            label:SetScript("OnMouseUp", function(self)
                SetHeaderTextPressed(self, false)
            end)
            label:SetScript("OnLeave", function(self)
                SetHeaderTextHovered(self, false)
                SetHeaderTextPressed(self, false)
            end)
            label:SetScript("OnClick", function()
                if FindGroupSearch.SetSort then
                    FindGroupSearch.SetSort(owner, columnKey)
                end
                RefreshHeaderSortState(frame, owner)
                if owner and owner.RefreshMainWindow then
                    owner:RefreshMainWindow("FindGroupSort", columnKey)
                end
            end)
        end
        frame[columnKey] = label
    end
    RefreshHeaderSortState(frame, owner)
    return frame
end

local function SetHeaderAccentGradient(texture, startAlpha, endAlpha)
    if not texture then
        return
    end

    if texture.SetGradient and CreateColor then
        local ok = pcall(
            texture.SetGradient,
            texture,
            "VERTICAL",
            CreateColor(HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], startAlpha),
            CreateColor(HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], endAlpha)
        )
        if ok then
            return
        end
    end

    if texture.SetGradientAlpha then
        texture:SetGradientAlpha(
            "VERTICAL",
            HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], startAlpha,
            HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], endAlpha
        )
    else
        texture:SetVertexColor(
            HEADER_ACCENT_COLOR[1],
            HEADER_ACCENT_COLOR[2],
            HEADER_ACCENT_COLOR[3],
            math.max(startAlpha or 0, endAlpha or 0)
        )
    end
end

local function LayoutHeaderAccent(accent)
    if not accent then
        return
    end

    local height = HEADER_ACCENT_HEIGHT
    local halfHeight = height / 2
    accent:SetSize(GetHeaderAccentPixelWidth(accent), height)

    if accent.top then
        accent.top:ClearAllPoints()
        accent.top:SetPoint("TOPLEFT", accent, "TOPLEFT", 0, 0)
        accent.top:SetPoint("TOPRIGHT", accent, "TOPRIGHT", 0, 0)
        accent.top:SetHeight(halfHeight)
    end
    if accent.bottom then
        accent.bottom:ClearAllPoints()
        accent.bottom:SetPoint("TOPLEFT", accent.top, "BOTTOMLEFT", 0, 0)
        accent.bottom:SetPoint("BOTTOMRIGHT", accent, "BOTTOMRIGHT", 0, 0)
    end
end

local function CreateHeaderAccent(parent)
    local accent = CreateFrame("Frame", nil, parent)

    local top = accent:CreateTexture(nil, "OVERLAY")
    top:SetTexture("Interface\\Buttons\\WHITE8X8")
    SetHeaderAccentGradient(top, HEADER_ACCENT_ALPHA, 0)

    local bottom = accent:CreateTexture(nil, "OVERLAY")
    bottom:SetTexture("Interface\\Buttons\\WHITE8X8")
    SetHeaderAccentGradient(bottom, 0, HEADER_ACCENT_ALPHA)

    accent.top = top
    accent.bottom = bottom
    LayoutHeaderAccent(accent)

    return accent
end

local function EnsureHeaderGapAccents(header)
    header.gapAccents = header.gapAccents or {}
    local requiredCount = math.max(0, #COLUMN_ORDER - 1)
    for index = #header.gapAccents + 1, requiredCount do
        header.gapAccents[index] = CreateHeaderAccent(header)
    end
    return header.gapAccents
end

local function ApplyHeaderLayout(header, availableWidth)
    local layout = GetColumnLayout(availableWidth)
    local gapAccents = header and EnsureHeaderGapAccents(header) or {}
    for _, key in ipairs(COLUMN_ORDER) do
        local label = header and header[key]
        if label and layout[key] then
            label:ClearAllPoints()
            label:SetPoint("LEFT", header, "LEFT", layout[key].x, 0)
            label:SetSize(layout[key].width, HEADER_HEIGHT)
            SetHeaderTextPressed(label, label.mkHeaderTextPressed == true)
        end
    end
    for index = 1, #gapAccents do
        local leftKey = COLUMN_ORDER[index]
        local leftColumn = layout[leftKey]
        local accent = gapAccents[index]
        if leftColumn and accent then
            accent:ClearAllPoints()
            LayoutHeaderAccent(accent)
            accent:SetPoint("CENTER", header, "LEFT", SnapHeaderAccentOffset(header, leftColumn.x + leftColumn.width), 0)
            accent:Show()
        elseif accent then
            accent:Hide()
        end
    end
    header.mkFindGroupColumnLayout = layout
end

local function SetTextureAtlas(texture, atlasName)
    if not texture or not atlasName then
        return false
    end
    if texture.SetAtlas then
        local ok = pcall(texture.SetAtlas, texture, atlasName)
        if ok then
            return true
        end
    end
    return false
end

local function CreateFallbackFindGroupPendingSpinner(parent)
    if not (CreateFrame and parent) then
        return nil
    end

    local spinner = CreateFrame("Frame", nil, parent)
    spinner.Ring = spinner:CreateTexture(nil, "ARTWORK")
    spinner.Ring:SetAllPoints(spinner)
    spinner.Ring:SetAtlas("Spinner_Ring")
    spinner.Sparks = spinner:CreateTexture(nil, "ARTWORK")
    spinner.Sparks:SetAllPoints(spinner)
    spinner.Sparks:SetAtlas("Spinner_Sparks")
    if spinner.Sparks.SetBlendMode then
        spinner.Sparks:SetBlendMode("ADD")
    end
    return spinner
end

local function CreateFindGroupPendingSpinner(parent)
    if not (CreateFrame and parent) then
        return nil
    end

    local ok, spinner = pcall(CreateFrame, "Frame", nil, parent, "SpinnerTemplate")
    spinner = ok and spinner or CreateFallbackFindGroupPendingSpinner(parent)
    if spinner then
        spinner:SetSize(APPLICATION_PENDING_SPINNER_SIZE, APPLICATION_PENDING_SPINNER_SIZE)
        spinner:Hide()
    end
    return spinner
end

local function StopFindGroupPendingSpinner(spinner)
    if not spinner then
        return
    end
    if spinner.Anim and spinner.Anim.Stop then
        spinner.Anim:Stop()
    end
    spinner:Hide()
end

local function StartFindGroupPendingSpinner(spinner)
    if not spinner then
        return
    end
    spinner:SetSize(APPLICATION_PENDING_SPINNER_SIZE, APPLICATION_PENDING_SPINNER_SIZE)
    spinner:Show()
    if spinner.Anim and spinner.Anim.Play and (not spinner.Anim.IsPlaying or not spinner.Anim:IsPlaying()) then
        spinner.Anim:Play()
    end
end

local function EnsureFindGroupButtonSpinner(button)
    if not button then
        return nil
    end
    if button.mkFindGroupButtonSpinner then
        return button.mkFindGroupButtonSpinner
    end

    local spinner = CreateFindGroupPendingSpinner(button)
    if not spinner then
        return nil
    end
    spinner:ClearAllPoints()
    spinner:SetPoint("CENTER", button, "CENTER", 0, 0)
    if spinner.SetFrameLevel and button.GetFrameLevel then
        spinner:SetFrameLevel((button:GetFrameLevel() or 0) + 2)
    end
    button.mkFindGroupButtonSpinner = spinner
    return spinner
end

local function SetFindGroupButtonTextHiddenForLoading(button, hidden)
    if not button then
        return
    end

    hidden = hidden == true
    local fontString = button.Text or (button.GetFontString and button:GetFontString())
    if hidden then
        if button.mkFindGroupButtonSavedText == nil then
            if type(button.mkFindGroupButtonText) == "string" then
                button.mkFindGroupButtonSavedText = button.mkFindGroupButtonText
            elseif button.GetText then
                button.mkFindGroupButtonSavedText = button:GetText()
            end
        end
        if button.SetText then
            button:SetText("")
        end
        if fontString then
            fontString:SetText("")
            if fontString.SetAlpha then
                fontString:SetAlpha(0)
            end
            if fontString.Hide then
                fontString:Hide()
            end
        end
        return
    end

    local text = button.mkFindGroupButtonText
    if text == nil then
        text = button.mkFindGroupButtonSavedText
    end
    button.mkFindGroupButtonSavedText = nil
    if text ~= nil then
        if button.SetText then
            button:SetText(text)
        end
        if fontString then
            fontString:SetText(text)
        end
    end
    if fontString then
        if fontString.Show then
            fontString:Show()
        end
        if fontString.SetAlpha then
            fontString:SetAlpha(1)
        end
    end
end

local function SetFindGroupSearchButtonLoading(button, loading)
    if not button then
        return
    end

    loading = loading == true
    if loading then
        button.mkFindGroupButtonLoading = true
        SetFindGroupButtonTextHiddenForLoading(button, true)
        local spinner = EnsureFindGroupButtonSpinner(button)
        StartFindGroupPendingSpinner(spinner)
    else
        local spinner = button.mkFindGroupButtonSpinner
        StopFindGroupPendingSpinner(spinner)
        button.mkFindGroupButtonLoading = nil
        SetFindGroupButtonTextHiddenForLoading(button, false)
    end
    if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
        core.MeetingStoneButtonArt.Update(button)
    end
end

local function PlayAlphaAnimation(frame, key, fromAlpha, toAlpha, duration, onFinished, smoothing)
    if not frame then
        return
    end
    if not frame.CreateAnimationGroup then
        frame:SetAlpha(toAlpha or 1)
        if onFinished then
            onFinished()
        end
        return
    end

    frame.mkFindGroupAnimations = frame.mkFindGroupAnimations or {}
    local group = frame.mkFindGroupAnimations[key]
    if not group then
        group = frame:CreateAnimationGroup()
        local alpha = group:CreateAnimation("Alpha")
        alpha:SetOrder(1)
        group.mkAlpha = alpha
        frame.mkFindGroupAnimations[key] = group
    end
    group:Stop()
    group.mkAlpha:SetFromAlpha(fromAlpha or 0)
    group.mkAlpha:SetToAlpha(toAlpha or 1)
    group.mkAlpha:SetDuration(duration or ROW_FADE_IN_SECONDS)
    if smoothing and group.mkAlpha.SetSmoothing then
        group.mkAlpha:SetSmoothing(smoothing)
    end
    group:SetScript("OnFinished", onFinished)
    frame:SetAlpha(fromAlpha or 0)
    group:Play()
end

local function StopAlphaAnimation(frame, key, alpha)
    if not frame then
        return
    end
    local group = frame.mkFindGroupAnimations and frame.mkFindGroupAnimations[key]
    if group then
        group:Stop()
    end
    if frame.SetAlpha then
        frame:SetAlpha(alpha or 1)
    end
end

local function MarkRowFadeComplete(row, resultID, widgets, state)
    local key = tostring(resultID or "")
    if key == "" then
        return nil, nil
    end

    if widgets then
        local requestID = tonumber(state and state.searchRequestID) or 0
        if widgets.mkFindGroupFadeSearchRequestID ~= requestID then
            widgets.mkFindGroupFadeSearchRequestID = requestID
            widgets.mkFindGroupFadedResultIDs = {}
        end
        widgets.mkFindGroupFadedResultIDs = widgets.mkFindGroupFadedResultIDs or {}
        widgets.mkFindGroupFadedResultIDs[key] = true
        row.mkFindGroupFadeKey = key
        return key, widgets.mkFindGroupFadedResultIDs
    end
    row.mkFindGroupFadeKey = key
    return key, nil
end

local function CompleteRowFadeIn(row, resultID, widgets, state, fadeToken)
    if not row then
        return
    end
    if fadeToken and row.mkFindGroupFadeToken ~= fadeToken then
        return
    end
    if tostring(row.resultID or "") ~= tostring(resultID or "") then
        return
    end

    MarkRowFadeComplete(row, resultID, widgets, state)
    if row.SetAlpha then
        row:SetAlpha(1)
    end
end

local function PlayRowFadeIn(row, resultID, widgets, state, animate)
    local key = tostring(resultID or "")
    if key == "" then
        row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    if animate == false then
        row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
        MarkRowFadeComplete(row, resultID, widgets, state)
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    local fadedResults
    if widgets then
        local requestID = tonumber(state and state.searchRequestID) or 0
        if widgets.mkFindGroupFadeSearchRequestID ~= requestID then
            widgets.mkFindGroupFadeSearchRequestID = requestID
            widgets.mkFindGroupFadedResultIDs = {}
        end
        widgets.mkFindGroupFadedResultIDs = widgets.mkFindGroupFadedResultIDs or {}
        fadedResults = widgets.mkFindGroupFadedResultIDs
        if fadedResults[key] then
            row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
            row.mkFindGroupFadeKey = key
            StopAlphaAnimation(row, "appear", 1)
            return
        end
    elseif row.mkFindGroupFadeKey == key then
        row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    row.mkFindGroupFadeKey = key
    row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
    local fadeToken = row.mkFindGroupFadeToken
    PlayAlphaAnimation(row, "appear", 0, 1, ROW_FADE_IN_SECONDS, function()
        CompleteRowFadeIn(row, resultID, widgets, state, fadeToken)
    end)
    if C_Timer and C_Timer.After then
        C_Timer.After(ROW_FADE_IN_SECONDS + 0.05, function()
            CompleteRowFadeIn(row, resultID, widgets, state, fadeToken)
        end)
    end
end

local function PlayRowFadeOut(row, result)
    local resultID = result and result.resultID
    local token = result and result.reportRemovalToken
    if not (row and resultID and token) then
        return
    end
    if row.mkFindGroupRemovalResultID == resultID and row.mkFindGroupRemovalToken == token then
        return
    end

    row.mkFindGroupRemovalResultID = resultID
    row.mkFindGroupRemovalToken = token
    StopAlphaAnimation(row, "appear", row.GetAlpha and row:GetAlpha() or 1)
    local function finishRemoval()
        if row.mkFindGroupRemovalResultID ~= resultID or row.mkFindGroupRemovalToken ~= token then
            return
        end
        if FindGroupSearch.CompleteReportRemoval then
            FindGroupSearch.CompleteReportRemoval(MK, resultID, token)
        end
    end
    PlayAlphaAnimation(row, "remove", row.GetAlpha and row:GetAlpha() or 1, 0, ROW_FADE_OUT_SECONDS, finishRemoval, "IN")
    if C_Timer and C_Timer.After then
        C_Timer.After(ROW_FADE_OUT_SECONDS + 0.05, finishRemoval)
    else
        finishRemoval()
    end
end

local function SetRowBackground(row, texturePath, color, animate)
    local widgets = row and row.widgets
    local background = widgets and widgets.background
    if not background then
        return
    end

    color = color or { 1, 1, 1, 1 }
    local key = table.concat({
        tostring(texturePath or ""),
        string.format("%.2f", color[1] or 1),
        string.format("%.2f", color[2] or 1),
        string.format("%.2f", color[3] or 1),
        string.format("%.2f", color[4] or 1),
    }, ":")

    if animate and row.mkFindGroupBackgroundKey and row.mkFindGroupBackgroundKey ~= key and widgets.backgroundTransition then
        widgets.backgroundTransition:SetTexture(row.mkFindGroupBackgroundTexture or ROW_TEXTURE_NORMAL)
        widgets.backgroundTransition:SetVertexColor(
            row.mkFindGroupBackgroundColorR or 1,
            row.mkFindGroupBackgroundColorG or 1,
            row.mkFindGroupBackgroundColorB or 1,
            row.mkFindGroupBackgroundColorA or 1
        )
        widgets.backgroundTransition:SetAlpha(0.92)
        widgets.backgroundTransition:Show()
        PlayAlphaAnimation(widgets.backgroundTransition, "background", 0.92, 0, ROW_BACKGROUND_FADE_OUT_SECONDS, function()
            widgets.backgroundTransition:Hide()
        end, "IN")
        background:SetTexture(texturePath or ROW_TEXTURE_NORMAL)
        background:SetVertexColor(color[1] or 1, color[2] or 1, color[3] or 1, color[4] or 1)
        background:SetAlpha(0.92)
    else
        background:SetTexture(texturePath or ROW_TEXTURE_NORMAL)
        background:SetVertexColor(color[1] or 1, color[2] or 1, color[3] or 1, color[4] or 1)
        background:SetAlpha(0.92)
    end
    row.mkFindGroupBackgroundKey = key
    row.mkFindGroupBackgroundTexture = texturePath or ROW_TEXTURE_NORMAL
    row.mkFindGroupBackgroundColorR = color[1] or 1
    row.mkFindGroupBackgroundColorG = color[2] or 1
    row.mkFindGroupBackgroundColorB = color[3] or 1
    row.mkFindGroupBackgroundColorA = color[4] or 1
end

local function SetRowHoverColor(row, color)
    if not (row and row.widgets) then
        return
    end
    color = color or { 1, 0.74, 0.18, 0.13 }
    local r, g, b = color[1] or 1, color[2] or 0.74, color[3] or 0.18
    local alpha = color[4] or 0.13
    if row.widgets.hoverLeft then
        if row.widgets.hoverLeft.SetGradient and CreateColor then
            row.widgets.hoverLeft:SetGradient("HORIZONTAL", CreateColor(r, g, b, 0), CreateColor(r, g, b, alpha))
        else
            row.widgets.hoverLeft:SetVertexColor(r, g, b, alpha * 0.6)
        end
    end
    if row.widgets.hoverRight then
        if row.widgets.hoverRight.SetGradient and CreateColor then
            row.widgets.hoverRight:SetGradient("HORIZONTAL", CreateColor(r, g, b, alpha), CreateColor(r, g, b, 0))
        else
            row.widgets.hoverRight:SetVertexColor(r, g, b, alpha * 0.6)
        end
    end
    if row.widgets.hover then
        row.widgets.hover:SetVertexColor(r, g, b, alpha)
    end
end

local function SetFindGroupRowHover(row, shown)
    if row and row.mkFindGroupContextMenuLocked and shown ~= true then
        shown = true
    end
    if MK and MK.SetGroupRowHover then
        MK:SetGroupRowHover(row, shown)
        return
    end
    if not (row and row.widgets) then
        return
    end
    if row.widgets.hoverLeft then
        row.widgets.hoverLeft:SetShown(shown == true)
    end
    if row.widgets.hover then
        row.widgets.hover:SetShown(shown == true)
    end
    if row.widgets.hoverRight then
        row.widgets.hoverRight:SetShown(shown == true)
    end
end

local function FinishFindGroupRowContextMenu(row, token)
    if not row or row.mkFindGroupContextMenuToken ~= token then
        return
    end
    row.mkFindGroupContextMenuLocked = nil
    row.mkFindGroupContextMenuToken = nil
    SetFindGroupRowHover(row, row.IsMouseOver and row:IsMouseOver())
end

local function LockFindGroupRowContextMenu(row)
    if not row then
        return nil
    end
    local token = (tonumber(row.mkFindGroupContextMenuToken) or 0) + 1
    row.mkFindGroupContextMenuToken = token
    row.mkFindGroupContextMenuLocked = true
    SetFindGroupRowHover(row, true)
    return token
end

local function CreateFindGroupHoverTextures(row)
    local hoverLeft = row:CreateTexture(nil, "BORDER", nil, -1)
    hoverLeft:SetPoint("LEFT", row, "LEFT", ROW_HOVER_INSET_X, ROW_HOVER_OFFSET_Y)
    hoverLeft:SetSize(ROW_HOVER_FADE_WIDTH, ROW_HOVER_HEIGHT)
    hoverLeft:SetTexture("Interface\\Buttons\\WHITE8X8")
    if hoverLeft.SetGradient and CreateColor then
        hoverLeft:SetGradient("HORIZONTAL", CreateColor(1, 0.74, 0.18, 0), CreateColor(1, 0.74, 0.18, 0.13))
    else
        hoverLeft:SetVertexColor(1, 0.74, 0.18, 0.08)
    end
    hoverLeft:Hide()

    local hoverRight = row:CreateTexture(nil, "BORDER", nil, -1)
    hoverRight:SetPoint("RIGHT", row, "RIGHT", -ROW_HOVER_INSET_X, ROW_HOVER_OFFSET_Y)
    hoverRight:SetSize(ROW_HOVER_FADE_WIDTH, ROW_HOVER_HEIGHT)
    hoverRight:SetTexture("Interface\\Buttons\\WHITE8X8")
    if hoverRight.SetGradient and CreateColor then
        hoverRight:SetGradient("HORIZONTAL", CreateColor(1, 0.74, 0.18, 0.13), CreateColor(1, 0.74, 0.18, 0))
    else
        hoverRight:SetVertexColor(1, 0.74, 0.18, 0.08)
    end
    hoverRight:Hide()

    local hover = row:CreateTexture(nil, "BORDER", nil, -1)
    hover:SetPoint("LEFT", hoverLeft, "RIGHT", 0, 0)
    hover:SetPoint("RIGHT", hoverRight, "LEFT", 0, 0)
    hover:SetHeight(ROW_HOVER_HEIGHT)
    hover:SetTexture("Interface\\Buttons\\WHITE8X8")
    hover:SetVertexColor(1, 0.74, 0.18, 0.13)
    hover:Hide()

    return hoverLeft, hover, hoverRight
end

local function GetSpecIcon(entry)
    local specID = tonumber(entry and entry.specID)
    if specID and GetSpecializationInfoByID then
        local icon = select(4, GetSpecializationInfoByID(specID))
        if icon then
            return icon
        end
    end
    return nil
end

local function SetRoleIcon(texture, entry)
    local specIcon = GetSpecIcon(entry)
    if specIcon then
        texture:SetTexture(specIcon)
        texture:SetTexCoord(0, 1, 0, 1)
        return
    end
    texture:SetTexture(nil)
    if not SetTextureAtlas(texture, ROLE_ATLAS_BY_KEY[entry and entry.roleKey] or ROLE_ATLAS_BY_KEY.EMPTY) then
        texture:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-ROLES")
        texture:SetTexCoord(0, 1, 0, 1)
    end
end

local function AnchorFindGroupCancelButtonIcon(button, x, y)
    local icon = button and button.mkFindGroupCancelIcon
    if not icon then
        return
    end
    icon:ClearAllPoints()
    icon:SetPoint("CENTER", button, "CENTER", x or 0, y or 0)
end

local function CreateFindGroupApplicationCancelButton(parent)
    local button
    if CreateSystemButton then
        button = CreateSystemButton(parent, nil, APPLICATION_CANCEL_BUTTON_SIZE, APPLICATION_CANCEL_BUTTON_SIZE, "", 0)
    else
        button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
        button:SetSize(APPLICATION_CANCEL_BUTTON_SIZE, APPLICATION_CANCEL_BUTTON_SIZE)
    end
    button:SetSize(APPLICATION_CANCEL_BUTTON_SIZE, APPLICATION_CANCEL_BUTTON_SIZE)
    button:SetScale(APPLICATION_CANCEL_BUTTON_SCALE)
    button:SetFrameLevel(parent:GetFrameLevel() + 4)
    button:SetPoint("CENTER", parent, "CENTER", 0, 0)
    button:RegisterForClicks("LeftButtonUp")
    button:Hide()

    local label = button.Text or (button.GetFontString and button:GetFontString())
    if label then
        label:SetText("")
        label:Hide()
    end

    local icon = button:CreateTexture(nil, "OVERLAY")
    icon:SetSize(APPLICATION_CANCEL_BUTTON_ICON_SIZE, APPLICATION_CANCEL_BUTTON_ICON_SIZE)
    icon:SetVertexColor(1, 1, 1, 1)
    if not SetTextureAtlas(icon, APPLICATION_CANCEL_BUTTON_ATLAS) then
        icon:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up")
        icon:SetTexCoord(0, 1, 0, 1)
    end
    button.mkFindGroupCancelIcon = icon
    AnchorFindGroupCancelButtonIcon(button, 0, 0)

    button:SetScript("OnEnter", function(self)
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(self)
        end
        if GameTooltip then
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(TRANSLATE("取消申请"))
            GameTooltip:Show()
        end
    end)
    button:SetScript("OnLeave", function(self)
        self.mkFindGroupButtonPressed = nil
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(self)
        end
        AnchorFindGroupCancelButtonIcon(self, 0, 0)
        GameTooltip_Hide()
    end)
    button:SetScript("OnMouseDown", function(self, mouseButton)
        if mouseButton == "LeftButton" then
            self.mkFindGroupButtonPressed = true
            if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
                core.MeetingStoneButtonArt.Update(self)
            end
            AnchorFindGroupCancelButtonIcon(self, 1, -1)
        end
    end)
    button:SetScript("OnMouseUp", function(self)
        self.mkFindGroupButtonPressed = nil
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(self)
        end
        AnchorFindGroupCancelButtonIcon(self, 0, 0)
    end)
    button:SetScript("OnClick", function(self)
        local resultID = tonumber(self.resultID)
        if not resultID then
            return
        end
        local ok, reason = FindGroupSearch.CancelApplication(MK, resultID)
        if not ok and MK and MK.Print then
            MK:Print(reason)
        end
        if MK and MK.RefreshMainWindow then
            MK:RefreshMainWindow("FindGroupCancelApplication", tostring(resultID))
        end
    end)

    return button
end

local function CreateResultRow(parent)
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(ROW_HEIGHT)
    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    row:EnableMouse(true)

    local background = row:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(row)
    background:SetTexture(ROW_TEXTURE_NORMAL)
    background:SetAlpha(0.92)

    local backgroundTransition = row:CreateTexture(nil, "BACKGROUND", nil, 1)
    backgroundTransition:SetAllPoints(row)
    backgroundTransition:SetAlpha(0)
    backgroundTransition:Hide()

    local hoverLeft, hover, hoverRight = CreateFindGroupHoverTextures(row)

    local title = CreatePlainLabel(row, "", 12, "LEFT")
    local dungeon = CreatePlainLabel(row, "", 12, "LEFT")
    local typeContainer = CreateFrame("Frame", nil, row)
    local typeIcon = typeContainer:CreateTexture(nil, "OVERLAY")
    typeIcon:SetSize(TYPE_ICON_SIZE, TYPE_ICON_SIZE)
    local typeText = CreatePlainLabel(typeContainer, "", 12, "LEFT")
    local roles = {}
    local rolesContainer = CreateFrame("Frame", nil, row)
    for index = 1, 5 do
        local icon = rolesContainer:CreateTexture(nil, "OVERLAY")
        icon:SetSize(ROLE_ICON_SIZE, ROLE_ICON_SIZE)
        icon:Hide()
        roles[index] = icon
    end
    local requirement = CreatePlainLabel(row, "", 12, "CENTER")
    local leader = CreatePlainLabel(row, "", 12, "CENTER")
    local score = CreatePlainLabel(row, "", 12, "CENTER")
    local comment = CreatePlainLabel(row, "", 12, "LEFT")
    local applicationStatusIcon = row:CreateTexture(nil, "OVERLAY")
    applicationStatusIcon:SetSize(APPLICATION_STATUS_ICON_SIZE, APPLICATION_STATUS_ICON_SIZE)
    applicationStatusIcon:Hide()
    local applicationPendingSpinner = CreateFindGroupPendingSpinner(row)
    local cancelApplicationButtonHost = CreateFrame("Frame", nil, row)
    cancelApplicationButtonHost:SetSize(APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE, APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE)
    cancelApplicationButtonHost:SetFrameLevel(row:GetFrameLevel() + 4)
    cancelApplicationButtonHost:Hide()
    local cancelApplicationButton = CreateFindGroupApplicationCancelButton(cancelApplicationButtonHost)

    row.widgets = {
        background = background,
        backgroundTransition = backgroundTransition,
        hoverLeft = hoverLeft,
        hover = hover,
        hoverRight = hoverRight,
        title = title,
        dungeon = dungeon,
        typeContainer = typeContainer,
        typeIcon = typeIcon,
        typeText = typeText,
        rolesContainer = rolesContainer,
        roles = roles,
        requirement = requirement,
        leader = leader,
        score = score,
        comment = comment,
        applicationStatusIcon = applicationStatusIcon,
        applicationPendingSpinner = applicationPendingSpinner,
        cancelApplicationButtonHost = cancelApplicationButtonHost,
        cancelApplicationButton = cancelApplicationButton,
    }

    row:SetScript("OnEnter", function(self)
        SetFindGroupRowHover(self, true)
        FindGroupTab.ShowTooltip(self)
    end)
    row:SetScript("OnLeave", function(self)
        SetFindGroupRowHover(self, false)
        if GameTooltip and GameTooltip.GetOwner and GameTooltip:GetOwner() == self then
            GameTooltip:Hide()
        end
    end)
    row:SetScript("OnMouseUp", function(self, button)
        if button == "RightButton" then
            FindGroupTab.ShowContextMenu(self)
        elseif button == "LeftButton" then
            FindGroupTab.HandleRowClick(self)
        end
    end)

    return row
end

local function ApplyRowLayout(row, availableWidth)
    local layout = GetColumnLayout(availableWidth)
    local widgets = row and row.widgets
    if not widgets then
        return
    end
    row.mkFindGroupColumnLayout = layout
    local function setText(key, object)
        local column = layout[key]
        object:ClearAllPoints()
        object:SetPoint("LEFT", row, "LEFT", column.x + 2, 0)
        object:SetSize(math.max(1, column.width - 4), 18)
    end
    local function setCenteredText(key, object)
        local column = layout[key]
        object:ClearAllPoints()
        object:SetPoint("LEFT", row, "LEFT", column.x, 0)
        object:SetSize(math.max(1, column.width), 18)
        object:SetJustifyH("CENTER")
    end
    local function setFrame(key, object, height)
        local column = layout[key]
        object:ClearAllPoints()
        object:SetPoint("LEFT", row, "LEFT", column.x, 0)
        object:SetSize(column.width, height or 18)
    end

    setText("title", widgets.title)
    setText("dungeon", widgets.dungeon)
    setFrame("type", widgets.typeContainer, 18)
    setFrame("members", widgets.rolesContainer, ROLE_ICON_SIZE)
    setCenteredText("requirement", widgets.requirement)
    setCenteredText("leader", widgets.leader)
    setCenteredText("score", widgets.score)
    setText("comment", widgets.comment)
end

local function LayoutApplicationComment(row, applicationState)
    local widgets = row and row.widgets
    local layout = row and row.mkFindGroupColumnLayout
    local column = layout and layout.comment
    if not (widgets and widgets.comment and column) then
        return
    end

    local showCancelButton = applicationState == "pending"
    local button = widgets.cancelApplicationButton
    local buttonHost = widgets.cancelApplicationButtonHost or button
    local buttonWidth = APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE
    local reservedWidth = showCancelButton and (buttonWidth + APPLICATION_CANCEL_BUTTON_GAP) or 0
    local availableWidth = math.max(1, column.width - 4 - reservedWidth)

    widgets.comment:ClearAllPoints()
    widgets.comment:SetJustifyV("MIDDLE")

    if buttonHost then
        buttonHost:ClearAllPoints()
        buttonHost:SetPoint("RIGHT", row, "LEFT", column.x + column.width - 2, 0)
        buttonHost:SetShown(showCancelButton)
    end
    if button then
        button:SetShown(showCancelButton)
    end

    local statusIcon = widgets.applicationStatusIcon
    local pendingSpinner = widgets.applicationPendingSpinner
    if not applicationState then
        if statusIcon then
            statusIcon:Hide()
        end
        StopFindGroupPendingSpinner(pendingSpinner)
        widgets.comment:SetPoint("LEFT", row, "LEFT", column.x + 2, 0)
        widgets.comment:SetSize(availableWidth, 18)
        widgets.comment:SetJustifyH("CENTER")
        return
    end

    local statusFrame
    if statusIcon then
        if applicationState == "pending" then
            statusIcon:Hide()
        else
            statusIcon:SetTexture(APPLICATION_ALERT_ICON_TEXTURE)
            statusIcon:SetTexCoord(0, 1, 0, 1)
            statusIcon:SetSize(APPLICATION_STATUS_ICON_SIZE, APPLICATION_STATUS_ICON_SIZE)
            statusFrame = statusIcon
        end
    end
    if applicationState == "pending" then
        statusFrame = pendingSpinner
    else
        StopFindGroupPendingSpinner(pendingSpinner)
    end

    local textWidth = math.ceil(widgets.comment:GetStringWidth() or 0)
    local statusIconLeftOffset = 10
    local iconAreaWidth = statusIconLeftOffset + APPLICATION_STATUS_ICON_SIZE + APPLICATION_STATUS_ICON_GAP
    local textLeftOffset = 2
    local textAreaWidth = availableWidth
    if statusFrame and math.floor((availableWidth - textWidth) / 2) < iconAreaWidth then
        textLeftOffset = textLeftOffset + iconAreaWidth
        textAreaWidth = math.max(1, availableWidth - iconAreaWidth)
    end
    if statusFrame then
        statusFrame:ClearAllPoints()
        statusFrame:SetPoint("LEFT", row, "LEFT", column.x + statusIconLeftOffset, 0)
        if applicationState == "pending" then
            StartFindGroupPendingSpinner(statusFrame)
        else
            statusFrame:Show()
        end
    end
    widgets.comment:SetPoint("LEFT", row, "LEFT", column.x + textLeftOffset, 0)
    widgets.comment:SetSize(textAreaWidth, 18)
    widgets.comment:SetJustifyH("CENTER")
end

local function LayoutResultType(row, hasIcon)
    local widgets = row and row.widgets
    if not (widgets and widgets.typeContainer and widgets.typeText and widgets.typeIcon) then
        return
    end

    local columnWidth = math.max(1, widgets.typeContainer:GetWidth() or 1)

    widgets.typeIcon:ClearAllPoints()
    widgets.typeText:ClearAllPoints()
    widgets.typeText:SetJustifyH("CENTER")
    widgets.typeText:SetPoint("LEFT", widgets.typeContainer, "LEFT", 0, 0)
    widgets.typeText:SetSize(columnWidth, 18)

    if hasIcon then
        widgets.typeIcon:SetSize(TYPE_ICON_SIZE, TYPE_ICON_SIZE)
        widgets.typeIcon:SetPoint("LEFT", widgets.typeContainer, "LEFT", TYPE_ICON_LEFT_OFFSET, 0)
        widgets.typeIcon:Show()
    else
        widgets.typeIcon:Hide()
    end
end

local function GetFindGroupHoverColor(resultType, applicationState)
    if applicationState == "cancelled" then
        return { 0.65, 0.65, 0.65, 0.18 }
    end
    if applicationState == "declined" or resultType == "leaver" then
        return { 1, 0.12, 0.08, 0.18 }
    end
    if resultType == "newbie" then
        return { 0.12, 1, 0.25, 0.16 }
    end
    if resultType == "leader" or resultType == "star" then
        return { 1, 0.82, 0, 0.16 }
    end
    if resultType == "bnet" or resultType == "guild" or resultType == "friend" then
        return { 0.35, 0.75, 1, 0.16 }
    end
    return { 1, 0.74, 0.18, 0.13 }
end

local function SetResultType(row, resultType, applicationState)
    local widgets = row.widgets
    widgets.typeIcon:Hide()
    widgets.typeText:SetText("")
    local backgroundTexture = ROW_TEXTURE_NORMAL
    local backgroundColor = { 1, 1, 1, 1 }
    local hasIcon = false
    if resultType == "leaver" then
        backgroundTexture = ROW_TEXTURE_RED
        widgets.typeIcon:SetTexture(LEAVER_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("逃兵"))
        widgets.typeText:SetTextColor(1, 0.08, 0.05, 1)
        hasIcon = true
    elseif resultType == "newbie" then
        backgroundTexture = ROW_TEXTURE_GREEN
        widgets.typeIcon:SetTexture(NEWBIE_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("新兵"))
        widgets.typeText:SetTextColor(0.12, 1, 0.25, 1)
        hasIcon = true
    elseif resultType == "leader" then
        widgets.typeIcon:SetTexture(LEADER_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("火车头"))
        widgets.typeText:SetTextColor(1, 0.82, 0, 1)
        hasIcon = true
    elseif resultType == "star" then
        widgets.typeIcon:SetTexture(STAR_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("星团长"))
        widgets.typeText:SetTextColor(1, 0.82, 0, 1)
        hasIcon = true
    elseif resultType == "bnet" then
        backgroundTexture = ROW_TEXTURE_BLUE
        widgets.typeIcon:SetTexture(BNET_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("战网好友"))
        widgets.typeText:SetTextColor(0.35, 0.75, 1, 1)
        hasIcon = true
    elseif resultType == "friend" then
        backgroundTexture = ROW_TEXTURE_BLUE
        widgets.typeIcon:SetTexture(BNET_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("好友"))
        widgets.typeText:SetTextColor(0.35, 0.75, 1, 1)
        hasIcon = true
    elseif resultType == "guild" then
        backgroundTexture = ROW_TEXTURE_BLUE
        widgets.typeIcon:SetTexture(BNET_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("公会"))
        widgets.typeText:SetTextColor(0.35, 0.75, 1, 1)
        hasIcon = true
    end
    if applicationState == "cancelled" or applicationState == "declined" then
        backgroundTexture = ROW_TEXTURE_GREY
        backgroundColor = { 1, 1, 1, 1 }
    end
    SetRowBackground(row, backgroundTexture, backgroundColor, true)
    SetRowHoverColor(row, GetFindGroupHoverColor(resultType, applicationState))
    LayoutResultType(row, hasIcon)
end

local function SetRoleEntries(row, entries)
    local widgets = row.widgets
    for index, icon in ipairs(widgets.roles or {}) do
        local entry = entries and entries[index]
        if entry then
            SetRoleIcon(icon, entry)
            icon:ClearAllPoints()
            icon:SetPoint("LEFT", widgets.rolesContainer, "LEFT", (index - 1) * (ROLE_ICON_SIZE + 2), 0)
            icon:Show()
        else
            icon:Hide()
        end
    end
end

local function FormatApplicationCountdown(seconds)
    seconds = math.max(0, math.floor(tonumber(seconds) or 0))
    return string.format("%d:%02d", math.floor(seconds / 60), seconds % 60)
end

local function RefreshRow(row, result, availableWidth, widgets, state, animateAppearance)
    row.mkFindGroupResult = result
    row.resultID = result.resultID
    if row.widgets.cancelApplicationButton then
        row.widgets.cancelApplicationButton.resultID = result.resultID
    end
    local applicationState = FindGroupSearch.GetApplicationDisplayState and FindGroupSearch.GetApplicationDisplayState(result) or nil
    ApplyRowLayout(row, availableWidth)
    SetResultType(row, FindGroupSearch.GetResultType(result), applicationState)
    SetRoleEntries(row, result.roleEntries)

    row.widgets.title:SetText(result.title or "-")
    row.widgets.title:SetTextColor(1, 1, 1, 1)
    row.widgets.dungeon:SetText(result.dungeonName or "-")
    row.widgets.dungeon:SetTextColor(1, 1, 1, 1)
    local requiredItemLevel = tonumber(result.requiredItemLevel) or 0
    row.widgets.requirement:SetText(requiredItemLevel > 0 and tostring(math.floor(requiredItemLevel + 0.5)) or "-")
    if requiredItemLevel > 0 then
        row.widgets.requirement:SetTextColor(REQUIREMENT_TEXT_COLOR[1], REQUIREMENT_TEXT_COLOR[2], REQUIREMENT_TEXT_COLOR[3], REQUIREMENT_TEXT_COLOR[4])
    else
        row.widgets.requirement:SetTextColor(REQUIREMENT_EMPTY_TEXT_COLOR[1], REQUIREMENT_EMPTY_TEXT_COLOR[2], REQUIREMENT_EMPTY_TEXT_COLOR[3], REQUIREMENT_EMPTY_TEXT_COLOR[4])
    end
    row.widgets.leader:SetText(result.leaderDisplayName or "-")
    local classColor = GetClassColor(result.leaderClassFile)
    if classColor then
        row.widgets.leader:SetTextColor(classColor.r, classColor.g, classColor.b, 1)
    else
        row.widgets.leader:SetTextColor(1, 1, 1, 1)
    end
    local score = tonumber(result.score) or 0
    row.widgets.score:SetText(score > 0 and tostring(math.floor(score + 0.5)) or "-")
    local scoreColor = GetScoreColor(score)
    row.widgets.score:SetTextColor(scoreColor.r or 1, scoreColor.g or 1, scoreColor.b or 1, 1)
    if applicationState == "pending" then
        local remainingSeconds = FindGroupSearch.GetApplicationRemainingSeconds and FindGroupSearch.GetApplicationRemainingSeconds(result) or 0
        row.widgets.comment:SetText(string.format(TRANSLATE("待定 %s"), FormatApplicationCountdown(remainingSeconds)))
        row.widgets.comment:SetTextColor(
            APPLICATION_PENDING_TEXT_COLOR[1],
            APPLICATION_PENDING_TEXT_COLOR[2],
            APPLICATION_PENDING_TEXT_COLOR[3],
            APPLICATION_PENDING_TEXT_COLOR[4]
        )
    elseif applicationState == "declined" then
        row.widgets.comment:SetText(TRANSLATE("被拒绝"))
        row.widgets.comment:SetTextColor(
            APPLICATION_DECLINED_TEXT_COLOR[1],
            APPLICATION_DECLINED_TEXT_COLOR[2],
            APPLICATION_DECLINED_TEXT_COLOR[3],
            APPLICATION_DECLINED_TEXT_COLOR[4]
        )
    elseif applicationState == "cancelled" then
        row.widgets.comment:SetText(TRANSLATE("已取消"))
        row.widgets.comment:SetTextColor(
            APPLICATION_CANCELLED_TEXT_COLOR[1],
            APPLICATION_CANCELLED_TEXT_COLOR[2],
            APPLICATION_CANCELLED_TEXT_COLOR[3],
            APPLICATION_CANCELLED_TEXT_COLOR[4]
        )
    else
        row.widgets.comment:SetText(result.comment or "")
        row.widgets.comment:SetTextColor(COMMENT_TEXT_COLOR[1], COMMENT_TEXT_COLOR[2], COMMENT_TEXT_COLOR[3], COMMENT_TEXT_COLOR[4])
    end
    if applicationState == "cancelled" then
        row.widgets.title:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.dungeon:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.typeText:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.requirement:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.leader:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.score:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
    end
    LayoutApplicationComment(row, applicationState)
    row:Show()
    if result.reportRemovalPending == true then
        PlayRowFadeOut(row, result)
    else
        row.mkFindGroupRemovalResultID = nil
        row.mkFindGroupRemovalToken = nil
        StopAlphaAnimation(row, "remove", 1)
        PlayRowFadeIn(row, result.resultID, widgets, state, animateAppearance)
    end
end

local function EnsureRows(widgets, count)
    widgets.findGroupRows = widgets.findGroupRows or {}
    for index = #widgets.findGroupRows + 1, count do
        widgets.findGroupRows[index] = CreateResultRow(widgets.findGroupRowsFrame)
    end
end

local function UpdateFindGroupScrollState(widgets, rowCount)
    local scrollFrame = widgets and widgets.findGroupScrollFrame
    local content = widgets and widgets.findGroupRowsFrame
    local scrollBar = widgets and widgets.findGroupScrollBar
    if not (scrollFrame and content and scrollBar) then
        return
    end

    local scrollHeight = math.max(scrollFrame:GetHeight() or 0, 1)
    local contentHeight = math.max((rowCount or 0) * ROW_HEIGHT, scrollHeight)
    content:SetHeight(contentHeight)

    local maxScroll = math.max(0, contentHeight - scrollHeight)
    local currentValue = 0
    if scrollBar.mkGetScrollValue then
        currentValue = scrollBar.mkGetScrollValue() or 0
    elseif scrollBar.GetValue then
        currentValue = scrollBar:GetValue() or 0
    end
    currentValue = math.min(math.max(currentValue, 0), maxScroll)

    if scrollBar.mkSetScrollRange then
        scrollBar.mkSetScrollRange(0, maxScroll)
    elseif scrollBar.SetMinMaxValues then
        scrollBar:SetMinMaxValues(0, maxScroll)
    end

    if maxScroll > 0 then
        scrollBar:Show()
        SetFindGroupScrollValue(scrollFrame, scrollBar, currentValue)
        return
    end

    SetFindGroupScrollValue(scrollFrame, scrollBar, 0)
    scrollBar:Hide()
end

local function GetFindGroupScrollValue(widgets)
    local scrollBar = widgets and widgets.findGroupScrollBar
    if scrollBar and scrollBar.mkGetScrollValue then
        return scrollBar.mkGetScrollValue() or 0
    end
    local scrollFrame = widgets and widgets.findGroupScrollFrame
    if scrollFrame and scrollFrame.GetVerticalScroll then
        return scrollFrame:GetVerticalScroll() or 0
    end
    return 0
end

local function QueueVisibleResultDetails(owner, widgets, results)
    if not (FindGroupSearch.QueueResultDetails and widgets and widgets.findGroupScrollFrame) then
        return
    end
    local state = FindGroupSearch.GetState and FindGroupSearch.GetState(owner) or nil
    if not results then
        results = state and state.results or {}
    end
    local totalRows = #results
    if totalRows <= 0 then
        return
    end

    local scrollValue = math.max(0, GetFindGroupScrollValue(widgets))
    local firstIndex = math.floor(scrollValue / ROW_HEIGHT) + 1
    firstIndex = math.min(math.max(firstIndex, 1), totalRows)
    local visibleRows = math.ceil(math.max(widgets.findGroupScrollFrame:GetHeight() or ROW_HEIGHT, ROW_HEIGHT) / ROW_HEIGHT)
    visibleRows = math.min(math.max(visibleRows, 1), FIND_GROUP_VISIBLE_DETAIL_ROWS)
    local lastIndex = math.min(totalRows, firstIndex + visibleRows - 1)

    for index = firstIndex, lastIndex do
        local result = results[index]
        if result and result.resultID and result.detailLoaded ~= true then
            FindGroupSearch.QueueResultDetails(owner, result.resultID)
        end
    end
end

local function RenderRows(owner, widgets)
    if not widgets then
        return false
    end
    widgets.mkFindGroupRenderingRows = true
    local state = FindGroupSearch.GetState(owner)
    local initialDisplayPending = state.initialDisplayPending == true and state.initialDisplayReady ~= true
    local results = initialDisplayPending and {} or (state.results or {})
    local totalRows = #results
    local isLoading = initialDisplayPending or state.searching or state.processing or state.newbieSearching
    UpdateFindGroupScrollState(widgets, totalRows)

    local scrollFrame = widgets.findGroupScrollFrame
    local scrollValue = math.max(0, GetFindGroupScrollValue(widgets))
    local firstIndex = totalRows > 0 and (math.floor(scrollValue / ROW_HEIGHT) + 1) or 1
    firstIndex = math.min(math.max(firstIndex, 1), math.max(totalRows, 1))
    local visibleRows = math.ceil(math.max(scrollFrame:GetHeight() or ROW_HEIGHT, ROW_HEIGHT) / ROW_HEIGHT) + 1
    visibleRows = math.min(math.max(visibleRows, 1), FIND_GROUP_VISIBLE_DETAIL_ROWS + 1)
    local renderCount = totalRows > 0 and math.min(visibleRows, totalRows - firstIndex + 1) or 0
    EnsureRows(widgets, renderCount)

    local availableWidth = widgets.findGroupScrollFrame:GetWidth() - 12
    for index = 1, #widgets.findGroupRows do
        local row = widgets.findGroupRows[index]
        local resultIndex = firstIndex + index - 1
        local result = index <= renderCount and results[resultIndex] or nil
        if result then
            local previousResultID = tonumber(row.resultID)
            local currentResultID = tonumber(result.resultID)
            local wasShown = row.IsShown and row:IsShown()
            local animateAppearance = not wasShown
            if wasShown and previousResultID ~= currentResultID then
                StopAlphaAnimation(row, "appear", 1)
            end
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", widgets.findGroupRowsFrame, "TOPLEFT", 6, -((resultIndex - 1) * ROW_HEIGHT))
            row:SetPoint("TOPRIGHT", widgets.findGroupRowsFrame, "TOPRIGHT", -6, -((resultIndex - 1) * ROW_HEIGHT))
            RefreshRow(row, result, availableWidth, widgets, state, animateAppearance)
            SetFindGroupRowHover(row, row.IsMouseOver and row:IsMouseOver())
        else
            row:Hide()
            row.mkFindGroupResult = nil
            row.resultID = nil
            SetFindGroupRowHover(row, false)
            if row.widgets and row.widgets.cancelApplicationButtonHost then
                row.widgets.cancelApplicationButtonHost:Hide()
            end
            if row.widgets and row.widgets.cancelApplicationButton then
                row.widgets.cancelApplicationButton.resultID = nil
                row.widgets.cancelApplicationButton:Hide()
            end
            if row.widgets and row.widgets.applicationStatusIcon then
                row.widgets.applicationStatusIcon:Hide()
            end
            if row.widgets and row.widgets.applicationPendingSpinner then
                StopFindGroupPendingSpinner(row.widgets.applicationPendingSpinner)
            end
            StopAlphaAnimation(row, "appear", 1)
            StopAlphaAnimation(row, "remove", 1)
            row.mkFindGroupFadeKey = nil
            row.mkFindGroupRemovalResultID = nil
            row.mkFindGroupRemovalToken = nil
            row.mkFindGroupContextMenuLocked = nil
            row.mkFindGroupContextMenuToken = nil
        end
    end
    QueueVisibleResultDetails(owner, widgets, results)

    if totalRows > 0 then
        widgets.findGroupEmpty:Hide()
        SetFindGroupLoadingAnimationShown(widgets, false, false)
    else
        local showLoading = isLoading
        local emptyText = state.lastError
            or isLoading and TRANSLATE("队伍列表检索中")
            or state.searchCompletedEmpty == true and TRANSLATE("未检索到队伍")
            or TRANSLATE("点击搜索获取队伍列表")
        if core.ApplyPanelEmptyPromptStyle then
            core.ApplyPanelEmptyPromptStyle(widgets.findGroupEmpty)
        else
            widgets.findGroupEmpty:SetTextColor(EMPTY_TEXT_COLOR[1], EMPTY_TEXT_COLOR[2], EMPTY_TEXT_COLOR[3], EMPTY_TEXT_COLOR[4])
        end
        widgets.findGroupEmpty:SetText(emptyText)
        UpdateFindGroupEmptyLayout(widgets, showLoading)
        widgets.findGroupEmpty:Show()
        SetFindGroupLoadingAnimationShown(widgets, showLoading, showLoading)
    end
    widgets.mkFindGroupRenderingRows = nil
    return isLoading and totalRows <= 0
end

local function AddRightHeader(panel, owner)
    local background = panel:CreateTexture(nil, "BACKGROUND", nil, 1)
    background:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, HEADER_TOP_OFFSET)
    background:SetPoint("BOTTOMRIGHT", panel, "TOPRIGHT", 0, HEADER_BOTTOM_OFFSET)
    TrySetAtlas(background, "housefinder_header-bg-gradient", false)
    panel.HeaderBackground = background

    local header = CreateHeader(panel, owner)
    header:SetPoint("TOPLEFT", background, "TOPLEFT", HEADER_CONTENT_INSET_X, HEADER_CONTENT_OFFSET_Y)
    header:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", -HEADER_CONTENT_INSET_X, HEADER_CONTENT_OFFSET_Y)
    header:SetFrameLevel(panel:GetFrameLevel() + 3)
    panel.HeaderFrame = header
    ApplyHeaderLayout(header, math.max((panel:GetWidth() or 0) - HEADER_CONTENT_INSET_X * 2, 1))
end

local function SetFindGroupRefreshIconPressed(button, pressed)
    local icon = button and button.Icon
    if not icon then
        return
    end

    icon:ClearAllPoints()
    if pressed then
        icon:SetSize(HEADER_REFRESH_ICON_PRESSED_SIZE, HEADER_REFRESH_ICON_PRESSED_SIZE)
        icon:SetPoint("CENTER", button, "CENTER", 1, -1)
        icon:SetAlpha(0.86)
    else
        icon:SetSize(HEADER_REFRESH_ICON_SIZE, HEADER_REFRESH_ICON_SIZE)
        icon:SetPoint("CENTER", button, "CENTER", 0, 0)
        icon:SetAlpha(1)
    end
end

local function SetFindGroupRefreshButtonLoading(button, loading)
    if not button then
        return
    end

    if button.Icon then
        button.Icon:SetShown(not loading)
    end

    local spinner = loading and EnsureFindGroupButtonSpinner(button) or button.mkFindGroupButtonSpinner
    if loading then
        StartFindGroupPendingSpinner(spinner)
    else
        StopFindGroupPendingSpinner(spinner)
        SetFindGroupRefreshIconPressed(button, false)
    end
end

local function SetFindGroupRefreshButtonEnabled(button, enabled)
    if not button then
        return
    end
    if enabled then
        button:Enable()
    else
        button:Disable()
        SetFindGroupRefreshIconPressed(button, false)
    end
    if button.SetAlpha then
        button:SetAlpha(enabled and 1 or 0.45)
    end
end

local function ApplyCurrentFindGroupSearchText(owner)
    local input = owner and owner.widgets and owner.widgets.findGroupSearchTextBox
    if input and FindGroupSearch.SetSearchTextFilter then
        FindGroupSearch.SetSearchTextFilter(owner, input:GetText() or "")
    end
end

local function TriggerFindGroupListRefresh(owner)
    ApplyCurrentFindGroupSearchText(owner)
    local ok, reason = FindGroupSearch.Search(owner)
    if not ok and owner and owner.Print then
        owner:Print(reason)
    end
    if owner and owner.RefreshMainWindow then
        owner:RefreshMainWindow("FindGroupListRefresh")
    end
end

local function CreateFindGroupListRefreshButton(panel, owner, scrollBar)
    local button = CreateFrame("Button", nil, panel)
    button:SetSize(HEADER_REFRESH_BUTTON_SIZE, HEADER_REFRESH_BUTTON_SIZE)
    button:SetFrameLevel((panel:GetFrameLevel() or 0) + 6)
    button:RegisterForClicks("LeftButtonUp")
    if scrollBar then
        button:SetPoint("BOTTOM", scrollBar, "TOP", 0, HEADER_REFRESH_BUTTON_OFFSET_Y)
    else
        button:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -HEADER_CONTENT_INSET_X, HEADER_TOP_OFFSET)
    end

    local icon = button:CreateTexture(nil, "OVERLAY")
    icon:SetTexture(REFRESH_BUTTON_TEXTURE)
    icon:SetTexCoord(0, 1, 0, 1)
    button.Icon = icon
    SetFindGroupRefreshIconPressed(button, false)

    button:SetScript("OnClick", function()
        if core.PlayPanelButtonClickSound then
            core.PlayPanelButtonClickSound()
        end
        TriggerFindGroupListRefresh(owner)
    end)
    button:SetScript("OnEnter", function(hoveredButton)
        GameTooltip:SetOwner(hoveredButton, "ANCHOR_RIGHT")
        if GameTooltip_SetTitle then
            GameTooltip_SetTitle(GameTooltip, TRANSLATE("刷新队伍列表"))
        else
            GameTooltip:SetText(TRANSLATE("刷新队伍列表"), 1, 0.82, 0, true)
        end
        GameTooltip:AddLine(TRANSLATE("重新搜索寻找队伍列表"), 1, 1, 1, true)
        GameTooltip:Show()
    end)
    button:SetScript("OnMouseDown", function(clickedButton, mouseButton)
        if mouseButton == "LeftButton" then
            SetFindGroupRefreshIconPressed(clickedButton, true)
        end
    end)
    button:SetScript("OnMouseUp", function(clickedButton)
        SetFindGroupRefreshIconPressed(clickedButton, false)
    end)
    button:SetScript("OnLeave", function(hoveredButton)
        SetFindGroupRefreshIconPressed(hoveredButton, false)
        if GameTooltip:GetOwner() == hoveredButton then
            GameTooltip:Hide()
        end
    end)
    button:SetScript("OnDisable", function(disabledButton)
        SetFindGroupRefreshIconPressed(disabledButton, false)
    end)

    SetFindGroupRefreshButtonEnabled(button, FindGroupSearch.HasSelectedDungeons and FindGroupSearch.HasSelectedDungeons(owner))
    return button
end

local function CreateRightPanel(owner, container, leftPanel)
    local panel = CreateFrame("Frame", nil, container, "BackdropTemplate")
    panel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", RIGHT_PANEL_OVERLAP, 0)
    panel:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 0)

    local rightBackground = panel:CreateTexture(nil, "BACKGROUND")
    rightBackground:SetAllPoints(panel)
    rightBackground:SetTexture("Interface\\Buttons\\WHITE8X8")
    rightBackground:SetVertexColor(0, 0, 0, 1)
    rightBackground:Hide()
    panel.Background = rightBackground

    AddRightHeader(panel, owner)

    local scrollFrame, rowsFrame, scrollBar = CreateFindGroupScrollContainer(panel)
    scrollFrame:SetPoint("TOPLEFT", panel.HeaderBackground, "BOTTOMLEFT", 0, -4)
    scrollFrame:SetPoint(
        "BOTTOMRIGHT",
        panel,
        "BOTTOMRIGHT",
        -(APPLICANT_SCROLLBAR_WIDTH + APPLICANT_SCROLLBAR_GAP + APPLICANT_SCROLLBAR_RIGHT_INSET),
        APPLICANT_SCROLL_BOTTOM_INSET
    )
    scrollFrame:SetFrameLevel(panel:GetFrameLevel() + 2)
    rowsFrame:SetFrameLevel(scrollFrame:GetFrameLevel() + 1)
    scrollBar:SetFrameLevel(scrollFrame:GetFrameLevel() + 2)
    scrollBar:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", APPLICANT_SCROLLBAR_GAP, -2)
    scrollBar:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", APPLICANT_SCROLLBAR_GAP, 2)
    panel.RefreshButton = CreateFindGroupListRefreshButton(panel, owner, scrollBar)
    if scrollFrame.SetClipsChildren then
        scrollFrame:SetClipsChildren(true)
    end
    scrollFrame.mkOnScrollValueChanged = function()
        if owner.widgets and owner.activeTab == "findgroup" then
            if owner.widgets.mkFindGroupRenderingRows then
                return
            end
            RenderRows(owner, owner.widgets)
        end
    end

    local empty = rowsFrame:CreateFontString(nil, "OVERLAY", "GameFontDisable")
    empty:SetPoint("CENTER", rowsFrame, "CENTER", 0, 0)
    empty:SetJustifyH("CENTER")
    empty:SetJustifyV("MIDDLE")
    empty:SetText(TRANSLATE("点击搜索获取队伍列表"))
    if core.ApplyPanelEmptyPromptStyle then
        core.ApplyPanelEmptyPromptStyle(empty)
    end

    panel:SetScript("OnSizeChanged", function()
        if panel.HeaderFrame then
            local headerWidth = scrollFrame and scrollFrame.GetWidth and scrollFrame:GetWidth()
            if not headerWidth or headerWidth <= 0 then
                headerWidth = (panel:GetWidth() or 0) - (HEADER_CONTENT_INSET_X * 2)
            end
            ApplyHeaderLayout(panel.HeaderFrame, math.max(headerWidth - 12, 1))
        end
    end)
    scrollFrame:HookScript("OnSizeChanged", function()
        if owner.widgets and owner.activeTab == "findgroup" then
            ApplyHeaderLayout(panel.HeaderFrame, math.max(scrollFrame:GetWidth() - 12, 1))
            RenderRows(owner, owner.widgets)
        end
    end)

    local topRightFiligree = panel:CreateTexture(nil, "ARTWORK", nil, 5)
    topRightFiligree:SetPoint("TOPRIGHT", panel, "TOPRIGHT", TOP_RIGHT_FILIGREE_OFFSET_X, TOP_RIGHT_FILIGREE_OFFSET_Y)
    TrySetAtlas(topRightFiligree, "housing-dashboard-filigree-wood-small-TR", true)
    panel.TopRightFiligree = topRightFiligree

    local bottomRightFiligree = panel:CreateTexture(nil, "ARTWORK", nil, 5)
    bottomRightFiligree:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", BOTTOM_RIGHT_FILIGREE_OFFSET_X, BOTTOM_RIGHT_FILIGREE_OFFSET_Y)
    TrySetAtlas(bottomRightFiligree, "housing-dashboard-filigree-wood-small-BR", true)
    panel.BottomRightFiligree = bottomRightFiligree

    return panel, {
        findGroupHeader = panel.HeaderFrame,
        findGroupScrollFrame = scrollFrame,
        findGroupRowsFrame = rowsFrame,
        findGroupScrollBar = scrollBar,
        findGroupListRefreshButton = panel.RefreshButton,
        findGroupEmpty = empty,
        findGroupRows = {},
    }
end

function FindGroupTab.HandleRowClick(row)
    local owner = MK
    local result = row and row.mkFindGroupResult
    if not (owner and result) then
        return
    end
    local settings = FindGroupSearch.GetSettings(owner) or {}
    if settings.quickJoin ~= true then
        return
    end
    local now = GetTime and GetTime() or time()
    if owner.mkFindGroupLastClickedResultID == result.resultID and now - (owner.mkFindGroupLastClickAt or 0) <= 0.35 then
        if FindGroupSearch.CanApplyToResult and not FindGroupSearch.CanApplyToResult(owner, result.resultID) then
            owner.mkFindGroupLastClickedResultID = nil
            owner.mkFindGroupLastClickAt = nil
            return
        end
        local ok, reason = FindGroupSearch.ApplyToResult(owner, result.resultID)
        if ok then
            owner:RefreshMainWindow("FindGroupApply", tostring(result.resultID))
        elseif reason and owner.Print then
            owner:Print(reason)
        end
    end
    owner.mkFindGroupLastClickedResultID = result.resultID
    owner.mkFindGroupLastClickAt = now
end

function FindGroupTab.ShowContextMenu(row)
    local owner = MK
    local result = row and row.mkFindGroupResult
    if not (owner and result and MenuUtil) then
        return
    end
    local menuToken = LockFindGroupRowContextMenu(row)
    local menu = MenuUtil.CreateContextMenu(row, function(_, rootDescription)
        rootDescription:SetTag("MENU_MK_FIND_GROUP_RESULT")
        rootDescription:CreateTitle(result.title or "")
        local canApply = not FindGroupSearch.CanApplyToResult or FindGroupSearch.CanApplyToResult(owner, result.resultID)
        local applyText = canApply
            and (APPLICATION_MENU_APPLY_COLOR_CODE .. TRANSLATE("申请加入") .. "|r")
            or TRANSLATE("申请加入")
        local applyButton = rootDescription:CreateButton(applyText, function()
            local ok, reason = FindGroupSearch.OpenApplicationDialog(owner, result.resultID)
            if not ok and owner.Print then
                owner:Print(reason)
            end
        end)
        if not canApply and applyButton and applyButton.SetEnabled then
            applyButton:SetEnabled(false)
        end
        rootDescription:CreateButton(TRANSLATE("与队长密聊"), function()
            if result.leaderName and ChatFrameUtil and ChatFrameUtil.SendTell then
                ChatFrameUtil.SendTell(result.leaderName)
            elseif result.leaderName and ChatFrame_SendTell then
                ChatFrame_SendTell(result.leaderName)
            end
        end)
        rootDescription:CreateButton(TRANSLATE("举报广告"), function()
            local ok = FindGroupSearch.ReportAdvertisement and FindGroupSearch.ReportAdvertisement(owner, result)
            if not ok and owner.Print then
                owner:Print(TRANSLATE("举报接口不可用。"))
            end
        end)
        rootDescription:CreateButton(TRANSLATE("屏蔽同标题广告菜单"), function()
            FindGroupSearch.BlockTitle(owner, result.title, result)
            owner:RefreshMainWindow("FindGroupBlockTitle")
        end)
        local blacklistButton = rootDescription:CreateButton(BLACKLIST_MENU_ICON_MARKUP .. BLACKLIST_MENU_COLOR_CODE .. TRANSLATE("加入黑名单") .. "|r", function()
            if owner.AddPlayerToBlacklist and result.leaderName then
                local ok, record = owner:AddPlayerToBlacklist(
                    result.leaderName,
                    nil,
                    owner.GetBlacklistNoteManual and owner:GetBlacklistNoteManual() or "手动拉黑",
                    "findgroup_menu"
                )
                if ok and FindGroupSearch.RefreshBlacklistFilter then
                    FindGroupSearch.RefreshBlacklistFilter(owner)
                end
                if ok and owner.PrintBlacklistAdded then
                    owner:PrintBlacklistAdded(record)
                end
                owner:RefreshMainWindow("FindGroupBlacklistLeader")
            end
        end)
        if not result.leaderName and blacklistButton and blacklistButton.SetEnabled then
            blacklistButton:SetEnabled(false)
        end
    end)
    if menu and menu.SetClosedCallback then
        menu:SetClosedCallback(function()
            FinishFindGroupRowContextMenu(row, menuToken)
        end)
    else
        FinishFindGroupRowContextMenu(row, menuToken)
    end
end

function FindGroupTab.ShowTooltip(row)
    local result = row and row.mkFindGroupResult
    if not result then
        return
    end
    if FindGroupSearch.EnsureResultDetails then
        result = FindGroupSearch.EnsureResultDetails(MK, result.resultID) or result
        row.mkFindGroupResult = result
        if result.detailLoaded == true then
            local applicationState = FindGroupSearch.GetApplicationDisplayState and FindGroupSearch.GetApplicationDisplayState(result) or nil
            SetResultType(row, FindGroupSearch.GetResultType(result), applicationState)
            SetRoleEntries(row, result.roleEntries)
        end
    end

    local tooltip = GameTooltip
    tooltip:SetOwner(row, "ANCHOR_RIGHT")
    tooltip:ClearLines()
    tooltip:AddLine(result.title or "-", 1, 1, 1)
    tooltip:AddLine(result.dungeonName or "-", 0.2, 1, 0.2)
    tooltip:AddLine(" ")
    tooltip:AddDoubleLine(TRANSLATE("队长："), ColorText(result.leaderDisplayName or "-", GetClassColor(result.leaderClassFile)), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("队长大秘评分："), ColorText(math.floor((tonumber(result.score) or 0) + 0.5), GetScoreColor(result.score)), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("队员："), string.format("%d (%d/%d/%d)", result.totalMembers or 0, tonumber(result.counts.TANK) or 0, tonumber(result.counts.HEALER) or 0, tonumber(result.counts.DAMAGER) or 0), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("空位："), tostring(result.availableSlots or 0), 1, 0.82, 0, 1, 1, 1)
    if tonumber(result.requiredItemLevel) and result.requiredItemLevel > 0 then
        tooltip:AddDoubleLine(TRANSLATE("需要物品等级："), tostring(math.floor(result.requiredItemLevel + 0.5)), 1, 0.82, 0, 1, 1, 1)
    end
    if tonumber(result.age) then
        tooltip:AddDoubleLine(TRANSLATE("创建于："), string.format(TRANSLATE("%d 分钟前"), math.max(0, math.floor(result.age / 60))), 1, 0.82, 0, 1, 1, 1)
    end

    local newbieNames = {}
    local leaverNames = {}
    local leaderNames = {}
    local starNames = {}
    if #result.members > 0 then
        tooltip:AddLine(" ")
        tooltip:AddLine(TRANSLATE("队伍成员"), 1, 0.82, 0)
        local shownMembers = 0
        for _, member in ipairs(result.members) do
            local memberName = member.displayName or member.name
            if not IsUnknownMemberName(memberName) then
                local roleKey = NormalizeTooltipRoleKey(member.role)
                local rolePrefix = GetTooltipRoleIconMarkup(member.role) or TRANSLATE(ROLE_TEXT_KEY_BY_ROLE[roleKey] or ROLE_TEXT_KEY_BY_ROLE.UNKNOWN)
                local classColor = GetClassColor(member.classFile)
                local formattedMemberName = IsFindGroupLeaderMember(result, member)
                    and FormatTooltipTeamLeaderName(memberName, classColor)
                    or ColorText(memberName, classColor)
                local specText = member.specName and member.specName ~= "" and ColorText(member.specName, classColor) or ""
                tooltip:AddDoubleLine(string.format("%s%s%s", rolePrefix, TOOLTIP_ICON_TEXT_GAP, formattedMemberName), specText, 1, 1, 1, 1, 1, 1)
                shownMembers = shownMembers + 1
                local formattedName = GetTooltipMemberName(member)
                if member.isLeaver == true and formattedName then
                    leaverNames[#leaverNames + 1] = formattedName
                end
                if member.isNewbie == true and formattedName then
                    newbieNames[#newbieNames + 1] = formattedName
                end
                if member.isAccountTypeLeader == true and formattedName then
                    leaderNames[#leaderNames + 1] = formattedName
                end
                if member.isAccountTypeStar == true and formattedName then
                    starNames[#starNames + 1] = formattedName
                end
            end
        end
        if shownMembers <= 0 then
            tooltip:AddLine(TRANSLATE("成员信息加载中"), 0.7, 0.7, 0.7, true)
        end
    elseif result.detailLoaded ~= true and (tonumber(result.totalMembers) or 0) > 0 then
        tooltip:AddLine(" ")
        tooltip:AddLine(TRANSLATE("成员信息加载中"), 0.7, 0.7, 0.7, true)
    end
    local resultType = FindGroupSearch.GetResultType(result)
    local hasTypeWarning = false
    if resultType == "leaver" or #leaverNames > 0 then
        tooltip:AddLine(" ")
        hasTypeWarning = AddTypeMemberTooltipLine(
            tooltip,
            LEAVER_ICON_TEXTURE,
            TRANSLATE("逃兵"),
            #leaverNames > 0 and leaverNames or { TRANSLATE("成员信息加载中") },
            1,
            0.08,
            0.05
        )
    end
    if result.hasNewbie == true or resultType == "newbie" then
        if not hasTypeWarning then
            tooltip:AddLine(" ")
            hasTypeWarning = true
        end
        AddTypeMemberTooltipLine(
            tooltip,
            NEWBIE_ICON_TEXTURE,
            TRANSLATE("新兵"),
            #newbieNames > 0 and newbieNames or { TRANSLATE("成员信息加载中") },
            0.12,
            1,
            0.25
        )
    end
    if result.hasAccountTypeLeader == true or resultType == "leader" then
        if not hasTypeWarning then
            tooltip:AddLine(" ")
            hasTypeWarning = true
        end
        AddTypeMemberTooltipLine(
            tooltip,
            LEADER_ICON_TEXTURE,
            TRANSLATE("火车头"),
            #leaderNames > 0 and leaderNames or { TRANSLATE("成员信息加载中") },
            1,
            0.82,
            0
        )
    end
    if result.hasAccountTypeStar == true or resultType == "star" then
        if not hasTypeWarning then
            tooltip:AddLine(" ")
            hasTypeWarning = true
        end
        AddTypeMemberTooltipLine(
            tooltip,
            STAR_ICON_TEXTURE,
            TRANSLATE("星团长"),
            #starNames > 0 and starNames or { TRANSLATE("成员信息加载中") },
            1,
            0.82,
            0
        )
    end
    if result.comment and result.comment ~= "" then
        tooltip:AddLine(" ")
        tooltip:AddLine(result.comment, GREEN_FONT_COLOR.r, GREEN_FONT_COLOR.g, GREEN_FONT_COLOR.b, true)
    end
    tooltip:Show()
end

function FindGroupList.CreateRightPanel(owner, container, leftPanel)
    return CreateRightPanel(owner, container, leftPanel)
end

function FindGroupList.Refresh(owner, widgets)
    if not widgets then
        return
    end
    if widgets.findGroupHeader and widgets.findGroupScrollFrame then
        RefreshHeaderSortState(widgets.findGroupHeader, owner)
        ApplyHeaderLayout(widgets.findGroupHeader, math.max(widgets.findGroupScrollFrame:GetWidth() - 12, 1))
    end
    SetFindGroupRefreshButtonEnabled(
        widgets.findGroupListRefreshButton,
        FindGroupSearch.HasSelectedDungeons and FindGroupSearch.HasSelectedDungeons(owner)
    )
    local buttonLoading = RenderRows(owner, widgets)
    SetFindGroupSearchButtonLoading(widgets.findGroupSearchButton, buttonLoading == true)
    SetFindGroupRefreshButtonLoading(widgets.findGroupListRefreshButton, buttonLoading == true)
end
