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

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 GROUP_LIST_PANEL_PADDING_TOP = 8
local GROUP_LIST_PANEL_PADDING_BOTTOM = 10
local GROUP_LIST_HEADER_HEIGHT = core.GROUP_LIST_HEADER_HEIGHT or 36
local GROUP_LIST_HEADER_ROW_GAP = 6
local GROUP_LIST_ROWS_PADDING_X = core.GROUP_LIST_ROWS_PADDING_X or 4
local GROUP_LIST_ROWS_PADDING_TOP = 4
local GROUP_LIST_ROWS_PADDING_BOTTOM = 4
local GROUP_LIST_ROW_HEIGHT = core.GROUP_LIST_ROW_HEIGHT or 36
local GROUP_LIST_MEETINGSTONE_BUTTON_WIDTH = 72
local GROUP_LIST_MEETINGSTONE_BUTTON_HEIGHT = 26
local GROUP_LIST_MEETINGSTONE_BUTTON_TEXT_OFFSET_Y = 0
local GROUP_LIST_MEETINGSTONE_BUTTON_PRESSED_TEXT_OFFSET_X = 1
local GROUP_LIST_MEETINGSTONE_BUTTON_PRESSED_TEXT_OFFSET_Y = -1
local GROUP_LIST_EMPTY_MIN_HEIGHT = 96
local GROUP_LIST_SCROLLBAR_SLOT_WIDTH = 17
local GROUP_LIST_SCROLLBAR_SLOT_RIGHT_OFFSET = 17
local GROUP_LIST_SCROLLBAR_SLOT_VERTICAL_INSET = 4
local GROUP_LIST_RATING_PENDING_SECONDS = 8
local GROUP_LIST_RATING_PENDING_SPINNER_SIZE = 16
local GROUP_LIST_UNIT_RATING_CACHE_TTL_SECONDS = 120
local GROUP_LIST_UNIT_RATING_FAILED_TTL_SECONDS = 60
local GROUP_LIST_UNIT_RATING_BATCH_SIZE = 2
local GROUP_LIST_UNIT_RATING_BATCH_DELAY_SECONDS = 0.08
local GROUP_LIST_ACCOUNT_TYPE_ICON_INSET_X = 20
local GROUP_LIST_ROW_TEXTURE_NORMAL = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowNormal.png"
local GROUP_LIST_ROW_TEXTURE_GREY = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowGrey.png"
local MAIN_PANEL_CONTENT_PADDING_X = 18
local GROUP_LIST_RATING_FAILED_TEXT_COLOR = { 0.52, 0.52, 0.52, 1 }
local GROUP_LIST_SPEC_ICON_SIZE = 16
local GROUP_LIST_SPEC_ICON_GAP = 4
local GROUP_LIST_ROW_HOVER_COLOR_NORMAL = { 1, 0.74, 0.18, 0.13 }
local GROUP_LIST_ROW_HOVER_COLOR_GREY = { 0.65, 0.65, 0.65, 0.18 }

local DungeonTeleport = core.DungeonTeleport or {}
local AddDungeonTeleportTooltipLine = DungeonTeleport.AddTeleportTooltipLine or function() end
local AttachDungeonTeleportDialogTrigger = DungeonTeleport.AttachDialogTrigger or function() end
local GetDungeonTeleportMacroText = DungeonTeleport.GetMacroText or function() return nil end
local GetDungeonTeleportStatus = DungeonTeleport.GetStatus or function() return nil end
local SetSecureButtonAttributes = core.SetSecureButtonAttributes or function(button, ...)
    if not (button and button.SetAttribute) then
        return false
    end

    local count = select("#", ...)
    for index = 1, count, 2 do
        local key = select(index, ...)
        if key ~= nil then
            local value = select(index + 1, ...)
            button:SetAttribute(key, value)
        end
    end
    return true
end
local ApplyDungeonTeleportButtonSecureState = DungeonTeleport.ApplyButtonSecureState or function(button, canUseTeleport, teleportMacroText, mapID)
    local canUse = canUseTeleport == true and type(teleportMacroText) == "string" and teleportMacroText ~= ""
    SetSecureButtonAttributes(
        button,
        "type", canUse and "macro" or nil,
        "type1", canUse and "macro" or nil,
        "macrotext", canUse and teleportMacroText or nil,
        "macrotext1", canUse and teleportMacroText or nil,
        "spell", nil,
        "spell1", nil
    )
    if button then
        button.mkTeleportReady = canUse
        button.mkTeleportEffectiveMapID = mapID
    end
    return canUse, false, mapID
end
local TrySetAtlas = core.TrySetAtlas or function(texture, atlas, useAtlasSize)
    if texture and texture.SetAtlas and atlas then
        local ok = pcall(texture.SetAtlas, texture, atlas, useAtlasSize)
        return ok == true
    end
    return false
end
local HideKeystoneTooltip = core.HideKeystoneTooltip or function()
    GameTooltip:Hide()
end
local ApplyGroupHeaderButtonStyle = core.ApplyGroupHeaderButtonStyle or function() end
local GetGroupListKind = core.GetGroupListKind or function(listKind)
    return listKind == "carpool" and "carpool" or "group"
end
local GetGroupSortState = core.GetGroupSortState or function(owner)
    return owner and owner.groupSort or core.GROUP_SORT_DEFAULT or { key = "roster", direction = "asc" }
end
local GetGroupSortConfig = core.GetGroupSortConfig or function(listKind)
    if GetGroupListKind(listKind) == "carpool" then
        return core.CARPOOL_SORT_DEFAULT or core.GROUP_SORT_DEFAULT,
            core.CARPOOL_SORT_INITIAL_DIRECTION or core.GROUP_SORT_INITIAL_DIRECTION,
            "carpoolSort"
    end

    return core.GROUP_SORT_DEFAULT, core.GROUP_SORT_INITIAL_DIRECTION, "groupSort"
end
local GetWarbandSourceDisplayName = core.GetWarbandSourceDisplayName or function(characterEntry)
    return characterEntry and (characterEntry.warbandSourceName or characterEntry.owner) or ""
end
local GetWarbandSourceDisplayInfo = core.GetWarbandSourceDisplayInfo or function(characterEntry)
    return GetWarbandSourceDisplayName(characterEntry), characterEntry and characterEntry.warbandSourceClass or nil
end
local GetShortNameFromCharacterKey = core.GetShortNameFromCharacterKey or function(characterKey)
    return type(characterKey) == "string" and (characterKey:match("^([^-]+)") or characterKey) or nil
end
local GetCurrentPlayerShortName = core.GetCurrentPlayerShortName or function()
    return UnitName and UnitName("player") or nil
end
local GetCurrentPlayerOwnerKey = core.GetCurrentPlayerOwnerKey or function()
    local name = UnitName and UnitName("player") or nil
    local realm = GetRealmName and GetRealmName() or nil
    if type(name) == "string" and name ~= "" and type(realm) == "string" and realm ~= "" then
        return string.format("%s-%s", name, realm)
    end
    return name
end
local GetCharacterIdentifierAliases = core.GetCharacterIdentifierAliases or function()
    return {}
end
local FindGroupRosterMemberForEntry = core.FindGroupRosterMemberForEntry or function()
    return nil
end

local function GetGroupRatingLookupNow()
    if GetTime then
        return GetTime()
    end
    return time and time() or 0
end

local function GetGroupRatingLookupKey(characterEntry)
    local character = characterEntry and characterEntry.data or {}
    return table.concat({
        tostring(characterEntry and characterEntry.owner or ""),
        tostring(characterEntry and characterEntry.key or ""),
        tostring(character.name or ""),
        tostring(character.realm or ""),
    }, "~")
end

local function GetGroupRatingEntrySource(characterEntry)
    local character = characterEntry and characterEntry.data or {}
    return (characterEntry and characterEntry.source) or character.source
end

local function GetPositiveGroupRating(value)
    local score = tonumber(value)
    if score and score > 0 then
        return score
    end
    return nil
end

local function HasExplicitNoGroupRating(value)
    local score = tonumber(value)
    return score ~= nil and score <= 0
end

local function GroupRatingIdentifierMatches(left, right)
    if type(left) ~= "string" or type(right) ~= "string" then
        return false
    end

    local rightAliases = GetCharacterIdentifierAliases(right)
    for alias in pairs(GetCharacterIdentifierAliases(left)) do
        if rightAliases[alias] then
            return true
        end
    end

    return false
end

local function GroupActionEntryMatchesIdentifier(characterEntry, identifier)
    if not characterEntry or type(identifier) ~= "string" or identifier == "" then
        return false
    end

    local identifierAliases = GetCharacterIdentifierAliases(identifier)
    local character = characterEntry.data or {}
    local candidates = {
        character.name,
        characterEntry.key,
        characterEntry.owner,
    }

    for _, candidate in ipairs(candidates) do
        for alias in pairs(GetCharacterIdentifierAliases(candidate)) do
            if identifierAliases[alias] then
                return true
            end
        end
    end

    return false
end

local function IsCurrentPlayerQuickActionEntry(characterEntry)
    if not characterEntry then
        return false
    end

    return GroupActionEntryMatchesIdentifier(characterEntry, GetCurrentPlayerOwnerKey())
        or GroupActionEntryMatchesIdentifier(characterEntry, GetCurrentPlayerShortName())
        or GroupActionEntryMatchesIdentifier(characterEntry, UnitName and UnitName("player"))
        or GroupActionEntryMatchesIdentifier(characterEntry, GetUnitName and GetUnitName("player", true))
end

local function GroupRatingEntryMatchesRosterMember(characterEntry, member)
    if not (characterEntry and member) then
        return false
    end

    local character = characterEntry.data or {}
    local candidates = {
        character.name,
        characterEntry.key,
    }
    local memberIdentifiers = {
        member.fullName,
        member.name,
    }

    for _, candidate in ipairs(candidates) do
        for _, memberIdentifier in ipairs(memberIdentifiers) do
            if GroupRatingIdentifierMatches(candidate, memberIdentifier) then
                return true
            end
        end
    end

    return false
end

local function ResolveGroupUnitRatingMember(owner, characterEntry)
    if not (owner and characterEntry) then
        return nil
    end

    local member = FindGroupRosterMemberForEntry(characterEntry)
    if not (member and type(member.unit) == "string" and member.unit ~= "" and member.fullName) then
        return nil
    end
    if member.connected == false then
        return nil
    end
    if UnitExists and not UnitExists(member.unit) then
        return nil
    end
    if UnitIsConnected and UnitIsConnected(member.unit) == false then
        return nil
    end
    if not GroupRatingEntryMatchesRosterMember(characterEntry, member) then
        return nil
    end

    return member
end

local function IsGroupRatingLikelyWaitingForData(owner, characterEntry, displayState)
    local character = characterEntry and characterEntry.data or {}
    if displayState and displayState.isOffline then
        return false
    end

    local source = characterEntry and characterEntry.source or character.source
    local hasCharacterScore = GetPositiveGroupRating(character.rating) ~= nil
    if (source == "mk" or source == "libkeystone" or source == "libopenkeystone") and hasCharacterScore then
        return false
    end

    -- 评分列只在确实可以通过队伍 unit token 查询时显示等待态；没有可查来源时显示无来源失败，避免一直转圈。
    if owner and owner.CanLookupGroupUnitRating and owner:CanLookupGroupUnitRating(characterEntry) then
        return true
    end

    return false
end

