local addonName, addonTable = ...
---@type MKA
local MKA = addonTable.MKA
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_SOURCE_WIDTH = 72
local GROUP_LIST_MEETINGSTONE_BUTTON_SOURCE_HEIGHT = 26
local GROUP_LIST_MEETINGSTONE_BUTTON_HEIGHT = 24
local GROUP_LIST_MEETINGSTONE_BUTTON_WIDTH = math.floor(GROUP_LIST_MEETINGSTONE_BUTTON_SOURCE_WIDTH * GROUP_LIST_MEETINGSTONE_BUTTON_HEIGHT / GROUP_LIST_MEETINGSTONE_BUTTON_SOURCE_HEIGHT + 0.5)
local GROUP_LIST_MEETINGSTONE_BUTTON_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_ACCOUNT_TYPE_ICON_INSET_X = 12
local GROUP_LIST_ROW_TEXTURE_NORMAL = "Interface\\AddOns\\" .. addonName .. "\\Art\\Rows\\ApplicantRowNormal.png"
local MAIN_PANEL_CONTENT_PADDING_X = 18
local GROUP_LIST_RATING_FAILED_TEXT_COLOR = { 0.52, 0.52, 0.52, 1 }

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 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 = "presence", direction = "desc" }
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 GetShortNameFromCharacterKey = core.GetShortNameFromCharacterKey or function(characterKey)
    return type(characterKey) == "string" and (characterKey:match("^([^-]+)") or characterKey) or 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 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
    if source == "libopenkeystone" then
        return true
    end
    if source == "libkeystone" or source == "mka" then
        return false
    end
    if (characterEntry and characterEntry.isRosterOnly == true) or character.isRosterOnly == true then
        return true
    end
    if owner and owner.IsCharacterEntryCurrentlyInGroup and owner:IsCharacterEntryCurrentlyInGroup(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.mkaGroupMeetingStoneButtonStyled 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.mkaGroupMeetingStoneButtonStyled = true
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.mkaGroupListKind)

    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.mkaTransmogHeaderBackground then
                button.mkaTransmogHeaderBackground:SetAlpha(isActive and 1 or 0.9)
                button.mkaTransmogHeaderBackground: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.mkaGroupListKind = GetGroupListKind(options.tabKey)

    local columnWidths = options.columnWidths or core.GROUP_LIST_COLUMN_WIDTHS
    local _, sortInitialDirection = GetGroupSortConfig(frame.mkaGroupListKind)
    local labels = {
        { key = "presence", text = "", width = columnWidths.presence },
        { key = "name", text = "COMMON.CHARACTER", width = columnWidths.name },
        { key = "armor", text = "COLUMN.ARMOR", width = columnWidths.armor },
        { key = "key", text = "COLUMN.KEYSTONE", width = columnWidths.key },
        { key = "rating", text = "COLUMN.RATING", width = columnWidths.rating },
        { key = "roles", text = "COLUMN.ROLES", width = columnWidths.roles },
        { key = "tp", text = "COLUMN.TELEPORT", width = columnWidths.tp },
        { key = "accountType", text = "COLUMN.ACCOUNT_TYPE", width = columnWidths.accountType },
        { key = "meetingStone", text = options.lastColumnText or "TAB.MEETINGSTONE", 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.mkaGroupListKind = frame.mkaGroupListKind
        if data.key ~= "presence" then
            ApplyGroupHeaderButtonStyle(label)
        end

        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] and data.key ~= "presence" 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(MKA, 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()
        if data.key == "presence" then
            arrow:SetPoint("CENTER", label, "CENTER", 0, 0)
        else
            arrow:SetPoint("RIGHT", label, "RIGHT", -9, 0)
        end
        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
                MKA:SetGroupSort(button.sortKey, button.mkaGroupListKind)
            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(MKA, 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

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

    return frame
end
core.CreateGroupHeaderFrame = CreateGroupHeaderFrame

function MKA: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 MKA: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

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

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

function MKA: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 MKA: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 MKA:SetGroupRowStripe(row)
    if not (row and row.widgets and row.widgets.rowStripe) then
        return
    end

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

function MKA: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.mkaGroupBackgroundKey = nil
    row.mkaGroupBackgroundVisible = nil
end

function MKA: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(MKA: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.mkaRoleIconSize 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.mkaRoleIconSize 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 MKA: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)
        MKA:SetGroupRowHover(button, true)
    end)
    row:SetScript("OnLeave", function(button)
        MKA: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 presenceFrame = CreateFrame("Frame", nil, row)
    presenceFrame:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.presence.x, 0)
    presenceFrame:SetSize(core.GROUP_LIST_COLUMN_WIDTHS.presence, 18)

    local presenceMarkerBorder = presenceFrame:CreateTexture(nil, "ARTWORK", nil, -1)
    presenceMarkerBorder:SetPoint("TOP", presenceFrame, "TOP", 0, 0)
    presenceMarkerBorder:SetPoint("BOTTOM", presenceFrame, "BOTTOM", 0, 0)
    presenceMarkerBorder:SetWidth(5)
    presenceMarkerBorder:SetTexture("Interface\\Buttons\\WHITE8X8")
    presenceMarkerBorder:SetVertexColor(0, 0, 0, 0.92)
    presenceMarkerBorder:Hide()

    local presenceMarker = presenceFrame:CreateTexture(nil, "ARTWORK")
    presenceMarker:SetPoint("TOP", presenceFrame, "TOP", 0, -1)
    presenceMarker:SetPoint("BOTTOM", presenceFrame, "BOTTOM", 0, 1)
    presenceMarker:SetWidth(3)
    presenceMarker:SetTexture("Interface\\Buttons\\WHITE8X8")
    presenceMarker:Hide()

    local nameLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nameLabel:SetPoint("LEFT", row, "LEFT", defaultColumnLayout.name.x, 0)
    nameLabel:SetWidth(core.GROUP_LIST_COLUMN_WIDTHS.name)
    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)
        MKA:SetGroupRowHover(row, true)
        if row.mkaCharacterEntry then
            MKA:ShowGroupCharacterTooltip(frame, row.mkaCharacterEntry, row.mkaDisplayState)
        end
    end)
    nameHoverFrame:SetScript("OnLeave", function()
        MKA:SetGroupRowHover(row, false)
        MKA: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)
        MKA:SetGroupRowHover(row, true)
        local link = button.keystoneLink
        if not link then
            return
        end
        GameTooltip:SetOwner(button, "ANCHOR_CURSOR")
        GameTooltip:SetHyperlink(link)
        GameTooltip:Show()
    end)
    keystoneButton:SetScript("OnLeave", function()
        MKA:SetGroupRowHover(row, false)
        MKA: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", "macro", "type1", "macro")

    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.mkaTeleportIcon = teleportIcon
    MKA:SetTeleportButtonVisualState(teleportButton, "normal")

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

        GameTooltip:SetOwner(button, "ANCHOR_CURSOR")
        AddDungeonTeleportTooltipLine(GameTooltip, mapID)
        GameTooltip:Show()
    end)
    teleportButton:SetScript("OnLeave", function(button)
        MKA:SetGroupRowHover(row, false)
        MKA:SetTeleportButtonVisualState(button, "normal")
        HideKeystoneTooltip()
    end)
    teleportButton:SetScript("OnMouseDown", function(button)
        MKA:SetTeleportButtonVisualState(button, "pushed")
    end)
    teleportButton:SetScript("OnMouseUp", function(button)
        if button.IsMouseOver and button:IsMouseOver() then
            MKA:SetTeleportButtonVisualState(button, "hover")
        else
            MKA:SetTeleportButtonVisualState(button, "normal")
        end
    end)
    AttachDungeonTeleportDialogTrigger(MKA, 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(MKA: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.mkaAccountTypeAlign = "LEFT"
    accountTypeContainer.mkaAccountTypeInsetX = GROUP_LIST_ACCOUNT_TYPE_ICON_INSET_X
    accountTypeContainer.mkaAccountTypeIconSlotSize = accountTypeIconSlotSize
    accountTypeContainer.mkaAccountTypeCenterLabel = true
    accountTypeContainer.mkaAccountTypeReserveCenteredLabelIconSpace = 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.mkaAccountTypeIconSlot = 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("MEETINGSTONE.ACTION.CREATE"))
    ApplyGroupMeetingStoneButtonStyle(meetingStoneButton)
    meetingStoneButton:SetScript("OnClick", function(button)
        if button.characterEntry then
            MKA:OpenMeetingStoneListingForCharacter(button.characterEntry)
        end
    end)
    meetingStoneButton:SetScript("OnEnter", function(button)
        MKA:SetGroupRowHover(row, true)
        MKA:HideGroupCharacterTooltip()
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(button)
        end
    end)
    meetingStoneButton:SetScript("OnLeave", function(button)
        MKA:SetGroupRowHover(row, false)
        MKA:HideGroupCharacterTooltip()
        button.mkaMeetingStoneButtonPressed = nil
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(button)
        end
    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 = {
        nameLabel = nameLabel,
        nameHoverFrame = nameHoverFrame,
        armorLabel = armorLabel,
        keystoneLink = keystoneButton,
        keystoneText = keystoneText,
        presenceFrame = presenceFrame,
        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,
        presenceMarkerBorder = presenceMarkerBorder,
        presenceMarker = presenceMarker,
    }

    return row
end

function MKA: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 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 = {
    presence = 14,
    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 MKA: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 widgets.groupBrowseFrame and widgets.groupBrowseFrame.GetWidth then
        rowWidth = math.max(rowWidth, (widgets.groupBrowseFrame:GetWidth() or 0) - (GROUP_LIST_ROWS_PADDING_X * 2))
    end
    if widgets.groupScrollBox and widgets.groupScrollBox.GetWidth then
        rowWidth = math.max(rowWidth, widgets.groupScrollBox:GetWidth() or 0)
    end
    if row.SetWidth then
        row:SetWidth(math.max(rowWidth, 1))
    end
    MKA:ResetGroupRowBackgroundAnchors(row)

    if row.widgets.presenceFrame then
        ApplyGroupRowColumn(row.widgets.presenceFrame, row, layout.presence, 18)
    end
    if row.widgets.nameLabel then
        ApplyGroupRowColumn(row.widgets.nameLabel, row, layout.name, 18)
    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
    MKA: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))

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

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

    if widgets.groupScrollBox and widgets.groupScrollBox.ForEachFrame then
        widgets.groupScrollBox:ForEachFrame(function(row)
            MKA: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

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

function MKA:ResetGroupRatingLookupState()
    self.groupRatingLookupStartedAt = {}
    self.groupRatingLookupRefreshTimers = {}
end

function MKA:GetGroupRatingDisplayScore(characterEntry)
    local character = characterEntry and characterEntry.data or {}
    local score = tonumber(character.rating)
    if score and score > 0 then
        return score, "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 MKA: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.lastRenderedSnapshotByTab then
            self.lastRenderedSnapshotByTab[self.activeTab] = nil
        end
        if self.RefreshMainWindow then
            self:RefreshMainWindow("GroupRatingLookupTimeout", lookupKey)
        end
    end)
end

function MKA:GetGroupRatingLookupState(characterEntry, displayState)
    local score = self:GetGroupRatingDisplayScore(characterEntry)
    local lookupKey = GetGroupRatingLookupKey(characterEntry)

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

    if not IsGroupRatingLikelyWaitingForData(self, characterEntry, displayState) then
        return "failed"
    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 "failed"
end

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

    if visible then
        row:Show()
        return
    end

    row.characterKey = nil
    row.mkaCharacterEntry = nil
    row.mkaDisplayState = nil
    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: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
    MKA:SetGroupRowBackground(row)
    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.presenceMarkerBorder then
        row.widgets.presenceMarkerBorder:Hide()
    end
    row.widgets.presenceMarker: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 MKA:SetGroupRatingLabel(label, rating, options)
    if not label then
        return
    end
    options = options or {}
    StopGroupRatingPendingAnimation(options.row)

    local score = tonumber(rating)
    if not score or score <= 0 then
        if options.state == "pending" then
            StartGroupRatingPendingAnimation(options.row, label)
            return
        end

        label:SetText(TRANSLATE("RATING.UNAVAILABLE"))
        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
        )
        return
    end

    score = math.floor(score + 0.5)
    label:SetText(tostring(score))

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

    label:SetTextColor(1, 1, 1, 1)
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("WARBAND.LABEL")
end