local function CreateFallbackGroupRatingSpinner(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 GetGroupRatingPendingSpinner(row)
    if not (row and row.widgets and CreateFrame) then
        return nil
    end

    local spinner = row.widgets.ratingPendingSpinner
    if spinner then
        return spinner
    end

    local ok, nativeSpinner = pcall(CreateFrame, "Frame", nil, row, "SpinnerTemplate")
    spinner = ok and nativeSpinner or CreateFallbackGroupRatingSpinner(row)
    if not spinner then
        return nil
    end

    if spinner.SetFrameLevel and row.GetFrameLevel then
        spinner:SetFrameLevel((row:GetFrameLevel() or 0) + 4)
    end
    spinner:Hide()
    row.widgets.ratingPendingSpinner = spinner
    return spinner
end

local function StopGroupRatingPendingAnimation(row)
    local spinner = row and row.widgets and row.widgets.ratingPendingSpinner
    if not spinner then
        return
    end

    if spinner.Anim and spinner.Anim.Stop then
        spinner.Anim:Stop()
    end
    spinner:Hide()
end

local function StartGroupRatingPendingAnimation(row, label)
    local spinner = GetGroupRatingPendingSpinner(row)
    if not spinner then
        if label then
            label:SetText("")
        end
        return false
    end

    if label then
        label:SetText("")
    end
    spinner:ClearAllPoints()
    spinner:SetSize(GROUP_LIST_RATING_PENDING_SPINNER_SIZE, GROUP_LIST_RATING_PENDING_SPINNER_SIZE)
    spinner:SetPoint("CENTER", label or row, "CENTER", 0, 0)
    spinner:Show()
    if spinner.Anim and spinner.Anim.Play and (not spinner.Anim.IsPlaying or not spinner.Anim:IsPlaying()) then
        spinner.Anim:Play()
    end
    return true
end

local function UpdateNativeScrollState(scrollFrame, content, scrollBar)
    if not scrollFrame or not content or not scrollBar then
        return
    end

    if scrollFrame.UpdateScrollChildRect then
        scrollFrame:UpdateScrollChildRect()
    end

    local viewHeight = scrollFrame:GetHeight() or 0
    local contentHeight = content:GetHeight() or 0
    local maxScroll = scrollFrame.GetVerticalScrollRange and scrollFrame:GetVerticalScrollRange() or math.max(contentHeight - viewHeight, 0)
    local currentValue = math.min(scrollBar:GetValue() or 0, maxScroll)

    scrollBar:SetMinMaxValues(0, maxScroll)
    scrollBar:SetValue(currentValue)

    if maxScroll > 0 then
        scrollBar:Show()
    else
        scrollBar:Hide()
        scrollFrame:SetVerticalScroll(0)
    end

    content:SetWidth(math.max((scrollFrame:GetWidth() or 0), 1))
end

local function CreateAtlasDisplay(parent, width, size, atlasName)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetSize(width, size)

    local icon = frame:CreateTexture(nil, "OVERLAY")
    icon:SetAllPoints(frame)
    if atlasName and icon.SetAtlas then
        icon:SetAtlas(atlasName, false)
    end
    icon:Hide()

    frame.icon = icon
    return frame
end

local function ApplyGroupMeetingStoneButtonStyle(button)
    if not button or button.mkGroupMeetingStoneButtonStyled then
        return
    end

    local meetingStoneForm = core.MeetingStoneForm
    if meetingStoneForm and meetingStoneForm.ApplySystemButtonStyle then
        meetingStoneForm.ApplySystemButtonStyle(button)
    else
        if button.SetNormalFontObject then
            button:SetNormalFontObject("GameFontNormalSmall")
        end
        if button.SetHighlightFontObject then
            button:SetHighlightFontObject("GameFontHighlightSmall")
        end
        if button.SetDisabledFontObject then
            button:SetDisabledFontObject("GameFontDisableSmall")
        end
        if core.ApplyFont then
            local fontString = button.Text or (button.GetFontString and button:GetFontString())
            core.ApplyFont(fontString, 12, "")
        end
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Apply then
            core.MeetingStoneButtonArt.Apply(button)
        end
    end

    if meetingStoneForm and meetingStoneForm.SetSystemButtonTextOffset then
        meetingStoneForm.SetSystemButtonTextOffset(button, GROUP_LIST_MEETINGSTONE_BUTTON_TEXT_OFFSET_Y)
    else
        local fontString = button.Text or (button.GetFontString and button:GetFontString())
        if fontString then
            fontString:ClearAllPoints()
            fontString:SetPoint("CENTER", button, "CENTER", 0, GROUP_LIST_MEETINGSTONE_BUTTON_TEXT_OFFSET_Y)
        end
    end

    button.mkGroupMeetingStoneButtonStyled = true
end

local function SetGroupMeetingStoneButtonTextPressed(button, pressed)
    local fontString = button and (button.Text or (button.GetFontString and button:GetFontString()))
    if not fontString then
        return
    end

    fontString:ClearAllPoints()
    fontString:SetPoint(
        "CENTER",
        button,
        "CENTER",
        pressed and GROUP_LIST_MEETINGSTONE_BUTTON_PRESSED_TEXT_OFFSET_X or 0,
        pressed and GROUP_LIST_MEETINGSTONE_BUTTON_PRESSED_TEXT_OFFSET_Y or GROUP_LIST_MEETINGSTONE_BUTTON_TEXT_OFFSET_Y
    )
    if fontString.SetJustifyH then
        fontString:SetJustifyH("CENTER")
    end
    if fontString.SetJustifyV then
        fontString:SetJustifyV("MIDDLE")
    end
end

local function LayoutGroupMeetingStoneButton(button, row, column)
    if not button or not row or not column then
        return
    end

    button:ClearAllPoints()
    button:SetPoint("CENTER", row, "LEFT", (column.x or 0) + ((column.width or 0) / 2), 0)
    button:SetSize(GROUP_LIST_MEETINGSTONE_BUTTON_WIDTH, GROUP_LIST_MEETINGSTONE_BUTTON_HEIGHT)
end

local function GetGroupHeaderText(owner, key, text)
    if text == "" then
        return ""
    end

    return TRANSLATE(text)
end

local function UpdateGroupHeaderSortIndicators(owner, headerFrame)
    if not headerFrame or not headerFrame.sortButtons then
        return
    end

    local sortState = GetGroupSortState(owner, headerFrame.mkGroupListKind)

    for key, button in pairs(headerFrame.sortButtons) do
        if button and button.label and button.sortText then
            local isActive = sortState.key == key
            button.label:SetText(GetGroupHeaderText(owner, key, button.sortText))
            if button.arrow then
                button.arrow:SetShown(isActive)
                if isActive then
                    if sortState.direction == "asc" then
                        button.arrow:SetTexCoord(0, 1, 1, 0)
                    else
                        button.arrow:SetTexCoord(0, 1, 0, 1)
                    end
                end
            end
            if button.mkTransmogHeaderBackground then
                button.mkTransmogHeaderBackground:SetAlpha(isActive and 1 or 0.9)
                button.mkTransmogHeaderBackground:SetVertexColor(isActive and 1 or 0.92, isActive and 1 or 0.92, isActive and 1 or 0.92, 1)
            end
            button.label:SetTextColor(1, isActive and 1 or 0.82, isActive and 1 or 0, 1)
        end
    end
end

local function CreateGroupHeaderFrame(parent, options)
    options = options or {}

    local frame = CreateFrame("Frame", nil, parent)
    frame:SetHeight(GROUP_LIST_HEADER_HEIGHT)
    frame:Hide()
    frame.mkGroupListKind = GetGroupListKind(options.tabKey)

    local columnWidths = options.columnWidths or core.GROUP_LIST_COLUMN_WIDTHS
    local _, sortInitialDirection = GetGroupSortConfig(frame.mkGroupListKind)
    local labels = {
        { key = "name", text = "角色", width = columnWidths.name },
        { key = "armor", text = "护甲", width = columnWidths.armor },
        { key = "key", text = "钥石", width = columnWidths.key },
        { key = "rating", text = "评分", width = columnWidths.rating },
        { key = "roles", text = "职责", width = columnWidths.roles },
        { key = "tp", text = "传送", width = columnWidths.tp },
        { key = "accountType", text = "账号类型", width = columnWidths.accountType },
        { key = "meetingStone", text = options.lastColumnText or "快捷操作", width = columnWidths.meetingStone },
    }

    local previous
    frame.sortButtons = {}
    for _, data in ipairs(labels) do
        local label = CreateFrame("Button", nil, frame)
        label:SetSize(data.width, GROUP_LIST_HEADER_HEIGHT)
        label.sortKey = data.key
        label.sortText = data.text
        label.mkGroupListKind = frame.mkGroupListKind
        ApplyGroupHeaderButtonStyle(label)

        local text = label.Text or label:GetFontString()
        if not text then
            text = label:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            label:SetFontString(text)
        end
        text:ClearAllPoints()
        if sortInitialDirection[data.key] then
            text:SetPoint("LEFT", label, "LEFT", 24, 0)
            text:SetPoint("RIGHT", label, "RIGHT", -24, 0)
        else
            text:SetPoint("LEFT", label, "LEFT", 0, 0)
            text:SetPoint("RIGHT", label, "RIGHT", 0, 0)
        end
        text:SetJustifyH("CENTER")
        text:SetJustifyV("MIDDLE")
        text:SetText(GetGroupHeaderText(MK, data.key, data.text))
        if core.ApplyFont then
            core.ApplyFont(text, 13, "")
        end
        text:SetTextColor(1, 0.82, 0, 1)
        label.label = text

        local arrow = label.Arrow
        if not arrow then
            arrow = label:CreateTexture(nil, "OVERLAY")
            TrySetAtlas(arrow, "auctionhouse-ui-sortarrow", true)
        end
        arrow:ClearAllPoints()
        arrow:SetPoint("RIGHT", label, "RIGHT", -9, 0)
        arrow:Hide()
        label.arrow = arrow

        if sortInitialDirection[data.key] then
            label:SetScript("OnClick", function(button)
                if PlaySound and _G.SOUNDKIT and _G.SOUNDKIT.UI_TRANSMOG_ITEM_CLICK then
                    pcall(PlaySound, _G.SOUNDKIT.UI_TRANSMOG_ITEM_CLICK)
                end
                MK:SetGroupSort(button.sortKey, button.mkGroupListKind)
            end)
            label:SetScript("OnEnter", function(button)
                if button.label then
                    button.label:SetTextColor(1, 1, 1, 1)
                end
            end)
            label:SetScript("OnLeave", function(button)
                UpdateGroupHeaderSortIndicators(MK, frame)
            end)
            frame.sortButtons[data.key] = label
        else
            label:EnableMouse(false)
        end

        if previous then
            label:SetPoint("LEFT", previous, "RIGHT", 0, 0)
        else
            label:SetPoint("LEFT", frame, "LEFT", 0, 0)
        end
        previous = label
        frame[data.key] = label
    end

    MK:ApplyGroupHeaderColumnLayout(frame, columnWidths)
    UpdateGroupHeaderSortIndicators(MK, frame)

    return frame
end
core.CreateGroupHeaderFrame = CreateGroupHeaderFrame

function MK:ApplyGroupHeaderColumnLayout(headerFrame, widths)
    if not headerFrame then
        return
    end

    local layout = core.GetGroupColumnLayout(widths)
    for key in pairs(layout.widths or {}) do
        local button = headerFrame[key]
        local column = layout[key]
        if button and column then
            button:ClearAllPoints()
            button:SetPoint("LEFT", headerFrame, "LEFT", column.x or 0, 0)
            button:SetSize(column.width or 0, GROUP_LIST_HEADER_HEIGHT)
        end
    end
end

function MK:SetGroupRowHover(row, shown)
    if not row then
        return
    end

    if row.HighlightTexture then
        row.HighlightTexture:Hide()
    end

    if row.widgets and row.widgets.hoverLeft then
        row.widgets.hoverLeft:SetShown(shown == true)
    end
    if row.widgets and row.widgets.hover then
        row.widgets.hover:SetShown(shown == true)
    end
    if row.widgets and row.widgets.hoverRight then
        row.widgets.hoverRight:SetShown(shown == true)
    end
end

local function SetGroupRowHoverColor(row, color)
    if not (row and row.widgets) then
        return
    end

    color = color or GROUP_LIST_ROW_HOVER_COLOR_NORMAL
    local r = color[1] or 1
    local g = color[2] or 0.74
    local b = 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

function MK:SetTeleportButtonVisualState(button, state)
    if not button then
        return
    end

    if button.mkTeleportIcon then
        local iconOffsetX = 0
        local iconOffsetY = 0
        if state == "pushed" then
            iconOffsetX = iconOffsetX + 1
            iconOffsetY = iconOffsetY - 1
        end
        button.mkTeleportIcon:ClearAllPoints()
        button.mkTeleportIcon:SetPoint("CENTER", button, "CENTER", iconOffsetX, iconOffsetY)
        button.mkTeleportIcon:SetSize(core.GROUP_LIST_STATUS_ICON_SIZE, core.GROUP_LIST_STATUS_ICON_SIZE)
    end
end

local function RefreshGroupTeleportButtonAvailabilityVisual(button)
    local icon = button and button.mkTeleportIcon
    if not icon then
        return
    end

    local unavailableColor = button.mkTeleportUnavailableColor or core.GROUP_LIST_UNAVAILABLE_TEXT_COLOR or { 0.52, 0.52, 0.52, 1 }
    local teleportStatus = button.mkTeleportStatus
    local canUseTeleport = button.mkTeleportReady == true

    if button.mkTeleportUnavailable or not button.mapID then
        if icon.SetDesaturated then
            icon:SetDesaturated(true)
        end
        icon:SetVertexColor(unavailableColor[1], unavailableColor[2], unavailableColor[3], 0.55)
        icon:SetAlpha(0.45)
    elseif canUseTeleport then
        if icon.SetDesaturated then
            icon:SetDesaturated(false)
        end
        icon:SetVertexColor(1, 1, 1, 0.92)
        icon:SetAlpha(1)
    elseif teleportStatus == "cooldown" or teleportStatus == "not_learned" then
        if icon.SetDesaturated then
            icon:SetDesaturated(true)
        end
        icon:SetVertexColor(0.45, 0.45, 0.45, teleportStatus == "cooldown" and 0.85 or 0.72)
        icon:SetAlpha(teleportStatus == "cooldown" and 0.82 or 0.72)
    else
        if icon.SetDesaturated then
            icon:SetDesaturated(true)
        end
        icon:SetVertexColor(1, 1, 1, 0.55)
        icon:SetAlpha(0.7)
    end
end

function MK:CropGroupRowBackground(texture)
    if not texture or not texture.GetTexCoord or not texture.SetTexCoord then
        return
    end

    local cropTop = tonumber(core.GROUP_LIST_ROW_BACKGROUND_CROP_TOP) or 0
    local cropBottom = tonumber(core.GROUP_LIST_ROW_BACKGROUND_CROP_BOTTOM) or 0
    texture:SetTexCoord(0, 1, 0, 1)
    if cropTop <= 0 and cropBottom <= 0 then
        return
    end

    cropTop = math.max(0, math.min(0.95, cropTop))
    cropBottom = math.max(0, math.min(0.95 - cropTop, cropBottom))
    local cropEnd = math.max(cropTop + 0.01, 1 - cropBottom)
    texture:SetTexCoord(0, 1, cropTop, cropEnd)
end

function MK:ResetGroupRowBackgroundAnchors(row)
    if not row or not row.widgets then
        return
    end

    if row.widgets.rowStripe then
        row.widgets.rowStripe:ClearAllPoints()
        row.widgets.rowStripe:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.widgets.rowStripe:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        row.widgets.rowStripe:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
        row.widgets.rowStripe:SetDrawLayer("BACKGROUND", -2)
        if row.widgets.rowStripe.SetTexCoord then
            row.widgets.rowStripe:SetTexCoord(0, 1, 0, 1)
        end
    end
    if row.widgets.rowStripeTransition then
        row.widgets.rowStripeTransition:ClearAllPoints()
        row.widgets.rowStripeTransition:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.widgets.rowStripeTransition:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        row.widgets.rowStripeTransition:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
        row.widgets.rowStripeTransition:SetDrawLayer("BACKGROUND", -1)
        if row.widgets.rowStripeTransition.SetTexCoord then
            row.widgets.rowStripeTransition:SetTexCoord(0, 1, 0, 1)
        end
    end

    if row.widgets.background then
        row.widgets.background:ClearAllPoints()
        row.widgets.background:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.widgets.background:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        row.widgets.background:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
    end
    if row.widgets.backgroundTransition then
        row.widgets.backgroundTransition:ClearAllPoints()
        row.widgets.backgroundTransition:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.widgets.backgroundTransition:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        row.widgets.backgroundTransition:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
    end
end

function MK:SetGroupRowStripe(row, texturePath)
    if not (row and row.widgets and row.widgets.rowStripe) then
        return
    end

    texturePath = texturePath or GROUP_LIST_ROW_TEXTURE_NORMAL
    local rowStripe = row.widgets.rowStripe
    local transition = row.widgets.rowStripeTransition
    if transition then
        transition:Hide()
    end
    rowStripe:SetTexture(texturePath)
    rowStripe:SetTexCoord(0, 1, 0, 1)
    rowStripe:SetVertexColor(1, 1, 1, 1)
    rowStripe:SetAlpha(1)
    rowStripe:Show()
end

function MK:SetGroupRowBackground(row)
    if not row or not row.widgets then
        return
    end

    if row.widgets.background then
        row.widgets.background:Hide()
    end
    if row.widgets.backgroundTransition then
        row.widgets.backgroundTransition:Hide()
    end

    row.mkGroupBackgroundKey = nil
    row.mkGroupBackgroundVisible = nil
end

function MK:CenterGroupRowRoles(row)
    if not row or not row.widgets or not row.widgets.rolesContainer or not row.widgets.roles then
        return
    end

    local visibleRoles = {}
    for _, roleKey in ipairs(MK:GetRoleOrder()) do
        local roleFrame = row.widgets.roles[roleKey]
        if roleFrame and roleFrame:IsShown() then
            visibleRoles[#visibleRoles + 1] = roleFrame
        end
    end

    local iconGap = core.GROUP_LIST_STATUS_ICON_GAP or 4
    local rolesWidth = row.widgets.rolesContainer:GetWidth() or core.GROUP_LIST_COLUMN_WIDTHS.roles
    local visibleWidth = math.max(0, #visibleRoles - 1) * iconGap
    for _, roleFrame in ipairs(visibleRoles) do
        visibleWidth = visibleWidth + (roleFrame.mkRoleIconSize or core.GROUP_LIST_STATUS_ICON_SIZE or 16)
    end
    local rolesStartX = math.max(0, (rolesWidth - visibleWidth) / 2)
    local currentX = rolesStartX
    for _, roleFrame in ipairs(visibleRoles) do
        local iconSize = roleFrame.mkRoleIconSize or core.GROUP_LIST_STATUS_ICON_SIZE or 16
        roleFrame:ClearAllPoints()
        roleFrame:SetPoint("LEFT", row.widgets.rolesContainer, "LEFT", currentX, 0)
        roleFrame:SetSize(iconSize, iconSize)
        if roleFrame.icon then
            roleFrame.icon:ClearAllPoints()
            roleFrame.icon:SetAllPoints(roleFrame)
            roleFrame.icon:SetSize(iconSize, iconSize)
        end
        currentX = currentX + iconSize + iconGap
    end
end

function MK:InitializeGroupCharacterRowFrame(row)
    if not row or row.widgets then
        return row
    end

    local defaultColumnLayout = core.GetGroupColumnLayout(core.GROUP_LIST_COLUMN_WIDTHS)

    row:SetHeight(GROUP_LIST_ROW_HEIGHT)
    row:Hide()
    row:SetScript("OnClick", nil)
    row:SetScript("OnEnter", function(button)
        MK:SetGroupRowHover(button, true)
    end)
    row:SetScript("OnLeave", function(button)
        MK:SetGroupRowHover(button, false)
    end)
    if row.HighlightTexture then
        row.HighlightTexture:Hide()
        row.HighlightTexture:SetAlpha(0)
    end

    local rowStripe = row.NormalTexture or row:GetNormalTexture()
    if not rowStripe then
        rowStripe = row:CreateTexture(nil, "BACKGROUND", nil, -2)
    end
    if rowStripe then
        rowStripe:ClearAllPoints()
        rowStripe:SetPoint("LEFT", row, "LEFT", 0, 0)
        rowStripe:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        rowStripe:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
        rowStripe:SetDrawLayer("BACKGROUND", -2)
        rowStripe:SetTexture(GROUP_LIST_ROW_TEXTURE_NORMAL)
        rowStripe:SetTexCoord(0, 1, 0, 1)
        rowStripe:SetVertexColor(1, 1, 1, 1)
        rowStripe:Show()
    end
    local rowStripeTransition = row:CreateTexture(nil, "BACKGROUND", nil, -1)
    rowStripeTransition:SetPoint("LEFT", row, "LEFT", 0, 0)
    rowStripeTransition:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    rowStripeTransition:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
    rowStripeTransition:SetDrawLayer("BACKGROUND", -1)
    rowStripeTransition:Hide()

    local background = row:CreateTexture(nil, "BACKGROUND")
    background:SetPoint("LEFT", row, "LEFT", 0, 0)
    background:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    background:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
    background:SetTexture("Interface\\Buttons\\WHITE8X8")
    background:Hide()

    local backgroundTransition = row:CreateTexture(nil, "BACKGROUND", nil, 1)
    backgroundTransition:SetPoint("LEFT", row, "LEFT", 0, 0)
    backgroundTransition:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    backgroundTransition:SetHeight(core.GROUP_LIST_ROW_BACKGROUND_HEIGHT)
    backgroundTransition:SetTexture("Interface\\Buttons\\WHITE8X8")
    backgroundTransition:Hide()

    local hoverLeft = row:CreateTexture(nil, "BORDER", nil, -1)
    hoverLeft:SetPoint("LEFT", row, "LEFT", core.GROUP_LIST_ROW_HOVER_INSET_X, core.GROUP_LIST_ROW_HOVER_OFFSET_Y)
    hoverLeft:SetSize(core.GROUP_LIST_ROW_HOVER_FADE_WIDTH, core.GROUP_LIST_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", -core.GROUP_LIST_ROW_HOVER_INSET_X, core.GROUP_LIST_ROW_HOVER_OFFSET_Y)
    hoverRight:SetSize(core.GROUP_LIST_ROW_HOVER_FADE_WIDTH, core.GROUP_LIST_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(core.GROUP_LIST_ROW_HOVER_HEIGHT)
    hover:SetTexture("Interface\\Buttons\\WHITE8X8")
    hover:SetVertexColor(1, 0.74, 0.18, 0.13)
    hover:Hide()

    local groupDivider = row:CreateTexture(nil, "BACKGROUND")
    groupDivider:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
    groupDivider:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0)
    groupDivider:SetHeight(1)
    groupDivider:SetTexture("Interface\\Buttons\\WHITE8X8")
    groupDivider:SetVertexColor(1, 0.82, 0, 0.35)
    groupDivider:Hide()

    local nameContainer = CreateFrame("Frame", nil, row)
    nameContainer:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.name.x, 0)
    nameContainer:SetSize(core.GROUP_LIST_COLUMN_WIDTHS.name, 18)

    local specIcon = nameContainer:CreateTexture(nil, "OVERLAY")
    specIcon:SetPoint("LEFT", nameContainer, "LEFT", 0, 0)
    specIcon:SetSize(GROUP_LIST_SPEC_ICON_SIZE, GROUP_LIST_SPEC_ICON_SIZE)
    specIcon:SetTexCoord(0, 1, 0, 1)
    specIcon:Hide()

    local nameLabel = nameContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nameLabel:SetPoint("LEFT", nameContainer, "LEFT", 0, 0)
    nameLabel:SetPoint("RIGHT", nameContainer, "RIGHT", 0, 0)
    nameLabel:SetHeight(18)
    nameLabel:SetJustifyH("LEFT")
    if core.ApplyFont then
        core.ApplyFont(nameLabel, 12, "")
    end

    local nameHoverFrame = CreateFrame("Frame", nil, row)
    nameHoverFrame:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.name.x, 0)
    nameHoverFrame:SetSize(core.GROUP_LIST_COLUMN_WIDTHS.name, GROUP_LIST_ROW_HEIGHT)
    nameHoverFrame:EnableMouse(true)
    nameHoverFrame:SetFrameLevel(row:GetFrameLevel() + 3)
    nameHoverFrame:SetScript("OnEnter", function(frame)
        MK:SetGroupRowHover(row, true)
        if row.mkCharacterEntry then
            MK:ShowGroupCharacterTooltip(frame, row.mkCharacterEntry, row.mkDisplayState)
        end
    end)
    nameHoverFrame:SetScript("OnLeave", function()
        MK:SetGroupRowHover(row, false)
        MK:HideGroupCharacterTooltip()
    end)

    local armorLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    armorLabel:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.armor.x, 0)
    armorLabel:SetWidth(core.GROUP_LIST_COLUMN_WIDTHS.armor)
    armorLabel:SetHeight(18)
    armorLabel:SetJustifyH("CENTER")
    if core.ApplyFont then
        core.ApplyFont(armorLabel, 12, "")
    end

    local keystoneButton = CreateFrame("Button", nil, row)
    keystoneButton:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.key.x, 0)
    keystoneButton:SetSize(core.GROUP_LIST_COLUMN_WIDTHS.key, 18)
    keystoneButton:EnableMouse(true)

    local keystoneText = keystoneButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    keystoneText:SetAllPoints(keystoneButton)
    keystoneText:SetJustifyH("LEFT")
    if core.ApplyFont then
        core.ApplyFont(keystoneText, 12, "")
    end

    keystoneButton:SetScript("OnEnter", function(button)
        MK:SetGroupRowHover(row, true)
        local link = button.keystoneLink
        if not link then
            return
        end
        GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
        GameTooltip:SetHyperlink(link)
        GameTooltip:Show()
    end)
    keystoneButton:SetScript("OnLeave", function()
        MK:SetGroupRowHover(row, false)
        MK:HideGroupCharacterTooltip()
    end)
    keystoneButton:SetScript("OnClick", function(button, mouseButton)
        local link = button.keystoneLink
        if not link then
            return
        end
        if HandleModifiedItemClick and HandleModifiedItemClick(link) then
            return
        end
        if mouseButton == "LeftButton" and SetItemRef then
            SetItemRef(link, link, mouseButton, button)
        end
    end)

    local teleportContainer = CreateFrame("Frame", nil, row)
    teleportContainer:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.tp.x, 0)
    teleportContainer:SetSize(core.GROUP_LIST_COLUMN_WIDTHS.tp, core.GROUP_LIST_TELEPORT_BUTTON_SIZE)

    local teleportButton = CreateFrame("Button", nil, teleportContainer, "SecureActionButtonTemplate")
    teleportButton:SetPoint("CENTER", teleportContainer, "CENTER", 0, 0)
    teleportButton:SetSize(core.GROUP_LIST_TELEPORT_BUTTON_SIZE, core.GROUP_LIST_TELEPORT_BUTTON_SIZE)
    teleportButton:RegisterForClicks("AnyUp", "AnyDown")
    SetSecureButtonAttributes(teleportButton, "type", nil, "type1", nil, "macrotext", nil, "macrotext1", nil, "spell", nil, "spell1", nil)

    local teleportIcon = teleportButton:CreateTexture(nil, "OVERLAY")
    teleportIcon:SetPoint("CENTER", teleportButton, "CENTER", 0, 0)
    teleportIcon:SetSize(core.GROUP_LIST_STATUS_ICON_SIZE, core.GROUP_LIST_STATUS_ICON_SIZE)
    if teleportIcon.SetAtlas then
        teleportIcon:SetAtlas("MagePortalAlliance", false)
    end
    teleportIcon:SetVertexColor(1, 1, 1, 0.92)
    teleportButton.mkTeleportIcon = teleportIcon
    teleportButton.mkSecureAttributesApplied = function(button)
        button.mkTeleportReady = button.mkTeleportDesiredReady == true
        button.mapID = button.mkTeleportEffectiveMapID
        button.mkTeleportStatus = GetDungeonTeleportStatus(button.mapID)
        button.mkTeleportSecurePending = nil
        RefreshGroupTeleportButtonAvailabilityVisual(button)
    end
    MK:SetTeleportButtonVisualState(teleportButton, "normal")

    teleportButton:SetScript("OnEnter", function(button)
        MK:SetGroupRowHover(row, true)
        MK:SetTeleportButtonVisualState(button, "hover")
        local mapID = button.mapID
        if not mapID then
            return
        end

        GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
        AddDungeonTeleportTooltipLine(GameTooltip, mapID)
        GameTooltip:Show()
    end)
    teleportButton:SetScript("OnLeave", function(button)
        MK:SetGroupRowHover(row, false)
        MK:SetTeleportButtonVisualState(button, "normal")
        HideKeystoneTooltip()
    end)
    teleportButton:SetScript("OnMouseDown", function(button)
        if not button.mkTeleportReady then
            MK:SetTeleportButtonVisualState(button, button.IsMouseOver and button:IsMouseOver() and "hover" or "normal")
            return
        end
        MK:SetTeleportButtonVisualState(button, "pushed")
    end)
    teleportButton:SetScript("OnMouseUp", function(button)
        if button.IsMouseOver and button:IsMouseOver() then
            MK:SetTeleportButtonVisualState(button, "hover")
        else
            MK:SetTeleportButtonVisualState(button, "normal")
        end
    end)
    AttachDungeonTeleportDialogTrigger(MK, teleportButton)

    local ratingLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    ratingLabel:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.rating.x, 0)
    ratingLabel:SetWidth(core.GROUP_LIST_COLUMN_WIDTHS.rating)
    ratingLabel:SetHeight(18)
    ratingLabel:SetJustifyH("CENTER")
    if core.ApplyFont then
        core.ApplyFont(ratingLabel, 12, "")
    end

    local rolesContainer = CreateFrame("Frame", nil, row)
    rolesContainer:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.roles.x, 0)
    rolesContainer:SetSize(core.GROUP_LIST_COLUMN_WIDTHS.roles, 18)

    local roles = {}
    for _, roleKey in ipairs(MK:GetRoleOrder()) do
        local roleFrame = CreateAtlasDisplay(rolesContainer, core.GROUP_LIST_STATUS_ICON_SIZE, core.GROUP_LIST_STATUS_ICON_SIZE, ROLE_ATLAS_BY_KEY[roleKey])
        if roleFrame.icon then
            roleFrame.icon:ClearAllPoints()
            roleFrame.icon:SetAllPoints(roleFrame)
        end
        roleFrame.icon:SetVertexColor(1, 1, 1, 0.95)
        roleFrame:Hide()
        roles[roleKey] = roleFrame
    end

    local rolesUnavailableLabel = rolesContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    rolesUnavailableLabel:SetAllPoints(rolesContainer)
    rolesUnavailableLabel:SetJustifyH("CENTER")
    rolesUnavailableLabel:SetText("-")
    rolesUnavailableLabel:SetTextColor(
        core.GROUP_LIST_UNAVAILABLE_TEXT_COLOR[1],
        core.GROUP_LIST_UNAVAILABLE_TEXT_COLOR[2],
        core.GROUP_LIST_UNAVAILABLE_TEXT_COLOR[3],
        core.GROUP_LIST_UNAVAILABLE_TEXT_COLOR[4] or 1
    )
    if core.ApplyFont then
        core.ApplyFont(rolesUnavailableLabel, 12, "")
    end
    rolesUnavailableLabel:Hide()

    local accountTypeContainer = CreateFrame("Frame", nil, row)
    accountTypeContainer:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.accountType.x, 0)
    accountTypeContainer:SetSize(core.GROUP_LIST_COLUMN_WIDTHS.accountType, 18)
    local accountTypeIconSlotSize = core.GROUP_LIST_ACCOUNT_TYPE_VETERAN_ICON_SIZE or 18
    accountTypeContainer.mkAccountTypeAlign = "LEFT"
    accountTypeContainer.mkAccountTypeInsetX = GROUP_LIST_ACCOUNT_TYPE_ICON_INSET_X
    accountTypeContainer.mkAccountTypeIconSlotSize = accountTypeIconSlotSize
    accountTypeContainer.mkAccountTypeCenterLabel = true
    accountTypeContainer.mkAccountTypeReserveCenteredLabelIconSpace = true

    local accountTypeIconSlot = CreateFrame("Frame", nil, accountTypeContainer)
    accountTypeIconSlot:SetSize(accountTypeIconSlotSize, accountTypeIconSlotSize)
    accountTypeIconSlot:SetPoint("LEFT", accountTypeContainer, "LEFT", GROUP_LIST_ACCOUNT_TYPE_ICON_INSET_X, 0)
    accountTypeIconSlot:Hide()
    accountTypeContainer.mkAccountTypeIconSlot = accountTypeIconSlot

    local accountTypeIcon = accountTypeContainer:CreateTexture(nil, "OVERLAY")
    accountTypeIcon:SetPoint("CENTER", accountTypeContainer, "CENTER", 0, 0)
    accountTypeIcon:SetSize(18, 18)
    accountTypeIcon:Hide()

    local accountTypeLabel = accountTypeContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    accountTypeLabel:SetAllPoints(accountTypeContainer)
    accountTypeLabel:SetJustifyH("CENTER")
    if core.ApplyFont then
        core.ApplyFont(accountTypeLabel, 12, "")
    end

    local meetingStoneButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    LayoutGroupMeetingStoneButton(meetingStoneButton, row, defaultColumnLayout.meetingStone)
    meetingStoneButton:SetText(TRANSLATE("创建招募"))
    ApplyGroupMeetingStoneButtonStyle(meetingStoneButton)
    meetingStoneButton:SetScript("OnClick", function(button)
        if button.characterEntry then
            if core.PlayPanelButtonClickSound then
                core.PlayPanelButtonClickSound()
            end
            if button.mkGroupQuickAction == "sendKeystone" then
                MK:SendGroupKeystoneInfo(button.characterEntry, button.mkGroupQuickActionIsCurrentPlayer == true)
            else
                MK:OpenMeetingStoneListingForCharacter(button.characterEntry)
            end
        end
    end)
    meetingStoneButton:SetScript("OnEnter", function(button)
        MK:SetGroupRowHover(row, true)
        MK:HideGroupCharacterTooltip()
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(button)
        end
        SetGroupMeetingStoneButtonTextPressed(button, button.mkMeetingStoneButtonPressed == true)
    end)
    meetingStoneButton:SetScript("OnLeave", function(button)
        MK:SetGroupRowHover(row, false)
        MK:HideGroupCharacterTooltip()
        button.mkMeetingStoneButtonPressed = nil
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(button)
        end
        SetGroupMeetingStoneButtonTextPressed(button, false)
    end)
    meetingStoneButton:SetScript("OnMouseDown", function(button, mouseButton)
        if mouseButton == "LeftButton" then
            button.mkMeetingStoneButtonPressed = true
            if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
                core.MeetingStoneButtonArt.Update(button)
            end
            SetGroupMeetingStoneButtonTextPressed(button, true)
        end
    end)
    meetingStoneButton:SetScript("OnMouseUp", function(button)
        button.mkMeetingStoneButtonPressed = nil
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(button)
        end
        SetGroupMeetingStoneButtonTextPressed(button, false)
    end)

    local meetingStoneLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    meetingStoneLabel:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.meetingStone.x, 0)
    meetingStoneLabel:SetWidth(core.GROUP_LIST_COLUMN_WIDTHS.meetingStone)
    meetingStoneLabel:SetHeight(18)
    meetingStoneLabel:SetJustifyH("CENTER")
    if core.ApplyFont then
        core.ApplyFont(meetingStoneLabel, 12, "")
    end

    row.widgets = {
        nameContainer = nameContainer,
        specIcon = specIcon,
        nameLabel = nameLabel,
        nameHoverFrame = nameHoverFrame,
        armorLabel = armorLabel,
        keystoneLink = keystoneButton,
        keystoneText = keystoneText,
        teleportContainer = teleportContainer,
        teleportButton = teleportButton,
        teleportIcon = teleportIcon,
        ratingLabel = ratingLabel,
        rolesContainer = rolesContainer,
        rolesUnavailableLabel = rolesUnavailableLabel,
        roles = roles,
        hoverLeft = hoverLeft,
        accountTypeContainer = accountTypeContainer,
        accountTypeIconSlot = accountTypeIconSlot,
        accountTypeIcon = accountTypeIcon,
        accountTypeLabel = accountTypeLabel,
        meetingStoneButton = meetingStoneButton,
        meetingStoneLabel = meetingStoneLabel,
        rowStripe = rowStripe,
        rowStripeTransition = rowStripeTransition,
        background = background,
        backgroundTransition = backgroundTransition,
        hover = hover,
        hoverRight = hoverRight,
        groupDivider = groupDivider,
    }

    return row
end

function MK:CreateGroupCharacterRowFrame(parent)
    local row = CreateFrame("Button", nil, parent)
    return self:InitializeGroupCharacterRowFrame(row)
end

local function ApplyGroupRowColumn(object, row, column, height)
    if not object or not row or not column then
        return
    end

    local isProtected = object.IsProtected and object:IsProtected()
    if isProtected and InCombatLockdown and InCombatLockdown() then
        return
    end

    if not isProtected then
        object:ClearAllPoints()
        object:SetPoint("LEFT", row, "LEFT", column.x or 0, 0)
    end

    if object.SetWidth then
        object:SetWidth(column.width or 0)
    end
    if height and object.SetHeight then
        object:SetHeight(height)
    end
end

local function LayoutGroupNameColumn(row)
    if not (row and row.widgets and row.widgets.nameContainer and row.widgets.nameLabel) then
        return
    end

    local nameContainer = row.widgets.nameContainer
    local nameLabel = row.widgets.nameLabel
    local specIcon = row.widgets.specIcon
    local hasIcon = specIcon and specIcon.IsShown and specIcon:IsShown()
    local labelInsetX = hasIcon and (GROUP_LIST_SPEC_ICON_SIZE + GROUP_LIST_SPEC_ICON_GAP) or 0

    if specIcon then
        specIcon:ClearAllPoints()
        specIcon:SetPoint("LEFT", nameContainer, "LEFT", 0, 0)
        specIcon:SetSize(GROUP_LIST_SPEC_ICON_SIZE, GROUP_LIST_SPEC_ICON_SIZE)
    end

    nameLabel:ClearAllPoints()
    nameLabel:SetPoint("LEFT", nameContainer, "LEFT", labelInsetX, 0)
    nameLabel:SetPoint("RIGHT", nameContainer, "RIGHT", 0, 0)
    nameLabel:SetHeight(18)
    nameLabel:SetJustifyH("LEFT")