local function GetCurrentPlayerOwnerKey()
    local name = UnitName and UnitName("player") or TRANSLATE("WARBAND.LABEL")
    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("TAB.CHARACTERS")
    local ownerName = GetShortNameFromCharacterKey(owner) or GetCurrentPlayerShortName()
    local suffix = string.format(TRANSLATE("WARBAND.OWNER_FORMAT"), 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 = characterEntry and characterEntry.warbandSourceName
    if type(sourceName) == "string" and sourceName ~= "" then
        return sourceName
    end

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

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

    if MKA.GetRaiderIOScoreColorString then
        return MKA:GetRaiderIOScoreColorString(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 MKA: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 MKA:ShowGroupCharacterTooltip(anchor, characterEntry, displayState)
    local character = characterEntry and characterEntry.data or nil
    if not anchor or type(character) ~= "table" then
        return
    end

    local isOffline = displayState and displayState.isOffline
    local isRosterOnly = displayState and displayState.isRosterOnly
    local bestRuns, isCached = self:GetGroupCharacterTooltipBestRuns(characterEntry, isOffline)
    local score = (bestRuns and bestRuns.score) or character.rating or 0
    local runsByMapID = GetCharacterBestRunsMap(bestRuns)
    local nameText = character.name or characterEntry.key or TRANSLATE("DUNGEON.UNKNOWN")
    local titleColor = isOffline and "ff888888" or GetClassColorString(character.class)
    local statusText = isOffline and TRANSLATE("STATUS.OFFLINE") or TRANSLATE("STATUS.ONLINE")
    local statusColor = isOffline and "ff888888" or "ff1eff00"
    local unavailableText = TRANSLATE("COMMON.NO_RECORD")

    GameTooltip:SetOwner(anchor, "ANCHOR_CURSOR")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(ColorText(nameText, titleColor))
    GameTooltip:AddDoubleLine(TRANSLATE("STATUS.LABEL"), ColorText(statusText, statusColor), 0.82, 0.82, 0.82, 1, 1, 1)
    GameTooltip:AddDoubleLine(TRANSLATE("KEYSTONE.CURRENT"), character.keystoneLink or self:GetCharacterKeystoneText(character), 0.82, 0.82, 0.82, 1, 1, 1)
    GameTooltip:AddDoubleLine(TRANSLATE("RATING.SEASON"), ColorText(math.floor((tonumber(score) or 0) + 0.5), GetScoreColorString(score)), 0.82, 0.82, 0.82, 1, 1, 1)

    if isCached and isOffline then
        GameTooltip:AddLine(TRANSLATE("TOOLTIP.CACHED_WHILE_OFFLINE"), 0.58, 0.58, 0.58, true)
    elseif isRosterOnly then
        GameTooltip:AddLine(TRANSLATE("TOOLTIP.ROSTER_ONLY_DATA"), 0.58, 0.58, 0.58, true)
    end

    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(TRANSLATE("CHARACTER.BEST_DUNGEON_RECORDS"), 1, 0.82, 0)

    local mapIDs = self.GetCurrentSeasonDungeonMapIDs and self:GetCurrentSeasonDungeonMapIDs() or {}
    if #mapIDs == 0 then
        GameTooltip:AddLine(TRANSLATE("SEASON.ERROR.LOAD_FAILED"), 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 = GetScoreColorString(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", rightText, ColorText("(" .. timeText .. ")", levelColor))
                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 MKA:HideGroupCharacterTooltip()
    GameTooltip:Hide()
end

function MKA:RefreshGroupCharacterRowFrame(row, characterEntry, displayState)
    local character = characterEntry.data
    local roles = self.GetCharacterEntryDisplayRoles and self:GetCharacterEntryDisplayRoles(characterEntry)
        or self:NormalizeRoles(character)
    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
    local isCurrentGroupMember = not isRosterOnly and not isWarbandEntry and not isCarpoolEntry
    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.mkaCharacterEntry = characterEntry
    row.mkaDisplayState = 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 isOffline then
        row.widgets.nameLabel:SetTextColor(0.52, 0.52, 0.52, 1)
        row.widgets.nameLabel:SetText(character.name or characterEntry.key or unavailableText)
    elseif 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

    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:Hide()
    end
    characterEntry.isOffline = isOffline or nil
    self:SetGroupAccountTypeIcon(row, characterEntry)
    if row.widgets.meetingStoneLabel and isCarpoolEntry then
        row.widgets.meetingStoneLabel:SetText(ColorText(GetWarbandSourceDisplayName(characterEntry), "ff58d7ff"))
        row.widgets.meetingStoneLabel:SetTextColor(0.35, 0.85, 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 canUseMeetingStone = meetingStoneAvailable
            and isCurrentGroupMember
            and not isOffline
            and canManageMeetingStone
        row.widgets.meetingStoneButton:SetText(TRANSLATE("MEETINGSTONE.ACTION.CREATE"))
        if canUseMeetingStone 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(canUseMeetingStone == true)
        end
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(row.widgets.meetingStoneButton)
        end
        row.widgets.meetingStoneButton:Show()
    end
    MKA:ResetGroupRowBackgroundAnchors(row)
    MKA:SetGroupRowStripe(row)
    if row.widgets.teleportButton and row.widgets.teleportIcon then
        local mapID = character.mapID
        local teleportStatus = GetDungeonTeleportStatus(mapID)
        local teleportMacroText = GetDungeonTeleportMacroText(mapID)

        row.widgets.teleportButton.mapID = mapID
        SetSecureButtonAttributes(row.widgets.teleportButton, "macrotext", teleportMacroText, "macrotext1", teleportMacroText)

        if isRosterOnly or not mapID then
            row.widgets.teleportIcon:SetVertexColor(unavailableColor[1], unavailableColor[2], unavailableColor[3], 0.55)
            row.widgets.teleportIcon:SetAlpha(0.45)
        elseif teleportStatus == "ready" then
            row.widgets.teleportIcon:SetVertexColor(1, 1, 1, 0.92)
            row.widgets.teleportIcon:SetAlpha(1)
        elseif teleportStatus == "cooldown" or teleportStatus == "not_learned" then
            row.widgets.teleportIcon:SetVertexColor(1, 1, 1, 0.92)
            row.widgets.teleportIcon:SetAlpha(1)
        else
            row.widgets.teleportIcon:SetVertexColor(1, 1, 1, 0.55)
            row.widgets.teleportIcon:SetAlpha(0.7)
        end
    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 not isRosterOnly then
        MKA:CenterGroupRowRoles(row)
    end

    MKA:SetGroupRowBackground(row)

    row.widgets.groupDivider:Hide()

    local markerColor = displayState and displayState.presenceMarkerColor
    if markerColor then
        if row.widgets.presenceMarkerBorder then
            row.widgets.presenceMarkerBorder:Show()
        end
        row.widgets.presenceMarker:SetVertexColor(markerColor[1], markerColor[2], markerColor[3], markerColor[4] or 1)
        row.widgets.presenceMarker:Show()
    else
        if row.widgets.presenceMarkerBorder then
            row.widgets.presenceMarkerBorder:Hide()
        end
        row.widgets.presenceMarker:Hide()
    end

    self:SetGroupRowFrameVisible(row, true)
end

---@param requiredCount integer
function MKA: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


function MKA:RefreshGroupListTab(widgets)
    local isCarpoolTab = self.activeTab == "carpool"
    local groupListKind = isCarpoolTab and "carpool" or "group"
    local emptyText = widgets.groupEmptyText or TRANSLATE(isCarpoolTab and "CARPOOL.EMPTY.NO_WARBAND_CHARACTERS" or "GROUP.EMPTY.NO_SHARED_DATA")

    if widgets.groupItemList and widgets.groupScrollBox then
        local groupCharacters = isCarpoolTab and self:GetCarpoolViewCharacters() or self:GetSharedGroupViewCharacters()
        if core.MeetingStoneData and core.MeetingStoneData.QueryCharacters then
            core.MeetingStoneData:QueryCharacters(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 = self:IsCharacterEntryOfflineInGroup(characterEntry)
            local isCurrentGroupMember = self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
            local isWarbandEntry = isCarpoolTab or not isCurrentGroupMember
            local presenceMarkerColor = nil
            if isOffline then
                presenceMarkerColor = { 0.45, 0.45, 0.45, 1 }
            elseif isCurrentGroupMember or isCarpoolTab then
                local classToken = characterEntry.data and characterEntry.data.class
                local classColors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
                local color = classToken and classColors and classColors[classToken]
                if color then
                    presenceMarkerColor = { color.r or 1, color.g or 1, color.b or 1, 1 }
                else
                    presenceMarkerColor = { 1, 0.82, 0, 1 }
                end
            end

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

        if #displayEntries > 0 then
            widgets.groupEmpty:Hide()
            widgets.groupScrollBox:SetDataProvider(CreateDataProvider(displayEntries), ScrollBoxConstants.DiscardScrollPosition)
        else
            widgets.groupScrollBox:RemoveDataProvider()
            if widgets.groupScrollBox.ScrollToBegin then
                widgets.groupScrollBox:ScrollToBegin(ScrollBoxConstants.NoScrollInterpolation)
            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 core.MeetingStoneData and core.MeetingStoneData.QueryCharacters then
        core.MeetingStoneData:QueryCharacters(groupCharacters)
    end
    local activeGroupAliases = self:GetActiveGroupCharacterAliases()

    local hasGroupCharacters = #groupCharacters > 0

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

    self:SyncGroupCharacterRows(#groupCharacters)

    for index, characterEntry in ipairs(groupCharacters) do
        local isOffline = self:IsCharacterEntryOfflineInGroup(characterEntry)
        local isCurrentGroupMember = self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
        local isWarbandEntry = isCarpoolTab or not isCurrentGroupMember
        local presenceMarkerColor = nil
        if isOffline then
            presenceMarkerColor = { 0.45, 0.45, 0.45, 1 }
        elseif isCurrentGroupMember or isCarpoolTab then
            local classToken = characterEntry.data and characterEntry.data.class
            local classColors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
            local color = classToken and classColors and classColors[classToken]
            if color then
                presenceMarkerColor = { color.r or 1, color.g or 1, color.b or 1, 1 }
            else
                presenceMarkerColor = { 1, 0.82, 0, 1 }
            end
        end

        self:RefreshGroupCharacterRowFrame(widgets.groupRowFrames[index], characterEntry, {
            alternate = index % 2 == 0,
            isWarbandEntry = isWarbandEntry,
            isCarpoolEntry = isCarpoolTab,
            warbandOwnerName = isWarbandEntry and characterEntry.owner or nil,
            warbandSourceName = isCarpoolTab and GetWarbandSourceDisplayName(characterEntry) or nil,
            showGroupDivider = false,
            presenceMarkerColor = presenceMarkerColor,
            isOffline = isOffline,
            isRosterOnly = characterEntry.isRosterOnly or (characterEntry.data and characterEntry.data.isRosterOnly),
        })
    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()
            row:SetPoint("TOPLEFT", widgets.groupRowsFrame, "TOPLEFT", GROUP_LIST_ROWS_PADDING_X, currentYOffset)
            row:SetPoint("TOPRIGHT", widgets.groupRowsFrame, "TOPRIGHT", -GROUP_LIST_ROWS_PADDING_X, currentYOffset)
            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


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