end

local function GetGroupListAvailableColumnWidth(widgets)
    if not widgets then
        return nil
    end

    local width = 0
    if widgets.groupHeaderFrame and widgets.groupHeaderFrame.GetWidth then
        width = math.max(width, widgets.groupHeaderFrame:GetWidth() or 0)
    end
    if widgets.groupBrowseFrame and widgets.groupBrowseFrame.GetWidth then
        width = math.max(width, (widgets.groupBrowseFrame:GetWidth() or 0) - (GROUP_LIST_ROWS_PADDING_X * 2))
    end
    if widgets.groupScrollBox and widgets.groupScrollBox.GetWidth then
        width = math.max(width, widgets.groupScrollBox:GetWidth() or 0)
    end

    if width <= 1 then
        return nil
    end
    return math.floor(width)
end

local GROUP_COLUMN_MIN_WIDTHS = {
    name = 112,
    armor = 54,
    key = 128,
    rating = 66,
    roles = 72,
    tp = 46,
    accountType = 92,
    meetingStone = 90,
}

local GROUP_COLUMN_SHRINK_ORDER = {
    "accountType",
    "name",
    "key",
    "roles",
    "armor",
    "rating",
    "tp",
}

local function FitGroupColumnWidthsToAvailable(baseWidths, availableWidth)
    local widths = core.GetGroupColumnWidths(baseWidths)
    local layout = core.GetGroupColumnLayout(widths)
    local overflow = availableWidth and layout.totalWidth and (layout.totalWidth - availableWidth) or 0
    if overflow <= 0 then
        return widths
    end

    for _, key in ipairs(GROUP_COLUMN_SHRINK_ORDER) do
        local minWidth = GROUP_COLUMN_MIN_WIDTHS[key] or 0
        local current = widths[key] or 0
        local reducible = math.max(0, current - minWidth)
        if reducible > 0 then
            local delta = math.min(reducible, overflow)
            widths[key] = current - delta
            overflow = overflow - delta
            if overflow <= 0 then
                break
            end
        end
    end

    if overflow > 0 and widths.meetingStone then
        widths.meetingStone = math.max(GROUP_COLUMN_MIN_WIDTHS.meetingStone or 90, widths.meetingStone - overflow)
    end

    return widths
end

function MK:ApplyGroupColumnWidthsToRow(widgets, row)
    if not widgets or not row or not row.widgets then
        return
    end

    local layout = core.GetGroupColumnLayout(widgets.groupColumnWidths)
    local rowWidth = layout.totalWidth or 1
    if row.SetWidth then
        row:SetWidth(math.max(rowWidth, 1))
    end
    MK:ResetGroupRowBackgroundAnchors(row)

    if row.widgets.nameContainer then
        ApplyGroupRowColumn(row.widgets.nameContainer, row, layout.name, 18)
        LayoutGroupNameColumn(row)
    end
    if row.widgets.nameHoverFrame then
        ApplyGroupRowColumn(row.widgets.nameHoverFrame, row, layout.name, GROUP_LIST_ROW_HEIGHT)
    end
    if row.widgets.armorLabel then
        ApplyGroupRowColumn(row.widgets.armorLabel, row, layout.armor, 18)
    end
    if row.widgets.keystoneLink then
        ApplyGroupRowColumn(row.widgets.keystoneLink, row, layout.key, 18)
    end
    if row.widgets.ratingLabel then
        ApplyGroupRowColumn(row.widgets.ratingLabel, row, layout.rating, 18)
    end
    if row.widgets.rolesContainer then
        ApplyGroupRowColumn(row.widgets.rolesContainer, row, layout.roles, 18)
    end
    if row.widgets.teleportContainer then
        ApplyGroupRowColumn(row.widgets.teleportContainer, row, layout.tp, core.GROUP_LIST_TELEPORT_BUTTON_SIZE)
    end
    if row.widgets.teleportButton then
        row.widgets.teleportButton:ClearAllPoints()
        row.widgets.teleportButton:SetPoint("CENTER", row.widgets.teleportContainer, "CENTER", 0, 0)
        row.widgets.teleportButton:SetSize(core.GROUP_LIST_TELEPORT_BUTTON_SIZE, core.GROUP_LIST_TELEPORT_BUTTON_SIZE)
    end
    MK:CenterGroupRowRoles(row)
    if row.widgets.accountTypeContainer then
        ApplyGroupRowColumn(row.widgets.accountTypeContainer, row, layout.accountType, 18)
    end
    if row.widgets.meetingStoneLabel then
        ApplyGroupRowColumn(row.widgets.meetingStoneLabel, row, layout.meetingStone, 18)
    end
    if row.widgets.meetingStoneButton then
        LayoutGroupMeetingStoneButton(row.widgets.meetingStoneButton, row, layout.meetingStone)
    end
end

local function UpdateGroupColumnWidths(widgets, groupCharacters)
    if not widgets or not widgets.groupHeaderFrame then
        return
    end

    widgets.groupColumnWidths = FitGroupColumnWidthsToAvailable(widgets.groupBaseColumnWidths or core.GROUP_LIST_COLUMN_WIDTHS, GetGroupListAvailableColumnWidth(widgets))

    MK:ApplyGroupHeaderColumnLayout(widgets.groupHeaderFrame, widgets.groupColumnWidths)

    for _, row in ipairs(widgets.groupRowFrames or {}) do
        MK:ApplyGroupColumnWidthsToRow(widgets, row)
    end

    if widgets.groupScrollBox and widgets.groupScrollBox.ForEachFrame then
        widgets.groupScrollBox:ForEachFrame(function(row)
            MK:ApplyGroupColumnWidthsToRow(widgets, row)
        end)
    end
end

local function GetCenteredGroupBrowseWidth(widgets, container)
    local maxWidth = tonumber(widgets and widgets.groupBrowseMaxWidth) or 0
    if maxWidth <= 0 then
        return nil
    end

    local availableWidth = container and container.GetWidth and container:GetWidth() or 0
    if availableWidth <= 0 then
        return maxWidth
    end

    return math.max(1, math.min(maxWidth, availableWidth))
end

local function LayoutGroupListPanel(widgets)
    if widgets and widgets.groupBrowseFrame and widgets.groupContainer then
        local itemList = widgets.groupBrowseFrame
        local container = widgets.groupContainer
        local panelBackplate = container.GetParent and container:GetParent() or nil
        local mainWindow = panelBackplate and panelBackplate.GetParent and panelBackplate:GetParent() or nil
        local headerContainer = itemList.HeaderContainer
        local scrollBox = itemList.ScrollBox
        local scrollBar = itemList.ScrollBar
        local scrollBarFrame = widgets.groupScrollBarFrame or (mainWindow and mainWindow.groupScrollBarFrame)
        local headerRowGap = widgets.groupHeaderRowGap or 2

        itemList:ClearAllPoints()
        if widgets.groupBrowseCenterList then
            local browseWidth = GetCenteredGroupBrowseWidth(widgets, container)
            itemList:SetPoint("TOP", container, "TOP", 0, widgets.groupBrowseTopOffset or 0)
            itemList:SetPoint("BOTTOM", container, "BOTTOM", 0, widgets.groupBrowseBottomOffset or 0)
            if browseWidth then
                itemList:SetWidth(browseWidth)
            end
        else
            itemList:SetPoint("TOPLEFT", container, "TOPLEFT", widgets.groupBrowseLeftOffset or 0, widgets.groupBrowseTopOffset or 0)
            itemList:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", widgets.groupBrowseRightOffset or 0, widgets.groupBrowseBottomOffset or 0)
        end

        if headerContainer then
            headerContainer:ClearAllPoints()
            headerContainer:SetPoint("TOPLEFT", itemList, "TOPLEFT", 0, 0)
            headerContainer:SetPoint("TOPRIGHT", itemList, "TOPRIGHT", 0, 0)
            headerContainer:SetHeight(GROUP_LIST_HEADER_HEIGHT)
        end

        if scrollBox and headerContainer then
            scrollBox:ClearAllPoints()
            scrollBox:SetPoint("TOPLEFT", headerContainer, "BOTTOMLEFT", 0, -headerRowGap)
            scrollBox:SetPoint("TOPRIGHT", headerContainer, "BOTTOMRIGHT", 0, -headerRowGap)
            scrollBox:SetPoint("BOTTOM", itemList, "BOTTOM", 0, 0)
        end

        if scrollBarFrame then
            if panelBackplate then
                scrollBarFrame:ClearAllPoints()
                scrollBarFrame:SetPoint("TOPRIGHT", container, "TOPRIGHT", MAIN_PANEL_CONTENT_PADDING_X + GROUP_LIST_SCROLLBAR_SLOT_RIGHT_OFFSET, -GROUP_LIST_SCROLLBAR_SLOT_VERTICAL_INSET)
                scrollBarFrame:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", MAIN_PANEL_CONTENT_PADDING_X + GROUP_LIST_SCROLLBAR_SLOT_RIGHT_OFFSET, GROUP_LIST_SCROLLBAR_SLOT_VERTICAL_INSET)
                scrollBarFrame:SetWidth(GROUP_LIST_SCROLLBAR_SLOT_WIDTH)
                if scrollBarFrame.SetFrameLevel then
                    scrollBarFrame:SetFrameLevel(panelBackplate:GetFrameLevel() + 8)
                end
                scrollBarFrame:Show()
            else
                scrollBarFrame:Hide()
            end
        end

        if scrollBar then
            scrollBar:ClearAllPoints()
            if scrollBar.SetWidth then
                scrollBar:SetWidth(widgets.groupScrollBarWidth or 17)
            end
            if scrollBarFrame and scrollBarFrame:IsShown() then
                if scrollBar.SetFrameLevel then
                    scrollBar:SetFrameLevel(scrollBarFrame:GetFrameLevel() + 1)
                end
                scrollBar:SetPoint("TOP", scrollBarFrame, "TOP", 0, 0)
                scrollBar:SetPoint("BOTTOM", scrollBarFrame, "BOTTOM", 0, 0)
            else
                local scrollBarAnchor = widgets.groupBrowseCenterList and itemList or container
                scrollBar:SetPoint("TOPLEFT", scrollBarAnchor, "TOPRIGHT", widgets.groupScrollBarXOffset or 0, -(widgets.groupScrollBarTopInset or 6))
                scrollBar:SetPoint("BOTTOMLEFT", scrollBarAnchor, "BOTTOMRIGHT", widgets.groupScrollBarXOffset or 0, widgets.groupScrollBarBottomInset or 6)
            end
        end

        if widgets.groupHeaderFrame then
            widgets.groupHeaderFrame:ClearAllPoints()
            widgets.groupHeaderFrame:SetPoint("TOPLEFT", itemList, "TOPLEFT", GROUP_LIST_ROWS_PADDING_X, 0)
            widgets.groupHeaderFrame:SetPoint("TOPRIGHT", itemList, "TOPRIGHT", -GROUP_LIST_ROWS_PADDING_X, 0)
            widgets.groupHeaderFrame:SetHeight(GROUP_LIST_HEADER_HEIGHT)
        end

        UpdateGroupColumnWidths(widgets)

        if widgets.groupEmpty then
            widgets.groupEmpty:ClearAllPoints()
            widgets.groupEmpty:SetPoint("CENTER", scrollBox or itemList, "CENTER", 0, 0)
            widgets.groupEmpty:SetWidth(math.max((itemList:GetWidth() or 0) - 80, 120))
            widgets.groupEmpty:SetJustifyH("CENTER")
        end
        return
    end

    if not widgets or not widgets.groupListPanel or not widgets.groupHeaderFrame or not widgets.groupRowsFrame or not widgets.groupContent then
        return
    end

    local headerHeight = widgets.groupHeaderFrame:GetHeight() or GROUP_LIST_HEADER_HEIGHT
    local rowsContentHeight = widgets.groupRowsContentHeight or GROUP_LIST_EMPTY_MIN_HEIGHT
    local viewportHeight = widgets.groupScrollFrame and widgets.groupScrollFrame:GetHeight() or 0
    local naturalPanelHeight = GROUP_LIST_PANEL_PADDING_TOP
        + headerHeight
        + GROUP_LIST_HEADER_ROW_GAP
        + rowsContentHeight
        + GROUP_LIST_PANEL_PADDING_BOTTOM
    local panelHeight = math.max(naturalPanelHeight, viewportHeight, 1)
    local rowsHeight = math.max(
        rowsContentHeight,
        panelHeight - GROUP_LIST_PANEL_PADDING_TOP - headerHeight - GROUP_LIST_HEADER_ROW_GAP - GROUP_LIST_PANEL_PADDING_BOTTOM
    )

    widgets.groupRowsFrame:SetHeight(math.max(rowsHeight, 1))
    widgets.groupListPanel:SetHeight(panelHeight)
    widgets.groupContent:SetHeight(math.max(panelHeight, 1))
end
core.LayoutGroupListPanel = LayoutGroupListPanel

local function UpdateGroupScrollBarVisibility(widgets, rowCount)
    if not widgets or not widgets.groupScrollBox or not widgets.groupScrollBar then
        return
    end

    local scrollBox = widgets.groupScrollBox
    local scrollBar = widgets.groupScrollBar
    local visibleHeight = scrollBox.GetHeight and (scrollBox:GetHeight() or 0) or 0
    local contentHeight = math.max(0, (tonumber(rowCount) or 0) * GROUP_LIST_ROW_HEIGHT)
    local hasOverflow = contentHeight > (visibleHeight + 0.5)

    if core.GROUP_LIST_FORCE_SCROLL_BAR_VISIBLE then
        if not hasOverflow and scrollBox.ScrollToBegin then
            scrollBox:ScrollToBegin(ScrollBoxConstants and ScrollBoxConstants.NoScrollInterpolation or nil)
        end
        scrollBar:Show()
        return
    end

    if hasOverflow then
        scrollBar:Show()
        return
    end

    if scrollBox.ScrollToBegin then
        scrollBox:ScrollToBegin(ScrollBoxConstants and ScrollBoxConstants.NoScrollInterpolation or nil)
    end
    scrollBar:Hide()
end

local function ShouldPreserveGroupScrollPosition(refreshSource)
    return refreshSource == "NetEaseAPI"
        or refreshSource == "GroupUnitRatingLookup"
        or refreshSource == "GroupRatingLookupTimeout"
end

core.UpdateGroupHeaderSortIndicators = UpdateGroupHeaderSortIndicators
core.UpdateGroupColumnWidths = UpdateGroupColumnWidths
core.UpdateGroupScrollBarVisibility = UpdateGroupScrollBarVisibility

function MK:ResetGroupRatingLookupState()
    -- 不清空成功/超时缓存和已开始等待时间；同步请求、LibKeystone 请求很频繁，清空会导致反复转圈和重复查询。
    self.groupRatingLookupStartedAt = self.groupRatingLookupStartedAt or {}
    self.groupRatingLookupRefreshTimers = self.groupRatingLookupRefreshTimers or {}
    self.groupUnitRatingCache = self.groupUnitRatingCache or {}
    self.groupUnitRatingLookupDirty = nil
    self.groupUnitRatingLookupQueue = {}
    self.groupUnitRatingLookupPending = {}
end

function MK:CanLookupGroupUnitRating(characterEntry)
    return ResolveGroupUnitRatingMember(self, characterEntry) ~= nil
end

function MK:GetGroupUnitRatingCacheEntry(characterEntry)
    local member = ResolveGroupUnitRatingMember(self, characterEntry)
    if not (member and member.fullName) then
        return nil, nil
    end

    local cache = self.groupUnitRatingCache
    local entry = type(cache) == "table" and cache[member.fullName] or nil
    if type(entry) ~= "table" then
        return nil, member
    end

    local now = GetGroupRatingLookupNow()
    if entry.status == "failed" or entry.status == "timeout" then
        if type(entry.expiresAt) == "number" and entry.expiresAt > now then
            return entry, member
        end
        cache[member.fullName] = nil
        return nil, member
    end

    if type(entry.expiresAt) == "number" and entry.expiresAt > now then
        return entry, member
    end

    cache[member.fullName] = nil
    return nil, member
end

function MK:GetGroupUnitRatingScore(characterEntry)
    local entry = self:GetGroupUnitRatingCacheEntry(characterEntry)
    return GetPositiveGroupRating(entry and entry.score)
end

function MK:GetGroupUnitRatingLookupStatus(characterEntry)
    local entry = self:GetGroupUnitRatingCacheEntry(characterEntry)
    return entry and entry.status or nil
end

function MK:ScheduleGroupUnitRatingLookupBatch()
    if self.groupUnitRatingLookupScheduled then
        return
    end

    self.groupUnitRatingLookupScheduled = true
    if C_Timer and C_Timer.After then
        C_Timer.After(GROUP_LIST_UNIT_RATING_BATCH_DELAY_SECONDS, function()
            self:ProcessGroupUnitRatingLookupBatch()
        end)
    else
        self:ProcessGroupUnitRatingLookupBatch()
    end
end

function MK:QueueGroupUnitRatingLookup(characterEntry)
    local cacheEntry, member = self:GetGroupUnitRatingCacheEntry(characterEntry)
    if cacheEntry or not (member and member.fullName and member.unit) then
        return false
    end

    self.groupUnitRatingLookupPending = self.groupUnitRatingLookupPending or {}
    if self.groupUnitRatingLookupPending[member.fullName] then
        return false
    end

    self.groupUnitRatingLookupQueue = self.groupUnitRatingLookupQueue or {}
    self.groupUnitRatingLookupPending[member.fullName] = true
    self.groupUnitRatingLookupQueue[#self.groupUnitRatingLookupQueue + 1] = {
        key = member.fullName,
        unit = member.unit,
    }
    self:ScheduleGroupUnitRatingLookupBatch()
    return true
end

function MK:ProcessGroupUnitRatingLookupBatch()
    self.groupUnitRatingLookupScheduled = nil

    local queue = self.groupUnitRatingLookupQueue
    if type(queue) ~= "table" or #queue == 0 then
        return
    end

    local now = GetGroupRatingLookupNow()
    self.groupUnitRatingCache = self.groupUnitRatingCache or {}
    self.groupUnitRatingLookupPending = self.groupUnitRatingLookupPending or {}

    local processed = 0
    local changed = false
    while processed < GROUP_LIST_UNIT_RATING_BATCH_SIZE and #queue > 0 do
        local item = table.remove(queue, 1)
        processed = processed + 1
        if item and item.key then
            self.groupUnitRatingLookupPending[item.key] = nil
        end

        local score
        local lookupStatus = "timeout"
        if item and type(item.unit) == "string"
            and item.unit ~= ""
            and UnitExists
            and UnitExists(item.unit)
            and (not UnitIsConnected or UnitIsConnected(item.unit) ~= false)
        then
            local unitFullName = GetUnitName and GetUnitName(item.unit, true) or nil
            if not unitFullName or GroupRatingIdentifierMatches(unitFullName, item.key) then
                if C_PlayerInfo and C_PlayerInfo.GetPlayerMythicPlusRatingSummary then
                    local ok, summary = pcall(C_PlayerInfo.GetPlayerMythicPlusRatingSummary, item.unit)
                    if ok and type(summary) == "table" then
                        score = GetPositiveGroupRating(summary.currentSeasonScore)
                        lookupStatus = score and "ready" or "no_score"
                    end
                end
            end
        end

        if item and item.key then
            self.groupUnitRatingCache[item.key] = {
                score = score,
                status = lookupStatus,
                expiresAt = (lookupStatus == "failed" or lookupStatus == "timeout")
                    and (now + GROUP_LIST_UNIT_RATING_FAILED_TTL_SECONDS)
                    or (now + GROUP_LIST_UNIT_RATING_CACHE_TTL_SECONDS),
            }
            changed = true
        end
    end

    if changed then
        self.groupUnitRatingLookupDirty = true
    end

    if #queue > 0 then
        self:ScheduleGroupUnitRatingLookupBatch()
        return
    end

    local shouldRefresh = self.groupUnitRatingLookupDirty and self.IsMainWindowShown and self:IsMainWindowShown()
        and (self.activeTab == "group" or self.activeTab == "carpool")
    self.groupUnitRatingLookupDirty = nil

    if shouldRefresh then
        if self.RefreshMainWindow then
            self:RefreshMainWindow("GroupUnitRatingLookup")
        end
    end
end

function MK:GetGroupRatingDisplayScore(characterEntry)
    local character = characterEntry and characterEntry.data or {}
    local source = GetGroupRatingEntrySource(characterEntry)
    local characterScore = GetPositiveGroupRating(character.rating)

    -- 本地 unit 缓存只读，不在显示路径触发查询；批量查询由队伍/车队刷新入口调度。
    local unitScore = self:GetGroupUnitRatingScore(characterEntry)
    if unitScore then
        return unitScore, "unit"
    end

    if source == "mk" and characterScore then
        return characterScore, "mk"
    end

    -- MK 广播以外的角色缓存通常来自 LibKeystone/LibOpenKeystone 等外部共享数据。
    if characterScore then
        return characterScore, source or "character"
    end

    local applicantRating = self.GetMeetingStoneApplicantRating and self:GetMeetingStoneApplicantRating(characterEntry)
    local applicantScore = tonumber(applicantRating and applicantRating.score)
    if applicantScore and applicantScore > 0 then
        return applicantScore, "meetingstone_applicant", applicantRating
    end

    return nil, nil, nil
end

function MK:QueueGroupUnitRatingLookups(characterEntries)
    if type(characterEntries) ~= "table" then
        return false
    end

    local queued = false
    for _, characterEntry in ipairs(characterEntries) do
        if self:QueueGroupUnitRatingLookup(characterEntry) then
            queued = true
        end
    end

    return queued
end

function MK:ScheduleGroupRatingLookupRefresh(lookupKey, delay)
    if not (lookupKey and lookupKey ~= "" and C_Timer and C_Timer.After) then
        return
    end

    self.groupRatingLookupRefreshTimers = self.groupRatingLookupRefreshTimers or {}
    if self.groupRatingLookupRefreshTimers[lookupKey] then
        return
    end

    self.groupRatingLookupRefreshTimers[lookupKey] = true
    C_Timer.After(delay or GROUP_LIST_RATING_PENDING_SECONDS, function()
        if self.groupRatingLookupRefreshTimers then
            self.groupRatingLookupRefreshTimers[lookupKey] = nil
        end
        if not (self.IsMainWindowShown and self:IsMainWindowShown()) then
            return
        end
        if self.activeTab ~= "group" and self.activeTab ~= "carpool" then
            return
        end
        if self.RefreshMainWindow then
            self:RefreshMainWindow("GroupRatingLookupTimeout", lookupKey)
        end
    end)
end

function MK:GetGroupRatingLookupState(characterEntry, displayState)
    local score = self:GetGroupRatingDisplayScore(characterEntry)
    local lookupKey = GetGroupRatingLookupKey(characterEntry)
    local character = characterEntry and characterEntry.data or {}

    if score and score > 0 then
        if self.groupRatingLookupStartedAt then
            self.groupRatingLookupStartedAt[lookupKey] = nil
        end
        return "ready"
    end

    local unitLookupStatus = self:GetGroupUnitRatingLookupStatus(characterEntry)
    if unitLookupStatus == "failed" or unitLookupStatus == "timeout" then
        if self.groupRatingLookupStartedAt then
            self.groupRatingLookupStartedAt[lookupKey] = nil
        end
        return "timeout"
    end
    if unitLookupStatus == "no_score" then
        if self.groupRatingLookupStartedAt then
            self.groupRatingLookupStartedAt[lookupKey] = nil
        end
        return "no_score"
    end

    if HasExplicitNoGroupRating(character.rating) then
        if self.groupRatingLookupStartedAt then
            self.groupRatingLookupStartedAt[lookupKey] = nil
        end
        return "no_score"
    end

    if not IsGroupRatingLikelyWaitingForData(self, characterEntry, displayState) then
        return "no_source"
    end

    if not lookupKey or lookupKey == "" then
        return "no_source"
    end

    self.groupRatingLookupStartedAt = self.groupRatingLookupStartedAt or {}
    local now = GetGroupRatingLookupNow()
    local startedAt = self.groupRatingLookupStartedAt[lookupKey]
    if not startedAt then
        startedAt = now
        self.groupRatingLookupStartedAt[lookupKey] = startedAt
        self:ScheduleGroupRatingLookupRefresh(lookupKey, GROUP_LIST_RATING_PENDING_SECONDS + 0.05)
    end

    if (now - startedAt) < GROUP_LIST_RATING_PENDING_SECONDS then
        return "pending"
    end

    return "timeout"
end

function MK:SetGroupRowFrameVisible(row, visible)
    if not row then
        return
    end

    if visible then
        row:Show()
        return
    end

    row.characterKey = nil
    row.mkCharacterEntry = nil
    row.mkDisplayState = nil
    if row.widgets.specIcon then
        row.widgets.specIcon:SetTexture(nil)
        row.widgets.specIcon:Hide()
    end
    LayoutGroupNameColumn(row)
    row.widgets.nameLabel:SetText("")
    row.widgets.armorLabel:SetText("")
    row.widgets.keystoneLink.keystoneLink = nil
    row.widgets.keystoneText:SetText("")
    if row.widgets.ratingLabel then
        row.widgets.ratingLabel:SetText("")
    end
    StopGroupRatingPendingAnimation(row)
    if row.widgets.accountTypeLabel then
        row.widgets.accountTypeLabel:SetText("")
    end
    if row.widgets.accountTypeIcon then
        row.widgets.accountTypeIcon:Hide()
    end
    if row.widgets.meetingStoneLabel then
        row.widgets.meetingStoneLabel:SetText("")
        row.widgets.meetingStoneLabel:Hide()
    end
    if row.widgets.meetingStoneButton then
        row.widgets.meetingStoneButton.characterEntry = nil
        row.widgets.meetingStoneButton.mkGroupQuickAction = nil
        row.widgets.meetingStoneButton.mkGroupQuickActionIsCurrentPlayer = nil
        row.widgets.meetingStoneButton:Hide()
    end
    for _, roleKey in ipairs(self:GetRoleOrder()) do
        local roleFrame = row.widgets.roles[roleKey]
        if roleFrame then
            roleFrame:Hide()
        end
    end
    if row.widgets.rolesUnavailableLabel then
        row.widgets.rolesUnavailableLabel:Hide()
    end
    MK:SetGroupRowBackground(row)
    SetGroupRowHoverColor(row, GROUP_LIST_ROW_HOVER_COLOR_NORMAL)
    if row.widgets.backgroundTransition then
        row.widgets.backgroundTransition:Hide()
    end
    if row.widgets.hoverLeft then
        row.widgets.hoverLeft:Hide()
    end
    if row.widgets.hover then
        row.widgets.hover:Hide()
    end
    if row.widgets.hoverRight then
        row.widgets.hoverRight:Hide()
    end
    row.widgets.groupDivider:Hide()
    if row.widgets.rowStripe then
        row.widgets.rowStripe:Hide()
    end
    if row.widgets.rowStripeTransition then
        row.widgets.rowStripeTransition:Hide()
    end
    if row.HighlightTexture then
        row.HighlightTexture:Hide()
    end
    row:Hide()
end

function MK:SetGroupRatingLabel(label, rating, options)
    if not label then
        return
    end
    options = options or {}
    StopGroupRatingPendingAnimation(options.row)

    local score = tonumber(rating)
    if score and score > 0 then
        score = math.floor(score + 0.5)
        label:SetText(tostring(score))

        if self.GetDungeonScoreColor then
            local r, g, b = self:GetDungeonScoreColor(score)
            label:SetTextColor(r, g, b, 1)
            return
        end

        label:SetTextColor(1, 1, 1, 1)
        return
    end

    label:SetTextColor(
        GROUP_LIST_RATING_FAILED_TEXT_COLOR[1],
        GROUP_LIST_RATING_FAILED_TEXT_COLOR[2],
        GROUP_LIST_RATING_FAILED_TEXT_COLOR[3],
        GROUP_LIST_RATING_FAILED_TEXT_COLOR[4] or 1
    )

    if options.state == "pending" then
        if not StartGroupRatingPendingAnimation(options.row, label) then
            label:SetText(TRANSLATE("查询中"))
        end
        return
    end

    if options.state == "failed" then
        label:SetText(TRANSLATE("查询失败"))
        return
    end

    if options.state == "no_source" then
        label:SetText(TRANSLATE("查询失败"))
        return
    end

    if options.state == "timeout" then
        label:SetText(TRANSLATE("查询超时"))
        return
    end

    if options.state == "no_score" then
        label:SetText(TRANSLATE("无评分"))
        return
    end

    label:SetText(TRANSLATE("查询失败"))
end

local function RefreshVisibleGroupRows(widgets, refreshRow)
    if not (widgets and type(refreshRow) == "function") then
        return false
    end

    if widgets.groupScrollBox and widgets.groupScrollBox.ForEachFrame then
        widgets.groupScrollBox:ForEachFrame(function(row)
            refreshRow(row)
        end)
        return true
    end

    if type(widgets.groupRowFrames) == "table" then
        for _, row in ipairs(widgets.groupRowFrames) do
            if row and (not row.IsShown or row:IsShown()) then
                refreshRow(row)
            end
        end
        return true
    end

    return false
end

function MK:RefreshVisibleGroupAccountTypeRows(widgets)
    return RefreshVisibleGroupRows(widgets, function(row)
        if row and row.mkCharacterEntry and self.SetGroupAccountTypeIcon then
            self:SetGroupAccountTypeIcon(row, row.mkCharacterEntry, row.mkDisplayState)
        end
    end)
end

function MK:RefreshVisibleGroupRatingRows(widgets)
    return RefreshVisibleGroupRows(widgets, function(row)
        if not (row and row.mkCharacterEntry and row.widgets and row.widgets.ratingLabel) then
            return
        end

        local ratingValue = self.GetGroupRatingDisplayScore and self:GetGroupRatingDisplayScore(row.mkCharacterEntry)
            or (row.mkCharacterEntry.data and row.mkCharacterEntry.data.rating)
        local ratingLookupState = self.GetGroupRatingLookupState
            and self:GetGroupRatingLookupState(row.mkCharacterEntry, row.mkDisplayState)
            or nil
        self:SetGroupRatingLabel(row.widgets.ratingLabel, ratingValue, {
            row = row,
            state = ratingLookupState,
        })
    end)
end

function MK:RefreshVisibleGroupRowStates(widgets, refreshSource)
    if refreshSource == "NetEaseAPI" then
        return self:RefreshVisibleGroupAccountTypeRows(widgets)
    end

    if refreshSource == "GroupUnitRatingLookup" or refreshSource == "GroupRatingLookupTimeout" then
        return self:RefreshVisibleGroupRatingRows(widgets)
    end

    return false
end

local function FormatGroupRunTime(durationMS)
    local duration = tonumber(durationMS) or 0
    if duration <= 0 then
        return ""
    end

    local totalSeconds = duration / 1000
    local minutes = math.floor(totalSeconds / 60)
    local seconds = totalSeconds - (minutes * 60)
    return string.format("%d:%04.1f", minutes, seconds)
end

local function ColorText(text, color)
    return string.format("|c%s%s|r", color or "ffffffff", tostring(text or ""))
end

local function GetClassColorString(classToken)
    local classColors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    local color = classToken and classColors and classColors[classToken]
    if not color then
        return "ffffd100"
    end

    if color.colorStr then
        return color.colorStr
    end

    return string.format("ff%02x%02x%02x", math.floor((color.r or 1) * 255), math.floor((color.g or 0.82) * 255), math.floor((color.b or 0) * 255))
end

local function GetCurrentPlayerShortName()
    local name = UnitName and UnitName("player")
    if type(name) == "string" and name ~= "" then
        return name
    end

    return TRANSLATE("战团")
end

local function GetCurrentPlayerOwnerKey()
    local name = UnitName and UnitName("player") or TRANSLATE("战团")
    local realm = GetRealmName and GetRealmName() or nil
    if type(realm) == "string" and realm ~= "" and type(name) == "string" and not name:find("-", 1, true) then
        return string.format("%s-%s", name, realm)
    end

    return name
end

local function GetShortNameFromCharacterKey(characterKey)
    if type(characterKey) ~= "string" or characterKey == "" then
        return nil
    end

    return characterKey:match("^([^-]+)") or characterKey
end

local function FormatWarbandGroupCharacterName(owner, character)
    local characterName = (character and character.name) or TRANSLATE("角色选项卡")
    local ownerName = GetShortNameFromCharacterKey(owner) or GetCurrentPlayerShortName()
    local suffix = string.format(TRANSLATE("%s的战团"), ownerName)
    local classColor = GetClassColorString(character and character.class)
    return string.format("%s-%s", ColorText(characterName, classColor), ColorText(suffix, "ff58d7ff"))
end

local function GetWarbandSourceDisplayName(characterEntry)
    local sourceName = select(1, GetWarbandSourceDisplayInfo(characterEntry))
    if type(sourceName) == "string" and sourceName ~= "" then
        return sourceName
    end

    return GetShortNameFromCharacterKey(characterEntry and characterEntry.owner) or GetCurrentPlayerShortName()
end

local function FormatWarbandSourceDisplayName(characterEntry)
    local sourceName, sourceClass = GetWarbandSourceDisplayInfo(characterEntry)
    if type(sourceName) ~= "string" or sourceName == "" then
        sourceName = GetWarbandSourceDisplayName(characterEntry)
    end

    return ColorText(sourceName, GetClassColorString(sourceClass))
end

local function GetScoreColorString(score)
    score = tonumber(score) or 0
    if score <= 0 then
        return "ff777777"
    end

    if MK.GetDungeonScoreColorString then
        return MK:GetDungeonScoreColorString(score)
    end

    return "ffffffff"
end

local function GetSpecificScoreColorString(score)
    score = tonumber(score) or 0
    if score <= 0 then
        return "ff777777"
    end

    if MK.GetSpecificDungeonScoreColorString then
        return MK:GetSpecificDungeonScoreColorString(score)
    end

    return "ffffffff"
end

local function GetCharacterBestRunsMap(bestRuns)
    local runsByMapID = {}
    if type(bestRuns) ~= "table" or type(bestRuns.runs) ~= "table" then
        return runsByMapID
    end

    for _, run in ipairs(bestRuns.runs) do
        local mapID = tonumber(run.mapID)
        if mapID then
            runsByMapID[mapID] = run
        end
    end

    return runsByMapID
end

function MK:GetGroupCharacterTooltipBestRuns(characterEntry, isOffline)
    local character = characterEntry and characterEntry.data or nil
    if type(character) ~= "table" then
        return nil
    end

    if type(character.bestRuns) == "table" then
        return character.bestRuns, true
    end

    if isOffline then
        return nil, false
    end

    local identifier = character.name or characterEntry.key or characterEntry.owner
    if type(identifier) ~= "string" or identifier == "" then
        return nil, false
    end

    if self.GetPlayerMythicPlusBestRuns then
        return self:GetPlayerMythicPlusBestRuns(identifier), false
    end

    return nil, false
end

function MK:ShowGroupCharacterTooltip(anchor, characterEntry, displayState)
    local character = characterEntry and characterEntry.data or nil
    if not anchor or type(character) ~= "table" then
        return
    end

    local isCarpoolEntry = displayState and displayState.isCarpoolEntry == true
    local isOffline = (not isCarpoolEntry) and displayState and displayState.isOffline
    local isRosterOnly = (not isCarpoolEntry) and displayState and displayState.isRosterOnly
    local bestRuns, isCached = self:GetGroupCharacterTooltipBestRuns(characterEntry, isOffline)
    local displayScore = self.GetGroupRatingDisplayScore and self:GetGroupRatingDisplayScore(characterEntry) or nil
    local score = (bestRuns and bestRuns.score) or displayScore or character.rating or 0
    local runsByMapID = GetCharacterBestRunsMap(bestRuns)
    local nameText = self.GetCharacterDisplayNameColored and self:GetCharacterDisplayNameColored(character)
        or character.name
        or characterEntry.key
        or TRANSLATE("未知地下城")
    local unavailableText = TRANSLATE("无记录")
    local specText = type(character.specName) == "string" and character.specName ~= "" and character.specName or TRANSLATE("未知")

    GameTooltip:SetOwner(anchor, "ANCHOR_RIGHT")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(nameText)
    if not isCarpoolEntry then
        local statusText = isOffline and TRANSLATE("离线") or TRANSLATE("在线")
        local statusColor = isOffline and "ff888888" or "ff1eff00"
        GameTooltip:AddDoubleLine(TRANSLATE("状态"), ColorText(statusText, statusColor), 0.82, 0.82, 0.82, 1, 1, 1)
    end
    GameTooltip:AddDoubleLine(TRANSLATE("当前钥石"), character.keystoneLink or self:GetCharacterKeystoneText(character), 0.82, 0.82, 0.82, 1, 1, 1)
    GameTooltip:AddDoubleLine(TRANSLATE("赛季评分"), ColorText(math.floor((tonumber(score) or 0) + 0.5), GetScoreColorString(score)), 0.82, 0.82, 0.82, 1, 1, 1)
    GameTooltip:AddDoubleLine(TRANSLATE("当前专精"), ColorText(specText, GetClassColorString(character.class)), 0.82, 0.82, 0.82, 1, 1, 1)

    if isCached and isOffline then
        GameTooltip:AddLine(TRANSLATE("该玩家离线，正在显示最后一次缓存数据。"), 0.58, 0.58, 0.58, true)
    elseif isRosterOnly then
        GameTooltip:AddLine(TRANSLATE("当前只能获得该玩家的队伍名册信息。"), 0.58, 0.58, 0.58, true)
    end

    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(TRANSLATE("最佳副本记录"), 1, 0.82, 0)

    local mapIDs = self.GetCurrentSeasonDungeonMapIDs and self:GetCurrentSeasonDungeonMapIDs() or {}
    if #mapIDs == 0 then
        GameTooltip:AddLine(TRANSLATE("无法加载当前赛季。"), 0.58, 0.58, 0.58, true)
    else
        for _, mapID in ipairs(mapIDs) do
            local run = runsByMapID[mapID]
            local dungeonName = self:GetAbbreviatedDungeonName(mapID)
            local leftText
            local rightText
            if run and (tonumber(run.level) or 0) > 0 then
                local levelColor = run.timed and "ff1eff00" or "ffff4040"
                local scoreColor = GetSpecificScoreColorString(run.score)
                local timeText = FormatGroupRunTime(run.durationMS)
                leftText = string.format("%s  %s", ColorText(string.format("%02d", tonumber(run.level) or 0), levelColor), dungeonName)
                rightText = ColorText(math.floor((tonumber(run.score) or 0) + 0.5), scoreColor)
                if timeText ~= "" then
                    rightText = string.format("%s  %s", ColorText("(" .. timeText .. ")", levelColor), rightText)
                end
            else
                leftText = string.format("%s  %s", ColorText("  ", "ff777777"), dungeonName)
                rightText = ColorText(unavailableText, "ff777777")
            end

            GameTooltip:AddDoubleLine(leftText, rightText, 1, 1, 1, 1, 1, 1)
        end
    end

    GameTooltip:Show()
end

function MK:HideGroupCharacterTooltip()
    GameTooltip:Hide()
end

function MK:RefreshGroupCharacterRowFrame(row, characterEntry, displayState)
    local character = characterEntry.data
    local roles = displayState and displayState.roles
    if type(roles) ~= "table" then
        roles = self.GetCharacterEntryDisplayRoles and self:GetCharacterEntryDisplayRoles(characterEntry)
            or self:NormalizeRoles(character)
    end
    local hasDisplayRoles = roles and next(roles) ~= nil
    local isOffline = displayState and displayState.isOffline
    local isRosterOnly = displayState and displayState.isRosterOnly
    local isWarbandEntry = displayState and displayState.isWarbandEntry == true
    local isCarpoolEntry = displayState and displayState.isCarpoolEntry == true
    if isCarpoolEntry then
        isOffline = false
    end
    local isCurrentGroupMember = displayState and displayState.isCurrentGroupMember
    if isCurrentGroupMember == nil then
        isCurrentGroupMember = not isRosterOnly and not isWarbandEntry and not isCarpoolEntry
    end
    if not displayState and self.IsCharacterEntryCurrentlyInGroup then
        isCurrentGroupMember = self:IsCharacterEntryCurrentlyInGroup(characterEntry)
    end
    local unavailableText = core.GROUP_LIST_UNAVAILABLE_TEXT or "-"
    local unavailableColor = core.GROUP_LIST_UNAVAILABLE_TEXT_COLOR or { 0.52, 0.52, 0.52, 1 }

    row.characterKey = characterEntry.key
    row.mkCharacterEntry = characterEntry
    row.mkDisplayState = displayState
    row.widgets.nameLabel:SetTextColor(1, 1, 1, 1)
    row.widgets.armorLabel:SetTextColor(1, 1, 1, 1)
    row.widgets.keystoneText:SetTextColor(1, 1, 1, 1)

    if isCarpoolEntry then
        row.widgets.nameLabel:SetText(self:GetCharacterDisplayNameColored(character))
    elseif displayState and displayState.isWarbandEntry then
        row.widgets.nameLabel:SetText(FormatWarbandGroupCharacterName(displayState.warbandOwnerName, character))
    else
        row.widgets.nameLabel:SetText(self:GetCharacterDisplayNameColored(character))
    end
    if row.widgets.specIcon then
        local specIconTexture = core.GetSpecIconTexture and core.GetSpecIconTexture(character.specID, character.class or character.classFile) or nil
        if specIconTexture then
            row.widgets.specIcon:SetTexture(specIconTexture)
            row.widgets.specIcon:SetTexCoord(0, 1, 0, 1)
            row.widgets.specIcon:SetVertexColor(1, 1, 1, 0.95)
            row.widgets.specIcon:Show()
        else
            row.widgets.specIcon:SetTexture(nil)
            row.widgets.specIcon:Hide()
        end
        LayoutGroupNameColumn(row)
    end

    local armorText = self:GetArmorType(character)
    if isRosterOnly and armorText == "-" then
        row.widgets.armorLabel:SetTextColor(unavailableColor[1], unavailableColor[2], unavailableColor[3], unavailableColor[4] or 1)
        row.widgets.armorLabel:SetText(unavailableText)
    else
        row.widgets.armorLabel:SetText(armorText)
    end

    row.widgets.keystoneLink.keystoneLink = character.keystoneLink
    if isRosterOnly and not character.keystoneLink and not character.hasKeystone then
        row.widgets.keystoneText:SetTextColor(unavailableColor[1], unavailableColor[2], unavailableColor[3], unavailableColor[4] or 1)
        row.widgets.keystoneText:SetText(unavailableText)
    else
        row.widgets.keystoneText:SetText(character.keystoneLink or self:GetCharacterKeystoneText(character))
    end
    local ratingValue = self.GetGroupRatingDisplayScore and self:GetGroupRatingDisplayScore(characterEntry) or character.rating
    local ratingLookupState = self.GetGroupRatingLookupState and self:GetGroupRatingLookupState(characterEntry, displayState) or nil
    self:SetGroupRatingLabel(row.widgets.ratingLabel, ratingValue, {
        row = row,
        state = ratingLookupState,
    })
    if row.widgets.accountTypeLabel then
        row.widgets.accountTypeLabel:SetText("")
    end
    if row.widgets.meetingStoneLabel then
        row.widgets.meetingStoneLabel:SetText("")
        row.widgets.meetingStoneLabel:Hide()
    end
    if row.widgets.meetingStoneButton then
        row.widgets.meetingStoneButton.characterEntry = nil
        row.widgets.meetingStoneButton.mkGroupQuickAction = nil
        row.widgets.meetingStoneButton.mkGroupQuickActionIsCurrentPlayer = nil
        row.widgets.meetingStoneButton:Hide()
    end
    characterEntry.isOffline = isOffline or nil
    self:SetGroupAccountTypeIcon(row, characterEntry, displayState)
    if row.widgets.meetingStoneLabel and isCarpoolEntry then
        row.widgets.meetingStoneLabel:SetText(FormatWarbandSourceDisplayName(characterEntry))
        row.widgets.meetingStoneLabel:SetTextColor(1, 1, 1, 1)
        row.widgets.meetingStoneLabel:Show()
    elseif row.widgets.meetingStoneLabel and isRosterOnly then
        row.widgets.meetingStoneLabel:SetText(unavailableText)
        row.widgets.meetingStoneLabel:SetTextColor(unavailableColor[1], unavailableColor[2], unavailableColor[3], unavailableColor[4] or 1)
        row.widgets.meetingStoneLabel:Show()
    elseif row.widgets.meetingStoneButton and character.mapID and character.keyLevel and (not isRosterOnly) and not isCarpoolEntry then
        local meetingStoneAvailable = not self.IsMeetingStoneManagementAvailable or self:IsMeetingStoneManagementAvailable()
        local listing = core.MeetingStoneListing
        local canManageMeetingStone = not listing or not listing.CanManageListing or listing.CanManageListing() == true
        local canCreateMeetingStone = meetingStoneAvailable
            and isCurrentGroupMember
            and canManageMeetingStone
        local canSendKeystone = not canCreateMeetingStone
            and self.CanSendGroupKeystoneInfo
            and self:CanSendGroupKeystoneInfo(characterEntry)
        local canUseQuickAction = canCreateMeetingStone or canSendKeystone

        if canCreateMeetingStone then
            row.widgets.meetingStoneButton:SetText(TRANSLATE("创建招募"))
            row.widgets.meetingStoneButton.mkGroupQuickAction = "createListing"
        else
            row.widgets.meetingStoneButton:SetText(TRANSLATE("发送钥石"))
            row.widgets.meetingStoneButton.mkGroupQuickAction = "sendKeystone"
        end
        row.widgets.meetingStoneButton.mkGroupQuickActionIsCurrentPlayer = IsCurrentPlayerQuickActionEntry(characterEntry)
        if canUseQuickAction then
            row.widgets.meetingStoneButton.characterEntry = characterEntry
            row.widgets.meetingStoneButton:Enable()
        else
            row.widgets.meetingStoneButton.characterEntry = nil
            row.widgets.meetingStoneButton:Disable()
        end
        if row.widgets.meetingStoneButton.EnableMouse then
            row.widgets.meetingStoneButton:EnableMouse(canUseQuickAction == true)
        end
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(row.widgets.meetingStoneButton)
        end
        row.widgets.meetingStoneButton:Show()
    end
    MK:ResetGroupRowBackgroundAnchors(row)
    MK:SetGroupRowStripe(row, isOffline and GROUP_LIST_ROW_TEXTURE_GREY or GROUP_LIST_ROW_TEXTURE_NORMAL)
    SetGroupRowHoverColor(row, isOffline and GROUP_LIST_ROW_HOVER_COLOR_GREY or GROUP_LIST_ROW_HOVER_COLOR_NORMAL)
    if row.widgets.teleportButton and row.widgets.teleportIcon then
        local mapID = character.mapID
        local teleportStatus = GetDungeonTeleportStatus(mapID)
        local teleportMacroText = GetDungeonTeleportMacroText(mapID)
        local canUseTeleport = (not isRosterOnly) and mapID and teleportStatus == "ready" and teleportMacroText ~= nil

        row.widgets.teleportButton.mapID = mapID
        row.widgets.teleportButton.mkTeleportStatus = teleportStatus
        row.widgets.teleportButton.mkTeleportUnavailable = isRosterOnly or not mapID
        row.widgets.teleportButton.mkTeleportUnavailableColor = unavailableColor
        local _, _, effectiveMapID = ApplyDungeonTeleportButtonSecureState(row.widgets.teleportButton, canUseTeleport, teleportMacroText, mapID)
        row.widgets.teleportButton.mapID = effectiveMapID
        if effectiveMapID ~= mapID then
            row.widgets.teleportButton.mkTeleportStatus = GetDungeonTeleportStatus(effectiveMapID)
            row.widgets.teleportButton.mkTeleportUnavailable = isRosterOnly or not effectiveMapID
        end
        RefreshGroupTeleportButtonAvailabilityVisual(row.widgets.teleportButton)
    end
    for _, roleKey in ipairs(self:GetRoleOrder()) do
        local roleFrame = row.widgets.roles[roleKey]
        if roleFrame then
            roleFrame:ClearAllPoints()
            if roles[roleKey] then
                roleFrame:Show()
                if roleFrame.icon then
                    roleFrame.icon:Show()
                end
            else
                roleFrame:Hide()
            end
        end
    end
    if row.widgets.rolesUnavailableLabel then
        if isRosterOnly and not hasDisplayRoles then
            row.widgets.rolesUnavailableLabel:SetText(unavailableText)
            row.widgets.rolesUnavailableLabel:SetTextColor(unavailableColor[1], unavailableColor[2], unavailableColor[3], unavailableColor[4] or 1)
            row.widgets.rolesUnavailableLabel:Show()
        else
            row.widgets.rolesUnavailableLabel:Hide()
        end
    end
    if hasDisplayRoles then
        MK:CenterGroupRowRoles(row)
    end

    MK:SetGroupRowBackground(row)

    row.widgets.groupDivider:Hide()

    self:SetGroupRowFrameVisible(row, true)
end

---@param requiredCount integer
function MK:SyncGroupCharacterRows(requiredCount)
    local widgets = self.widgets
    if not widgets or not widgets.groupRowsFrame then
        return
    end

    widgets.groupRowFrames = widgets.groupRowFrames or {}

    while #widgets.groupRowFrames < requiredCount do
        widgets.groupRowFrames[#widgets.groupRowFrames + 1] = self:CreateGroupCharacterRowFrame(widgets.groupRowsFrame)
    end
end


local function RefreshGroupListTabInternal(self, widgets, refreshSource)
    local isCarpoolTab = self.activeTab == "carpool"
    local groupListKind = isCarpoolTab and "carpool" or "group"
    local emptyText = widgets.groupEmptyText or TRANSLATE(isCarpoolTab and "车队列表中没有战团角色" or "没有找到已共享钥石数据的队伍成员")

    if widgets.groupItemList and widgets.groupScrollBox then
        local groupCharacters = isCarpoolTab and self:GetCarpoolViewCharacters() or self:GetSharedGroupViewCharacters()
        if self.QueueGroupUnitRatingLookups then
            self:QueueGroupUnitRatingLookups(groupCharacters)
        end
        if core.AccountType and core.AccountType.QueueQueries then
            core.AccountType.QueueQueries(groupCharacters)
        end
        local activeGroupAliases = self:GetActiveGroupCharacterAliases()

        LayoutGroupListPanel(widgets)
        UpdateGroupHeaderSortIndicators(self, widgets.groupHeaderFrame)
        UpdateGroupColumnWidths(widgets, groupCharacters)

        local displayEntries = {}

        for index, characterEntry in ipairs(groupCharacters) do
            local isOffline = (not isCarpoolTab) and self:IsCharacterEntryOfflineInGroup(characterEntry) or false
            local isCurrentGroupMember = self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
            local isWarbandEntry = isCarpoolTab or not isCurrentGroupMember
            local displayRoles = self.GetCharacterEntryDisplayRoles and self:GetCharacterEntryDisplayRoles(characterEntry)
                or self:NormalizeRoles(characterEntry and characterEntry.data or nil)

            displayEntries[#displayEntries + 1] = {
                characterEntry = characterEntry,
                displayState = {
                    alternate = index % 2 == 0,
                    isWarbandEntry = isWarbandEntry,
                    isCarpoolEntry = isCarpoolTab,
                    isCurrentGroupMember = isCurrentGroupMember,
                    warbandOwnerName = isWarbandEntry and characterEntry.owner or nil,
                    warbandSourceName = isCarpoolTab and GetWarbandSourceDisplayName(characterEntry) or nil,
                    warbandSourceClass = isCarpoolTab and characterEntry.warbandSourceClass or nil,
                    showGroupDivider = false,
                    isOffline = isOffline,
                    isRosterOnly = characterEntry.isRosterOnly or (characterEntry.data and characterEntry.data.isRosterOnly),
                    roles = displayRoles,
                },
            }
        end

        if #displayEntries > 0 then
            widgets.groupEmpty:Hide()
            local retainScrollPosition = ShouldPreserveGroupScrollPosition(refreshSource)
            widgets.groupScrollBox:SetDataProvider(
                CreateDataProvider(displayEntries),
                retainScrollPosition and true or (ScrollBoxConstants and ScrollBoxConstants.DiscardScrollPosition)
            )
        else
            widgets.groupScrollBox:RemoveDataProvider()
            if widgets.groupScrollBox.ScrollToBegin then
                widgets.groupScrollBox:ScrollToBegin(ScrollBoxConstants.NoScrollInterpolation)
            end
            if core.ApplyPanelEmptyPromptStyle then
                core.ApplyPanelEmptyPromptStyle(widgets.groupEmpty)
            end
            widgets.groupEmpty:SetText(emptyText)
            widgets.groupEmpty:Show()
        end

        UpdateGroupScrollBarVisibility(widgets, #displayEntries)
        if C_Timer and C_Timer.After then
            local visibleEntryCount = #displayEntries
            C_Timer.After(0, function()
                UpdateGroupScrollBarVisibility(widgets, visibleEntryCount)
            end)
        end

        return
    end

    if not widgets.groupRowsFrame or not widgets.groupEmpty or not widgets.groupScrollFrame or not widgets.groupContent then
        return
    end

    local groupCharacters = isCarpoolTab and self:GetCarpoolViewCharacters() or self:GetSharedGroupViewCharacters()
    if self.QueueGroupUnitRatingLookups then
        self:QueueGroupUnitRatingLookups(groupCharacters)
    end
    if core.AccountType and core.AccountType.QueueQueries then
        core.AccountType.QueueQueries(groupCharacters)
    end
    local activeGroupAliases = self:GetActiveGroupCharacterAliases()

    local hasGroupCharacters = #groupCharacters > 0

    if hasGroupCharacters then
        widgets.groupEmpty:SetText("")
        widgets.groupEmpty:Hide()
    else
        if core.ApplyPanelEmptyPromptStyle then
            core.ApplyPanelEmptyPromptStyle(widgets.groupEmpty)
        end
        widgets.groupEmpty:SetText(emptyText)
        widgets.groupEmpty:Show()
    end

    self:SyncGroupCharacterRows(#groupCharacters)

    for index, characterEntry in ipairs(groupCharacters) do
        local isOffline = (not isCarpoolTab) and self:IsCharacterEntryOfflineInGroup(characterEntry) or false
        local isCurrentGroupMember = self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
        local isWarbandEntry = isCarpoolTab or not isCurrentGroupMember
        local displayRoles = self.GetCharacterEntryDisplayRoles and self:GetCharacterEntryDisplayRoles(characterEntry)
            or self:NormalizeRoles(characterEntry and characterEntry.data or nil)

        self:RefreshGroupCharacterRowFrame(widgets.groupRowFrames[index], characterEntry, {
            alternate = index % 2 == 0,
            isWarbandEntry = isWarbandEntry,
            isCarpoolEntry = isCarpoolTab,
            isCurrentGroupMember = isCurrentGroupMember,
            warbandOwnerName = isWarbandEntry and characterEntry.owner or nil,
            warbandSourceName = isCarpoolTab and GetWarbandSourceDisplayName(characterEntry) or nil,
            warbandSourceClass = isCarpoolTab and characterEntry.warbandSourceClass or nil,
            showGroupDivider = false,
            isOffline = isOffline,
            isRosterOnly = characterEntry.isRosterOnly or (characterEntry.data and characterEntry.data.isRosterOnly),
            roles = displayRoles,
        })
    end

    for index = #groupCharacters + 1, #(widgets.groupRowFrames or {}) do
        self:SetGroupRowFrameVisible(widgets.groupRowFrames[index], false)
    end

    UpdateGroupColumnWidths(widgets, groupCharacters)

    local currentYOffset = -GROUP_LIST_ROWS_PADDING_TOP
    for index, row in ipairs(widgets.groupRowFrames or {}) do
        if row:IsShown() then
            row:ClearAllPoints()
            if widgets.groupBrowseCenterList then
                row:SetPoint("TOP", widgets.groupRowsFrame, "TOP", 0, currentYOffset)
            else
                row:SetPoint("TOPLEFT", widgets.groupRowsFrame, "TOPLEFT", GROUP_LIST_ROWS_PADDING_X, currentYOffset)
                row:SetPoint("TOPRIGHT", widgets.groupRowsFrame, "TOPRIGHT", -GROUP_LIST_ROWS_PADDING_X, currentYOffset)
            end
            currentYOffset = currentYOffset - GROUP_LIST_ROW_HEIGHT
        end
    end

    local rowsHeight = -currentYOffset + GROUP_LIST_ROWS_PADDING_BOTTOM
    if not hasGroupCharacters then
        local emptyHeight = (widgets.groupEmpty:GetStringHeight() or 0) + 28
        rowsHeight = math.max(GROUP_LIST_EMPTY_MIN_HEIGHT, emptyHeight)
    end

    widgets.groupRowsContentHeight = math.max(rowsHeight, 1)
    LayoutGroupListPanel(widgets)
    UpdateNativeScrollState(widgets.groupScrollFrame, widgets.groupContent, widgets.groupScrollBar)
    return
end

function MK:RefreshGroupListTab(widgets, refreshSource, refreshDetail)
    if not (self.BeginGroupListRosterCache and self.EndGroupListRosterCache) then
        return RefreshGroupListTabInternal(self, widgets, refreshSource, refreshDetail)
    end

    self:BeginGroupListRosterCache()
    local ok, result = pcall(RefreshGroupListTabInternal, self, widgets, refreshSource, refreshDetail)
    self:EndGroupListRosterCache()
    if not ok then
        error(result, 2)
    end

    return result
end


core.GroupListView = {
    Refresh = function(owner, widgets)
        return owner:RefreshGroupListTab(widgets)
    end,
}
