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

core.MeetingStoneApplicantList = core.MeetingStoneApplicantList or {}
local ApplicantList = core.MeetingStoneApplicantList
local MeetingStoneConstants = core.MeetingStoneConstants or {}
local MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY = MeetingStoneConstants.MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY or "MeetingStone addon missing."

local MeetingStoneApplicantTestData = core.MeetingStoneApplicantTestData or {}

local function SeedMeetingStoneApplicantTestAccountType(applicant)
    if MeetingStoneApplicantTestData.SeedAccountType then
        MeetingStoneApplicantTestData.SeedAccountType(applicant)
    end
end

local function CreateMeetingStoneApplicantTestRows(owner, activityID, isMythicPlusActivity)
    if MeetingStoneApplicantTestData.CreateRows then
        return MeetingStoneApplicantTestData.CreateRows(owner, activityID, isMythicPlusActivity)
    end

    return nil
end

local GROUP_LIST_HEADER_HEIGHT = 36
local GROUP_LIST_ROW_HEIGHT = 36
local MK_ICON_PATH = core.MK_ICON_PATH or ("Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Icon\\")
local MEETINGSTONE_APPLICANT_SPEC_ICON_BY_ID = core.SPEC_ICON_FILE_BY_ID or {
    [62] = "Mage_Arcane_Spell_Holy_MagicalSentry.png",
    [63] = "Mage_Fire_Spell_Fire_FireBolt02.png",
    [64] = "Mage_Frost_Spell_Frost_FrostBolt02.png",
    [65] = "Paladin_Holy_Spell_Holy_HolyBolt.png",
    [66] = "Paladin_Protection_Ability_Paladin_ShieldoftheTemplar.png",
    [70] = "Paladin_Retribution_Spell_Holy_AuraOfLight.png",
    [71] = "Warrior_Arms_Ability_Warrior_SavageBlow.png",
    [72] = "Warrior_Fury_Ability_Warrior_InnerRage.png",
    [73] = "Warrior_Protection_Ability_Warrior_DefensiveStance.png",
    [102] = "Druid_Balance_Spell_Nature_StarFall.png",
    [103] = "Druid_Feral_Ability_Druid_CatForm.png",
    [104] = "Druid_Guardian_Ability_Racial_BearForm.png",
    [105] = "Druid_Restoration_SPELL_NATURE_HEALINGTOUCH.png",
    [250] = "DeathKnight_Blood_Spell_Deathknight_BloodPresence.png",
    [251] = "DeathKnight_Frost_Spell_Deathknight_FrostPresence.png",
    [252] = "DeathKnight_Unholy_Spell_Deathknight_UnholyPresence.png",
    [253] = "Hunter_BeastMastery_Ability_Hunter_BeastTaming.png",
    [254] = "Hunter_Marksmanship_Ability_Hunter_FocusedAim.png",
    [255] = "Hunter_Survival_Ability_Hunter_Camouflage.png",
    [256] = "Priest_Discipline_Spell_Holy_PowerWordShield.png",
    [257] = "Priest_Holy_Spell_Holy_GuardianSpirit.png",
    [258] = "Priest_Shadow_Spell_Shadow_ShadowWordPain.png",
    [259] = "Rogue_Assassination_Ability_Rogue_DeadlyBrew.png",
    [260] = "Rogue_Outlaw_INV_Sword_30.png",
    [261] = "Rogue_Subtlety_Ability_Stealth.png",
    [262] = "Shaman_Elemental_Spell_Nature_Lightning.png",
    [263] = "Shaman_Enhancement_Spell_Nature_LightningShield.png",
    [264] = "Shaman_Restoration_Spell_Nature_MagicImmunity.png",
    [265] = "Warlock_Affliction_Spell_Shadow_DeathCoil.png",
    [266] = "Warlock_Demonology_Spell_Shadow_Metamorphosis.png",
    [267] = "Warlock_Destruction_Spell_Shadow_RainOfFire.png",
    [268] = "Monk_Brewmaster_Spell_Monk_Brewmaster_Spec.png",
    [269] = "Monk_Windwalker_Spell_Monk_WindWalker_Spec.png",
    [270] = "Monk_Mistweaver_Spell_Monk_MistWeaver_Spec.png",
    [577] = "DemonHunter_Havoc_Ability_DemonHunter_SpecDPS.png",
    [581] = "DemonHunter_Vengeance_Ability_DemonHunter_SpecTank.png",
    [1467] = "Evoker_Devastation_ClassIcon_Evoker_Devastation.png",
    [1468] = "Evoker_Preservation_ClassIcon_Evoker_Preservation.png",
    [1473] = "Evoker_Augmentation_ClassIcon_Evoker_Augmentation.png",
    [1480] = "DemonHunter_Devourer_Classicon_DemonHunter_Void.png",
}
local MEETINGSTONE_APPLICANT_LEAVER_ICON_TEXTURE = MK_ICON_PATH .. "isLeaver.png"
local MEETINGSTONE_APPLICANT_LEAVER_TEXT = "逃兵"
local MEETINGSTONE_APPLICANT_LEAVER_TEXT_COLOR = { 1, 0.08, 0.05, 1 }
local BLACKLIST_MENU_ICON_TEXTURE = MK_ICON_PATH .. "Blacklist.png"
local BLACKLIST_MENU_ICON_MARKUP = "|T" .. BLACKLIST_MENU_ICON_TEXTURE .. ":16:16:0:0|t "
local BLACKLIST_MENU_COLOR_CODE = "|cffff2020"
local BLACKLIST_MENU_RESET_CODE = "|r"
local MEETINGSTONE_APPLICANT_BLACKLIST_TEXT = "屏蔽"
local MEETINGSTONE_APPLICANT_BLACKLIST_TEXT_COLOR = { 1, 0.08, 0.05, 1 }
local MEETINGSTONE_APPLICANT_BLACKLIST_TOOLTIP_COLOR = { 1, 0.08, 0.05, 1 }
local MEETINGSTONE_APPLICANT_COLUMN_WIDTHS = {
    star = 68.5,
    bnet = 26,
    name = 82.5,
    role = 56,
    class = 45,
    score = 128,
    itemLevel = 45,
    description = 63,
    action = 62,
}
local MEETINGSTONE_APPLICANT_LAYOUT_SIDE_INSET = 2.3
local MEETINGSTONE_APPLICANT_COLUMN_GAP = 1
local MEETINGSTONE_APPLICANT_COLUMN_ORDER = {
    "star",
    "bnet",
    "name",
    "role",
    "class",
    "score",
    "itemLevel",
    "description",
    "action",
}
local MEETINGSTONE_APPLICANT_HEADER_LABELS = {
    star = "类型",
    bnet = "@",
    name = "角色名",
    role = "职责",
    class = "专精",
    score = "大秘境分数",
    itemLevel = "装等",
    description = "描述信息",
    action = "操作",
}
local MEETINGSTONE_APPLICANT_SORTABLE_COLUMNS = {
    role = true,
    class = true,
    score = true,
    itemLevel = true,
}
local MEETINGSTONE_APPLICANT_ACTION_BUTTON = {
    size = 32,
    scale = 0.7,
    iconSize = 16,
    gap = 4,
    inviteAtlas = "UI-LFG-ReadyMark",
    declineAtlas = "UI-LFG-DeclineMark",
}
local MEETINGSTONE_APPLICANT_ACTION_VISUAL_COLORS = {
    normal = { 1, 1, 1, 1 },
    hover = { 1, 0.96, 0.62, 1 },
    pressed = { 0.95, 0.68, 0.18, 1 },
    disabled = { 0.55, 0.55, 0.55, 0.75 },
}
local MEETINGSTONE_SYSTEM_BUTTON_TEMPLATE = "UIPanelButtonTemplate"
local MEETINGSTONE_BUTTON_TEXT_SIZE = 12
local MEETINGSTONE_APPLICANT_HEADER_TEXT_SIZE = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_SIZE or 14
local MEETINGSTONE_APPLICANT_HEADER_TEXT_COMPACT_SIZE = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_COMPACT_SIZE or 12
local MEETINGSTONE_APPLICANT_EMPTY_TEXT_COLOR = { 1, 0.82, 0, 1 }
local MEETINGSTONE_APPLICANT_MISSING_ADDON_TEXT_COLOR = { 1, 0.82, 0, 1 }
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_TEXTURE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\TeamUp.png"
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT = 3
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_WIDTH = 27
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_HEIGHT = 25
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_GAP = 6
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_TEXT_GAP = 7
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_STEP_SECONDS = 0.28
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_FADE_IN_SECONDS = 0.16
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_HOLD_SECONDS = 0.4
local MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_FADE_OUT_SECONDS = 0.45
local MEETINGSTONE_APPLICANT_ROW_FADE_IN_SECONDS = 0.18
local MEETINGSTONE_APPLICANT_BACKGROUND_FADE_OUT_SECONDS = 0.45
local MEETINGSTONE_APPLICANT_RATING_CACHE_TTL_SECONDS = 30 * 60
local MEETINGSTONE_APPLICANT_HEADER_ACCENT_WIDTH = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_WIDTH or 1.5
local MEETINGSTONE_APPLICANT_HEADER_ACCENT_HEIGHT = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_HEIGHT or 22
local MEETINGSTONE_APPLICANT_HEADER_ACCENT_COLOR = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_COLOR or { 126 / 255, 112 / 255, 82 / 255 }
local MEETINGSTONE_APPLICANT_HEADER_ACCENT_ALPHA = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_ALPHA or 0.6
local GetMeetingStoneHeaderAccentPixelWidth = MeetingStoneConstants.GetMeetingStoneHeaderAccentPixelWidth or function()
    return MEETINGSTONE_APPLICANT_HEADER_ACCENT_WIDTH
end
local SnapMeetingStoneHeaderAccentOffset = MeetingStoneConstants.SnapMeetingStoneHeaderAccentOffset or function(_, value)
    return math.floor((tonumber(value) or 0) + 0.5)
end
local MEETINGSTONE_APPLICANT_ICON_SIZE = 16
local MEETINGSTONE_APPLICANT_LIST_ICON_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_CLASS_ICON_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_VETERAN_ICON_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_NEWBIE_ICON_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_LEADER_ICON_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_BNET_ICON_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_ROLE_ICON_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SLOT_SIZE = MEETINGSTONE_APPLICANT_ICON_SIZE
local MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SIZES = {
    veteran = MEETINGSTONE_APPLICANT_VETERAN_ICON_SIZE,
    newbie = MEETINGSTONE_APPLICANT_NEWBIE_ICON_SIZE,
    leader = MEETINGSTONE_APPLICANT_LEADER_ICON_SIZE,
    star = MEETINGSTONE_APPLICANT_ICON_SIZE,
    leaver = MEETINGSTONE_APPLICANT_ICON_SIZE,
    blacklist = MEETINGSTONE_APPLICANT_ICON_SIZE,
}

local function CanManageMeetingStoneApplicantActions()
    local listing = core.MeetingStoneListing
    if listing and listing.CanManageListing then
        return listing.CanManageListing()
    end
    return true
end

local function HasMeetingStoneApplicantActiveEntry()
    if not (C_LFGList and C_LFGList.HasActiveEntryInfo) then
        return false
    end

    local ok, hasEntry = pcall(C_LFGList.HasActiveEntryInfo)
    return ok and hasEntry == true
end

local function HasSharedMeetingStoneLeaderListing(owner)
    local shared = owner and owner.meetingStoneSharedLeaderDraft
    return type(shared) == "table" and shared.active ~= false and shared.activityID ~= nil
end

local function HasRecentSharedMeetingStoneListingRemoval(owner)
    if not owner or not owner.meetingStoneSharedLeaderListingRemovedAt then
        return false
    end

    local now = GetTime and GetTime() or time and time() or 0
    if now <= 0 then
        return true
    end
    return (now - owner.meetingStoneSharedLeaderListingRemovedAt) < 8
end

local function GetMeetingStoneApplicantEmptyState(owner, hasActiveEntry)
    if CanManageMeetingStoneApplicantActions() then
        local publishFailureMessage = not hasActiveEntry
            and owner
            and owner.GetMeetingStonePublishFailureMessage
            and owner:GetMeetingStonePublishFailureMessage()
            or nil
        if publishFailureMessage and publishFailureMessage ~= "" then
            return publishFailureMessage, true
        end

        return hasActiveEntry and TRANSLATE("队伍正在招募中") or TRANSLATE("请创建集合石招募"), true
    end

    if hasActiveEntry or HasSharedMeetingStoneLeaderListing(owner) then
        return TRANSLATE("队伍正在招募中"), true
    end

    return TRANSLATE("仅队长有权限预创建队伍"), true
end

local function UpdateMeetingStoneApplicantEmptyBlockLayout(widgets, showAnimation)
    local empty = widgets and widgets.meetingStoneApplicantEmpty
    local rowsFrame = widgets and widgets.meetingStoneApplicantRowsFrame
    if not (empty and rowsFrame) then
        return
    end

    local offsetY = 0
    if showAnimation then
        offsetY = (MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_HEIGHT + MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_TEXT_GAP) / 2
    end

    empty:ClearAllPoints()
    empty:SetPoint("CENTER", rowsFrame, "CENTER", 0, offsetY)
end

local function GetMeetingStoneApplicantTeamUpCycleSeconds()
    return (MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_STEP_SECONDS * MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT)
        + MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_HOLD_SECONDS
        + MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_FADE_OUT_SECONDS
end

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

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

    local fadeOutStart = (MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_STEP_SECONDS * MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT)
        + MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_HOLD_SECONDS
    if elapsed < fadeOutStart then
        return 1
    end

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

    return 0
end

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

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

local function EnsureMeetingStoneApplicantEmptyAnimation(widgets)
    if not widgets then
        return nil
    end
    if widgets.meetingStoneApplicantEmptyAnimation then
        return widgets.meetingStoneApplicantEmptyAnimation
    end

    local parent = widgets.meetingStoneApplicantRowsFrame
    local anchor = widgets.meetingStoneApplicantEmpty
    if not (parent and anchor) then
        return nil
    end

    local frame = CreateFrame("Frame", nil, parent)
    local width = (MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_WIDTH * MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT)
        + (MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_GAP * (MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT - 1))
    frame:SetSize(width, MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_HEIGHT)
    frame:SetPoint("TOP", anchor, "BOTTOM", 0, -MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_TEXT_GAP)
    frame.icons = {}

    for index = 1, MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT do
        local icon = frame:CreateTexture(nil, "ARTWORK")
        icon:SetTexture(MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_TEXTURE)
        icon:SetTexCoord(
            (index - 1) / MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT,
            index / MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_COUNT,
            0,
            1
        )
        icon:SetSize(MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_WIDTH, MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_HEIGHT)
        icon:SetPoint("LEFT", frame, "LEFT", (index - 1) * (MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_WIDTH + MEETINGSTONE_APPLICANT_EMPTY_TEAMUP_ICON_GAP), 0)
        icon:SetAlpha(0)
        icon:Hide()
        frame.icons[index] = icon
    end

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

    widgets.meetingStoneApplicantEmptyAnimation = frame
    return frame
end

local function SetMeetingStoneApplicantEmptyAnimationShown(widgets, shown)
    local animation = shown and EnsureMeetingStoneApplicantEmptyAnimation(widgets) or widgets and widgets.meetingStoneApplicantEmptyAnimation
    if not animation then
        return
    end

    if shown then
        animation.elapsed = 0
        RefreshMeetingStoneApplicantTeamUpAnimation(animation)
        animation:Show()
    else
        animation:Hide()
    end
end

local function GetMeetingStoneApplicantActiveEntryInfo()
    if not (C_LFGList and C_LFGList.GetActiveEntryInfo) then
        return nil
    end

    local ok, activeInfo = pcall(C_LFGList.GetActiveEntryInfo)
    return ok and type(activeInfo) == "table" and activeInfo or nil
end

local function GetMeetingStoneApplicants()
    if not (C_LFGList and C_LFGList.GetApplicants) then
        return {}
    end

    local ok, applicants = pcall(C_LFGList.GetApplicants)
    return ok and type(applicants) == "table" and applicants or {}
end

local function GetMeetingStoneApplicantInfo(applicantID)
    if not (C_LFGList and C_LFGList.GetApplicantInfo) then
        return nil
    end

    local ok, info = pcall(C_LFGList.GetApplicantInfo, applicantID)
    return ok and type(info) == "table" and info or nil
end

local function GetMeetingStoneApplicantMemberInfo(applicantID, memberIndex)
    if not (C_LFGList and C_LFGList.GetApplicantMemberInfo) then
        return nil
    end

    local ok, name, class, localizedClass, level, itemLevel, honorLevel, tank, healer, damage, assignedRole, relationship, dungeonScore, pvpItemLevel, honorLevelName, role, specID, isLeaver = pcall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIndex)
    if ok then
        return name, class, localizedClass, level, itemLevel, honorLevel, tank, healer, damage, assignedRole, relationship, dungeonScore, pvpItemLevel, honorLevelName, role, specID, isLeaver
    end
    return nil
end

local function SplitLFGCharacterName(name)
    if type(name) ~= "string" or name == "" then
        return nil, nil
    end

    local shortName, realm = name:match("^([^-]+)%-(.+)$")
    if shortName and realm then
        return shortName, realm
    end

    return name, nil
end

local function UpdateMeetingStoneApplicantLFGLeaver(owner, name, isLeaver)
    if not (owner and owner.UpdateCharacterLFGLeaver and type(name) == "string" and name ~= "") then
        return
    end

    local shortName, realm = SplitLFGCharacterName(name)
    owner:UpdateCharacterLFGLeaver(shortName or name, realm, isLeaver == true, "BlizzardLFG:Applicant")
end

local function GetMeetingStoneApplicantBlacklistInfo(owner, name)
    if not (owner and owner.IsPlayerBlacklisted and type(name) == "string" and name ~= "") then
        return false, nil, nil
    end

    return owner:IsPlayerBlacklisted(name)
end

local function DeclineMeetingStoneApplicant(applicantID)
    if not (applicantID and C_LFGList) then
        return false
    end

    if C_LFGList.DeclineApplicant then
        return pcall(C_LFGList.DeclineApplicant, applicantID)
    end
    if C_LFGList.RemoveApplicant then
        return pcall(C_LFGList.RemoveApplicant, applicantID)
    end
    return false
end

local function GetMeetingStoneApplicantDungeonScore(applicantID, memberIndex, activityID)
    if not (activityID and C_LFGList and C_LFGList.GetApplicantDungeonScoreForListing) then
        return nil
    end

    local ok, score = pcall(C_LFGList.GetApplicantDungeonScoreForListing, applicantID, memberIndex, activityID)
    return ok and score or nil
end

local function GetMeetingStoneApplicantMemberStats(applicantID, memberIndex)
    if not (C_LFGList and C_LFGList.GetApplicantMemberStats) then
        return nil
    end

    local ok, stats = pcall(C_LFGList.GetApplicantMemberStats, applicantID, memberIndex)
    return ok and stats or nil
end

local function ShouldCacheMeetingStoneApplicantRating(info)
    if type(info) ~= "table" or info.pendingApplicationStatus then
        return true
    end

    local status = info.applicationStatus
    return status == nil or status == ""
        or status == "applied"
        or status == "invited"
        or status == "inviteaccepted"
end

function ApplicantList.CacheCurrentApplicants(owner)
    owner = owner or MK
    if not (owner and owner.CacheMeetingStoneApplicantRating) then
        return 0
    end

    local applicants = GetMeetingStoneApplicants()
    if #applicants == 0 then
        return 0
    end

    local activeInfo = GetMeetingStoneApplicantActiveEntryInfo()
    local activityID = activeInfo and activeInfo.activityIDs and activeInfo.activityIDs[1]
    local isMythicPlusActivity = activeInfo and activeInfo.isMythicPlusActivity
    local cachedCount = 0

    for _, applicantID in ipairs(applicants) do
        local info = GetMeetingStoneApplicantInfo(applicantID)
        if ShouldCacheMeetingStoneApplicantRating(info) then
            local numMembers = info and info.numMembers or 0
            for memberIndex = 1, numMembers do
                local name, class, localizedClass, level, itemLevel, honorLevel, tank, healer, damage, assignedRole, relationship, dungeonScore, _, _, _, specID, isLeaver = GetMeetingStoneApplicantMemberInfo(applicantID, memberIndex)
                UpdateMeetingStoneApplicantLFGLeaver(owner, name, isLeaver)
                local bestScore = GetMeetingStoneApplicantDungeonScore(applicantID, memberIndex, activityID)
                local stats = GetMeetingStoneApplicantMemberStats(applicantID, memberIndex)
                if owner:CacheMeetingStoneApplicantRating({
                    applicantID = applicantID,
                    memberIndex = memberIndex,
                    name = name,
                    class = localizedClass or class,
                    classFile = class,
                    level = level,
                    honorLevel = honorLevel,
                    role = tank and "TANK" or healer and "HEALER" or damage and "DAMAGER" or assignedRole,
                    relationship = relationship,
                    isLeaver = isLeaver == true,
                    specID = specID,
                    score = dungeonScore or 0,
                    bestScore = bestScore,
                    stats = stats,
                    activityID = activityID,
                    isMythicPlusActivity = isMythicPlusActivity,
                    itemLevel = itemLevel or 0,
                }) then
                    cachedCount = cachedCount + 1
                end
            end
        end
    end

    return cachedCount
end

local MEETINGSTONE_APPLICANT_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 MEETINGSTONE_APPLICANT_ROLE_KEY_BY_LFG_ROLE = {
    TANK = "TANK",
    HEAL = "HEAL",
    HEALER = "HEAL",
    DPS = "DPS",
    DAMAGER = "DPS",
}
local MEETINGSTONE_APPLICANT_INACTIVE_STATUS = {
    failed = true,
    cancelled = true,
    declined = true,
    invited = true,
    inviteaccepted = true,
    invitedeclined = true,
    timedout = true,
}
local MEETINGSTONE_APPLICANT_RETAINED_INACTIVE_STATUS = {
    cancelled = true,
}
local MEETINGSTONE_APPLICANT_INACTIVE_ACTION_LABEL = {
    cancelled = "已取消",
}
local MEETINGSTONE_APPLICANT_INACTIVE_TEXT_COLOR = { 0.55, 0.55, 0.55, 1 }
local MEETINGSTONE_APPLICANT_INACTIVE_ICON_COLOR = { 0.55, 0.55, 0.55, 0.65 }
local MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR = { 1, 0.12, 0.08, 1 }
local MEETINGSTONE_APPLICANT_NEWBIE_BACKGROUND_COLOR = { 0.16, 0.85, 0.24, 1 }
local MEETINGSTONE_APPLICANT_LEAVER_BACKGROUND_COLOR = { 1, 0.08, 0.05, 1 }
local MEETINGSTONE_APPLICANT_BNET_BACKGROUND_COLOR = { 0.16, 0.48, 1, 1 }
local MEETINGSTONE_APPLICANT_DEFAULT_HOVER_COLOR = { 1, 0.74, 0.18, 1 }
local MEETINGSTONE_APPLICANT_HOVER_ALPHA = 0.16
local MEETINGSTONE_APPLICANT_ROW_ART_PATH = "Interface\\AddOns\\MyKeyStone\\Art\\Ui\\"
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_WIDTH = 564
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_HEIGHT = 52
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_CAP_WIDTH = 18
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_SOURCE_HEIGHT = 8
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_DISPLAY_HEIGHT = math.max(
    1,
    math.floor((core.GROUP_LIST_ROW_BACKGROUND_HEIGHT or 33) * MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_SOURCE_HEIGHT / MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_HEIGHT + 0.5)
)
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_BOTTOM_DISPLAY_HEIGHT = math.max(
    1,
    (core.GROUP_LIST_ROW_BACKGROUND_HEIGHT or 33) - MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_DISPLAY_HEIGHT
)
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_DISPLAY_CAP_WIDTH = math.max(
    1,
    math.floor((core.GROUP_LIST_ROW_BACKGROUND_HEIGHT or 33) * MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_CAP_WIDTH / MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_HEIGHT + 0.5)
)
local MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TEXTURE = {
    normal = MEETINGSTONE_APPLICANT_ROW_ART_PATH .. "ApplicantRowNormal.png",
    newbie = MEETINGSTONE_APPLICANT_ROW_ART_PATH .. "ApplicantRowGreen.png",
    leaver = MEETINGSTONE_APPLICANT_ROW_ART_PATH .. "ApplicantRowRed.png",
    bnet = MEETINGSTONE_APPLICANT_ROW_ART_PATH .. "ApplicantRowBlue.png",
    grey = MEETINGSTONE_APPLICANT_ROW_ART_PATH .. "ApplicantRowGrey.png",
}

local MEETINGSTONE_APPLICANT_PROVING_GROUND_DATA = PROVING_GROUND_DATA or {
    {
        { id = 23690, text = LFG_LIST_PROVING_TANK_GOLD },
        { id = 23687, text = LFG_LIST_PROVING_TANK_SILVER },
        { id = 23684, text = LFG_LIST_PROVING_TANK_BRONZE },
    },
    {
        { id = 23691, text = LFG_LIST_PROVING_HEALER_GOLD },
        { id = 23688, text = LFG_LIST_PROVING_HEALER_SILVER },
        { id = 23685, text = LFG_LIST_PROVING_HEALER_BRONZE },
    },
    {
        { id = 23689, text = LFG_LIST_PROVING_DAMAGER_GOLD },
        { id = 23686, text = LFG_LIST_PROVING_DAMAGER_SILVER },
        { id = 23683, text = LFG_LIST_PROVING_DAMAGER_BRONZE },
    },
}

local function TrySetMeetingStoneAtlas(texture, atlas, useAtlasSize)
    if not texture or not atlas then
        return false
    end

    if core.TrySetAtlas and core.TrySetAtlas(texture, atlas, useAtlasSize) then
        return true
    end

    if not texture.SetAtlas then
        return false
    end

    local ok = pcall(texture.SetAtlas, texture, atlas, useAtlasSize)
    return ok == true
end

local function ApplyMeetingStoneSystemButtonStyle(button)
    if not button then
        return
    end

    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, MEETINGSTONE_BUTTON_TEXT_SIZE, "")
    end
    if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Apply then
        core.MeetingStoneButtonArt.Apply(button)
    end
end

local function CreateMeetingStoneSystemButton(parent, name, width, height, text)
    local button = CreateFrame("Button", name, parent, MEETINGSTONE_SYSTEM_BUTTON_TEMPLATE)
    button:SetSize(width, height)
    if text then
        button:SetText(text)
    end
    ApplyMeetingStoneSystemButtonStyle(button)
    return button
end

local function SetMeetingStoneApplicantRowHover(row, shown)
    if row and row.mkMeetingStoneApplicantContextMenuLocked and shown ~= true then
        shown = true
    end
    if MK and MK.SetGroupRowHover then
        MK:SetGroupRowHover(row, shown)
        return
    end

    if not (row and row.widgets) then
        return
    end

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

local function FinishMeetingStoneApplicantContextMenu(row, token)
    if not row or row.mkMeetingStoneApplicantContextMenuToken ~= token then
        return
    end
    row.mkMeetingStoneApplicantContextMenuLocked = nil
    row.mkMeetingStoneApplicantContextMenuToken = nil
    SetMeetingStoneApplicantRowHover(row, row.IsMouseOver and row:IsMouseOver())
end

local function LockMeetingStoneApplicantContextMenu(row)
    if not row then
        return nil
    end
    local token = (tonumber(row.mkMeetingStoneApplicantContextMenuToken) or 0) + 1
    row.mkMeetingStoneApplicantContextMenuToken = token
    row.mkMeetingStoneApplicantContextMenuLocked = true
    SetMeetingStoneApplicantRowHover(row, true)
    return token
end

local function StopMeetingStoneApplicantRowFade(row, alpha)
    if row and row.mkMeetingStoneFadeGroup then
        row.mkMeetingStoneFadeGroup:Stop()
    end
    if row and row.SetAlpha then
        row:SetAlpha(alpha or 1)
    end
end

local function PlayMeetingStoneApplicantRowFadeIn(row, key, animate)
    if not row then
        return
    end
    key = tostring(key or "")
    if key == "" or animate == false or row.mkMeetingStoneFadeKey == key then
        StopMeetingStoneApplicantRowFade(row, 1)
        row.mkMeetingStoneFadeKey = key
        return
    end

    row.mkMeetingStoneFadeKey = key
    if not row.CreateAnimationGroup then
        row:SetAlpha(1)
        return
    end

    if not row.mkMeetingStoneFadeGroup then
        row.mkMeetingStoneFadeGroup = row:CreateAnimationGroup()
        row.mkMeetingStoneFadeAlpha = row.mkMeetingStoneFadeGroup:CreateAnimation("Alpha")
        row.mkMeetingStoneFadeAlpha:SetOrder(1)
    end
    row.mkMeetingStoneFadeGroup:Stop()
    row.mkMeetingStoneFadeAlpha:SetFromAlpha(0)
    row.mkMeetingStoneFadeAlpha:SetToAlpha(1)
    row.mkMeetingStoneFadeAlpha:SetDuration(MEETINGSTONE_APPLICANT_ROW_FADE_IN_SECONDS)
    row.mkMeetingStoneFadeGroup:SetScript("OnFinished", function()
        if row.SetAlpha then
            row:SetAlpha(1)
        end
    end)
    row:SetAlpha(0)
    row.mkMeetingStoneFadeGroup:Play()
end

local function CreateMeetingStoneApplicantHoverTextures(row)
    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()

    return hoverLeft, hover, hoverRight
end

local function CreateMeetingStoneApplicantBackgroundTextures(row, subLevel)
    local capWidth = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_DISPLAY_CAP_WIDTH
    local rowHeight = core.GROUP_LIST_ROW_BACKGROUND_HEIGHT or 33
    local leftTexCoord = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_CAP_WIDTH / MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_WIDTH
    local rightTexCoord = 1 - leftTexCoord
    subLevel = subLevel or 0

    local left = row:CreateTexture(nil, "BACKGROUND", nil, subLevel)
    left:SetPoint("LEFT", row, "LEFT", 0, 0)
    left:SetSize(capWidth, rowHeight)
    left:SetTexCoord(0, leftTexCoord, 0, 1)

    local right = row:CreateTexture(nil, "BACKGROUND", nil, subLevel)
    right:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    right:SetSize(capWidth, rowHeight)
    right:SetTexCoord(rightTexCoord, 1, 0, 1)

    local middle = row:CreateTexture(nil, "BACKGROUND", nil, subLevel)
    middle:SetPoint("LEFT", left, "RIGHT", 0, 0)
    middle:SetPoint("RIGHT", right, "LEFT", 0, 0)
    middle:SetHeight(rowHeight)
    middle:SetTexCoord(leftTexCoord, rightTexCoord, 0, 1)

    return {
        left = left,
        middle = middle,
        right = right,
    }
end

local function SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, piece, key, height, texLeft, texRight, texTop, texBottom, verticalMode)
    if not (row and piece) then
        return
    end

    local capWidth = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_DISPLAY_CAP_WIDTH
    piece:ClearAllPoints()
    if key == "left" then
        piece:SetPoint(verticalMode == "bottom" and "BOTTOMLEFT" or "TOPLEFT", row, verticalMode == "bottom" and "BOTTOMLEFT" or "TOPLEFT", 0, 0)
        piece:SetSize(capWidth, height)
    elseif key == "right" then
        piece:SetPoint(verticalMode == "bottom" and "BOTTOMRIGHT" or "TOPRIGHT", row, verticalMode == "bottom" and "BOTTOMRIGHT" or "TOPRIGHT", 0, 0)
        piece:SetSize(capWidth, height)
    else
        piece:SetPoint("LEFT", pieces.left, "RIGHT", 0, 0)
        piece:SetPoint("RIGHT", pieces.right, "LEFT", 0, 0)
        if verticalMode == "bottom" then
            piece:SetPoint("BOTTOM", row, "BOTTOM", 0, 0)
        else
            piece:SetPoint("TOP", row, "TOP", 0, 0)
        end
        piece:SetHeight(height)
    end
    piece:SetTexCoord(texLeft, texRight, texTop, texBottom)
    piece:Show()
end

local function SetMeetingStoneApplicantBackgroundPiecesAlpha(pieces, alpha)
    for _, piece in pairs(pieces or {}) do
        if piece and piece.SetAlpha then
            piece:SetAlpha(alpha)
        end
    end
end

local function SetMeetingStoneApplicantBackgroundPiecesShown(pieces, shown)
    for _, piece in pairs(pieces or {}) do
        if piece then
            piece:SetShown(shown == true)
        end
    end
end

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

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

local function PlayMeetingStoneApplicantBackgroundAlpha(row, pieces, key, fromAlpha, toAlpha, duration, onFinished, smoothing)
    local animated = false
    for _, piece in pairs(pieces or {}) do
        if piece then
            animated = true
            PlayMeetingStoneApplicantPieceAlpha(piece, key, fromAlpha, toAlpha, duration, onFinished, smoothing)
        end
    end
    if not animated and onFinished then
        onFinished()
    end
end

local function SetMeetingStoneApplicantBackgroundTexturePieces(row, pieces, texturePath, mode)
    if not (row and pieces) then
        return
    end

    for _, piece in pairs(pieces) do
        if piece then
            piece:SetTexture(texturePath)
        end
    end

    local leftTexCoord = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_CAP_WIDTH / MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_WIDTH
    local rightTexCoord = 1 - leftTexCoord
    local topTexBottom = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_SOURCE_HEIGHT / MEETINGSTONE_APPLICANT_ROW_BACKGROUND_SOURCE_HEIGHT

    if mode == "hidden" then
        SetMeetingStoneApplicantBackgroundPiecesShown(pieces, false)
        return
    end

    if mode == "top" then
        SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.left, "left", MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_DISPLAY_HEIGHT, 0, leftTexCoord, 0, topTexBottom, "top")
        SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.middle, "middle", MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_DISPLAY_HEIGHT, leftTexCoord, rightTexCoord, 0, topTexBottom, "top")
        SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.right, "right", MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TOP_DISPLAY_HEIGHT, rightTexCoord, 1, 0, topTexBottom, "top")
        return
    end

    if mode == "bottom" then
        SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.left, "left", MEETINGSTONE_APPLICANT_ROW_BACKGROUND_BOTTOM_DISPLAY_HEIGHT, 0, leftTexCoord, topTexBottom, 1, "bottom")
        SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.middle, "middle", MEETINGSTONE_APPLICANT_ROW_BACKGROUND_BOTTOM_DISPLAY_HEIGHT, leftTexCoord, rightTexCoord, topTexBottom, 1, "bottom")
        SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.right, "right", MEETINGSTONE_APPLICANT_ROW_BACKGROUND_BOTTOM_DISPLAY_HEIGHT, rightTexCoord, 1, topTexBottom, 1, "bottom")
        return
    end

    SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.left, "left", core.GROUP_LIST_ROW_BACKGROUND_HEIGHT or 33, 0, leftTexCoord, 0, 1, "top")
    SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.middle, "middle", core.GROUP_LIST_ROW_BACKGROUND_HEIGHT or 33, leftTexCoord, rightTexCoord, 0, 1, "top")
    SetMeetingStoneApplicantBackgroundPieceLayout(row, pieces, pieces.right, "right", core.GROUP_LIST_ROW_BACKGROUND_HEIGHT or 33, rightTexCoord, 1, 0, 1, "top")
end

local function SetMeetingStoneApplicantBackgroundTexture(row, texturePath, mode, animate)
    if not (row and row.widgets and row.widgets.backgroundPieces) then
        return
    end

    mode = mode or "full"
    local key = tostring(texturePath or "") .. ":" .. tostring(mode or "")
    if row.mkMeetingStoneBackgroundKey == key then
        if mode == "hidden" then
            SetMeetingStoneApplicantBackgroundPiecesShown(row.widgets.backgroundPieces, false)
        else
            SetMeetingStoneApplicantBackgroundPiecesShown(row.widgets.backgroundPieces, true)
            SetMeetingStoneApplicantBackgroundPiecesAlpha(row.widgets.backgroundPieces, 1)
        end
        return
    end

    local transitionPieces = row.widgets.backgroundTransitionPieces
    local canAnimate = animate == true
        and row.mkMeetingStoneBackgroundKey ~= nil
        and transitionPieces ~= nil
    if canAnimate then
        SetMeetingStoneApplicantBackgroundTexturePieces(
            row,
            transitionPieces,
            row.mkMeetingStoneBackgroundTexturePath or MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TEXTURE.normal,
            row.mkMeetingStoneBackgroundMode or "full"
        )
        SetMeetingStoneApplicantBackgroundPiecesAlpha(transitionPieces, 1)
        SetMeetingStoneApplicantBackgroundTexturePieces(row, row.widgets.backgroundPieces, texturePath, mode)
        SetMeetingStoneApplicantBackgroundPiecesAlpha(row.widgets.backgroundPieces, 1)
        PlayMeetingStoneApplicantBackgroundAlpha(row, transitionPieces, "old", 1, 0, MEETINGSTONE_APPLICANT_BACKGROUND_FADE_OUT_SECONDS, function()
            SetMeetingStoneApplicantBackgroundPiecesShown(transitionPieces, false)
        end, "IN")
    else
        SetMeetingStoneApplicantBackgroundTexturePieces(row, row.widgets.backgroundPieces, texturePath, mode)
        SetMeetingStoneApplicantBackgroundPiecesAlpha(row.widgets.backgroundPieces, 1)
        if transitionPieces then
            SetMeetingStoneApplicantBackgroundPiecesShown(transitionPieces, false)
        end
    end
    row.mkMeetingStoneBackgroundKey = key
    row.mkMeetingStoneBackgroundTexturePath = texturePath
    row.mkMeetingStoneBackgroundMode = mode
end

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

    color = color or MEETINGSTONE_APPLICANT_DEFAULT_HOVER_COLOR
    local r, g, b = color[1] or 1, color[2] or 0.74, color[3] or 0.18
    local alpha = MEETINGSTONE_APPLICANT_HOVER_ALPHA
    if row.widgets.hoverLeft then
        if row.widgets.hoverLeft.SetGradient and CreateColor then
            row.widgets.hoverLeft:SetGradient("HORIZONTAL", CreateColor(r, g, b, 0), CreateColor(r, g, b, alpha))
        else
            row.widgets.hoverLeft:SetVertexColor(r, g, b, alpha * 0.6)
        end
    end
    if row.widgets.hoverRight then
        if row.widgets.hoverRight.SetGradient and CreateColor then
            row.widgets.hoverRight:SetGradient("HORIZONTAL", CreateColor(r, g, b, alpha), CreateColor(r, g, b, 0))
        else
            row.widgets.hoverRight:SetVertexColor(r, g, b, alpha * 0.6)
        end
    end
    if row.widgets.hover then
        row.widgets.hover:SetVertexColor(r, g, b, alpha)
    end
end

local function NormalizeMeetingStoneApplicantNamePart(value)
    value = tostring(value or "")
    if strtrim then
        value = strtrim(value)
    else
        value = value:match("^%s*(.-)%s*$") or value
    end
    if value == "" then
        return nil
    end
    return string.lower(value:gsub("%s+", ""))
end

local function GetMeetingStoneCurrentRealmName()
    local realm = GetRealmName and GetRealmName()
    if realm and realm ~= "" then
        return realm
    end
    if GetNormalizedRealmName then
        local normalizedRealm = GetNormalizedRealmName()
        if normalizedRealm and normalizedRealm ~= "" then
            return normalizedRealm
        end
    end
    return nil
end

local function SplitMeetingStoneApplicantName(name)
    if type(name) ~= "string" or name == "" then
        return nil, nil
    end

    if name:find("-", 1, true) then
        local shortName, realm = strsplit("-", name)
        return shortName or name, realm
    end

    return name, GetMeetingStoneCurrentRealmName()
end

local function SplitMeetingStoneApplicantAccountTypeName(name)
    if type(name) ~= "string" or name == "" then
        return nil, nil
    end

    if name:find("-", 1, true) then
        local shortName, realm = strsplit("-", name)
        if type(realm) == "string" and realm ~= "" then
            return shortName or name, realm
        end
    end

    return name, nil
end

local function GetMeetingStoneApplicantNameKey(name, realm)
    local shortName = name
    local resolvedRealm = realm
    if not resolvedRealm then
        shortName, resolvedRealm = SplitMeetingStoneApplicantName(name)
    end

    shortName = NormalizeMeetingStoneApplicantNamePart(shortName)
    resolvedRealm = NormalizeMeetingStoneApplicantNamePart(resolvedRealm or GetMeetingStoneCurrentRealmName())
    if not shortName then
        return nil
    end
    return string.format("%s-%s", shortName, resolvedRealm or "")
end

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

local function GetMeetingStoneApplicantRatingCacheKeys(name, realm)
    local keys = {}
    local added = {}
    local function addKey(key)
        if key and key ~= "" and not added[key] then
            keys[#keys + 1] = key
            added[key] = true
        end
    end

    local shortName, resolvedRealm = SplitMeetingStoneApplicantName(name)
    if realm then
        resolvedRealm = realm
    end

    addKey(GetMeetingStoneApplicantNameKey(shortName or name, resolvedRealm))
    addKey(GetMeetingStoneApplicantNameKey(name, realm))
    addKey(NormalizeMeetingStoneApplicantNamePart(shortName or name))

    return keys
end

function MK:CacheMeetingStoneApplicantRating(applicant)
    if type(applicant) ~= "table" then
        return false
    end

    local score = tonumber(applicant.score or applicant.dungeonScore)
    if not score or score <= 0 then
        return false
    end

    local keys = GetMeetingStoneApplicantRatingCacheKeys(applicant.name, applicant.realm)
    if #keys == 0 then
        return false
    end

    local now = GetMeetingStoneApplicantRatingCacheNow()
    local data = {
        name = applicant.name,
        realm = applicant.realm,
        score = score,
        bestScore = applicant.bestScore,
        stats = applicant.stats,
        itemLevel = applicant.itemLevel,
        role = applicant.role,
        roles = applicant.roles,
        classFile = applicant.classFile,
        specID = applicant.specID,
        activityID = applicant.activityID,
        cachedAt = now,
        expiresAt = now + MEETINGSTONE_APPLICANT_RATING_CACHE_TTL_SECONDS,
        source = "meetingstone_applicant",
    }

    self.meetingStoneApplicantRatingCache = self.meetingStoneApplicantRatingCache or {}
    for _, key in ipairs(keys) do
        self.meetingStoneApplicantRatingCache[key] = data
    end

    return true
end

function MK:GetMeetingStoneApplicantRating(characterEntry)
    local cache = self.meetingStoneApplicantRatingCache
    if type(cache) ~= "table" then
        return nil
    end

    local character = characterEntry and characterEntry.data or {}
    local keys = {}
    local added = {}
    local function addKeys(name, realm)
        for _, key in ipairs(GetMeetingStoneApplicantRatingCacheKeys(name, realm)) do
            if not added[key] then
                keys[#keys + 1] = key
                added[key] = true
            end
        end
    end

    addKeys(character.name, character.realm)
    addKeys(characterEntry and characterEntry.key, character.realm)
    addKeys(characterEntry and characterEntry.owner, character.realm)

    local now = GetMeetingStoneApplicantRatingCacheNow()
    for _, key in ipairs(keys) do
        local data = cache[key]
        if type(data) == "table" then
            if type(data.expiresAt) == "number" and data.expiresAt < now then
                cache[key] = nil
            elseif tonumber(data.score) and tonumber(data.score) > 0 then
                return data
            end
        end
    end

    return nil
end

local function AddMeetingStoneRosterMemberKey(keys, name, realm)
    local key = GetMeetingStoneApplicantNameKey(name, realm)
    if key then
        keys[key] = true
    end
end

local function BuildMeetingStoneRosterMemberKeys()
    local keys = {}
    local function addUnit(unit)
        if UnitExists and not UnitExists(unit) then
            return
        end

        local name, realm
        if UnitFullName then
            name, realm = UnitFullName(unit)
            AddMeetingStoneRosterMemberKey(keys, name, realm)
        end
        if GetUnitName then
            AddMeetingStoneRosterMemberKey(keys, GetUnitName(unit, true))
        end
        if UnitName then
            AddMeetingStoneRosterMemberKey(keys, UnitName(unit))
        end
    end

    addUnit("player")
    if IsInRaid and IsInRaid(LE_PARTY_CATEGORY_HOME) then
        for index = 1, MAX_RAID_MEMBERS or 40 do
            addUnit("raid" .. index)
        end
    elseif IsInGroup and IsInGroup(LE_PARTY_CATEGORY_HOME) then
        for index = 1, MAX_PARTY_MEMBERS or 4 do
            addUnit("party" .. index)
        end
    end
    return keys
end

local function IsMeetingStoneApplicantAlreadyInRoster(name, rosterMemberKeys)
    if type(rosterMemberKeys) ~= "table" then
        return false
    end

    local key = GetMeetingStoneApplicantNameKey(name)
    return key and rosterMemberKeys[key] == true or false
end

local function GetMeetingStoneApplicantRoleKey(role)
    return MEETINGSTONE_APPLICANT_ROLE_KEY_BY_LFG_ROLE[role]
end

local function IsMeetingStoneApplicantInactive(info)
    if type(info) ~= "table" or info.pendingApplicationStatus then
        return false
    end

    return MEETINGSTONE_APPLICANT_INACTIVE_STATUS[info.applicationStatus] == true
end

local function ShouldRetainMeetingStoneInactiveApplicant(info)
    if not IsMeetingStoneApplicantInactive(info) then
        return false
    end

    return MEETINGSTONE_APPLICANT_RETAINED_INACTIVE_STATUS[info.applicationStatus] == true
end

local function GetMeetingStoneInactiveApplicantActionLabel(info)
    local status = info and info.applicationStatus
    local labelKey = MEETINGSTONE_APPLICANT_INACTIVE_ACTION_LABEL[status]
    return labelKey and TRANSLATE(labelKey) or ""
end

local function GetMeetingStoneApplicantStatusRank(info)
    if type(info) ~= "table" then
        return 0
    end

    if info.pendingApplicationStatus then
        return 50
    end

    local status = info.applicationStatus
    if status == "applied" then
        return 40
    end
    if status == "invited" then
        return 30
    end
    if status == "inviteaccepted" then
        return 20
    end
    if MEETINGSTONE_APPLICANT_INACTIVE_STATUS[status] then
        return 0
    end

    return 10
end

local function GetMeetingStoneApplicantDedupKey(name, memberIndex, applicantID)
    if type(name) == "string" and name ~= "" then
        local shortName, realm = SplitMeetingStoneApplicantName(name)
        return GetMeetingStoneApplicantNameKey(shortName or name, realm)
            or string.format("unknown-%s-%s", tostring(applicantID or ""), tostring(memberIndex or 1))
    end

    return string.format("unknown-%s-%s", tostring(applicantID or ""), tostring(memberIndex or 1))
end

local function ShouldReplaceMeetingStoneApplicantRow(candidate, existing)
    if not existing then
        return true
    end

    local candidateRank = tonumber(candidate and candidate.statusRank) or 0
    local existingRank = tonumber(existing and existing.statusRank) or 0
    if candidateRank ~= existingRank then
        return candidateRank > existingRank
    end

    local candidateID = tonumber(candidate and candidate.applicantID)
    local existingID = tonumber(existing and existing.applicantID)
    if candidateID and existingID and candidateID ~= existingID then
        return candidateID > existingID
    end

    return (tonumber(candidate and candidate.sourceIndex) or 0) > (tonumber(existing and existing.sourceIndex) or 0)
end

local function IsMeetingStoneApplicantNewbie(applicant)
    if applicant and applicant.mkMeetingStoneTestAccountType == "newbie" then
        return true
    end
    if not (applicant and AccountType.GetAccountState) then
        return false
    end

    SeedMeetingStoneApplicantTestAccountType(applicant)
    local name, realm = SplitMeetingStoneApplicantAccountTypeName(applicant.name)
    local accountState = AccountType.GetAccountState(name, realm, {
        requireRealm = true,
        returnUnavailable = false,
        includeVeteran = false,
    })
    return accountState and accountState.status == "newbie"
end

local function GetMeetingStoneApplicantGroupKey(applicant)
    if not applicant then
        return nil
    end

    if applicant.mkMeetingStoneTestGroupKey then
        return applicant.mkMeetingStoneTestGroupKey
    end

    if tonumber(applicant.numMembers) and tonumber(applicant.numMembers) > 1 and applicant.applicantID then
        return tostring(applicant.applicantID)
    end

    return nil
end

local function GetMeetingStoneApplicantGroupTextureKey(applicants)
    local hasBlacklisted = false
    local hasLeaver = false
    local hasNewbie = false
    local hasRelationship = false
    for _, applicant in ipairs(applicants or {}) do
        local name, realm = SplitMeetingStoneApplicantAccountTypeName(applicant and applicant.name)
        local status = AccountType.ResolveUnitStatus and AccountType.ResolveUnitStatus({
            name = name,
            realm = realm,
            relationship = applicant and applicant.relationship or nil,
            isBlacklisted = applicant and applicant.isBlacklisted == true,
            blacklistRecord = applicant and applicant.blacklistRecord or nil,
            isLeaver = applicant and applicant.isLeaver == true,
        }, {
            checkBlacklist = false,
            includeBlacklist = true,
            includeLeaver = true,
            includeAccount = true,
            includeRelationship = true,
            requireRealm = true,
            returnUnavailable = false,
            includeVeteran = false,
            accountStatuses = {
                newbie = true,
            },
        }) or nil
        local statusKey = status and status.status or nil
        if statusKey == "blacklist" then
            hasBlacklisted = true
        elseif statusKey == "leaver" then
            hasLeaver = true
        elseif statusKey == "newbie" or IsMeetingStoneApplicantNewbie(applicant) then
            hasNewbie = true
        elseif statusKey == "bnet" or statusKey == "friend" or statusKey == "guild" or (applicant and applicant.relationship) then
            hasRelationship = true
        end
    end

    if hasBlacklisted then
        return "blacklist"
    end
    if hasLeaver then
        return "leaver"
    end
    if hasNewbie then
        return "newbie"
    end
    return hasRelationship and "bnet" or "normal"
end

local function ArrangeMeetingStoneApplicantRowsByGroup(applicantRows)
    if type(applicantRows) ~= "table" or #applicantRows <= 1 then
        return
    end

    local groupsByKey = {}
    local orderedGroups = {}
    for _, applicant in ipairs(applicantRows) do
        local groupKey = GetMeetingStoneApplicantGroupKey(applicant)
        local group
        if groupKey then
            group = groupsByKey[groupKey]
            if not group then
                group = {
                    key = groupKey,
                    rows = {},
                    isApplicantGroup = true,
                }
                groupsByKey[groupKey] = group
                orderedGroups[#orderedGroups + 1] = group
            end
        else
            group = {
                rows = {},
            }
            orderedGroups[#orderedGroups + 1] = group
        end
        group.rows[#group.rows + 1] = applicant
    end

    local outputIndex = 1
    for _, group in ipairs(orderedGroups) do
        local rows = group.rows

        local groupSize = group.isApplicantGroup and #rows or 1
        local actionIndex = math.ceil(groupSize / 2)
        local groupTextureKey = group.isApplicantGroup and GetMeetingStoneApplicantGroupTextureKey(rows) or nil
        for index, applicant in ipairs(rows) do
            if group.isApplicantGroup and groupSize > 1 then
                applicant.mkMeetingStoneGroupKey = group.key
                applicant.mkMeetingStoneGroupIndex = index
                applicant.mkMeetingStoneGroupSize = groupSize
                applicant.mkMeetingStoneGroupActionIndex = actionIndex
                applicant.mkMeetingStoneGroupTextureKey = groupTextureKey
            else
                applicant.mkMeetingStoneGroupKey = nil
                applicant.mkMeetingStoneGroupIndex = nil
                applicant.mkMeetingStoneGroupSize = nil
                applicant.mkMeetingStoneGroupActionIndex = nil
                applicant.mkMeetingStoneGroupTextureKey = nil
            end
            applicantRows[outputIndex] = applicant
            outputIndex = outputIndex + 1
        end
    end

    for index = outputIndex, #applicantRows do
        applicantRows[index] = nil
    end
end

local function SetMeetingStoneApplicantRowBackground(row, applicant, inactive)
    if not (row and row.widgets) then
        return
    end

    local color
    local texturePath = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TEXTURE.normal
    local textureKey = applicant and applicant.mkMeetingStoneGroupTextureKey or nil
    if inactive == true then
        texturePath = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TEXTURE.grey
    elseif textureKey == "bnet" or (not textureKey and applicant and applicant.relationship) then
        color = MEETINGSTONE_APPLICANT_BNET_BACKGROUND_COLOR
        texturePath = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TEXTURE.bnet
    elseif textureKey == "blacklist"
        or textureKey == "leaver"
        or (not textureKey and applicant and (applicant.isBlacklisted == true or applicant.isLeaver == true))
    then
        color = MEETINGSTONE_APPLICANT_LEAVER_BACKGROUND_COLOR
        texturePath = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TEXTURE.leaver
    elseif textureKey == "newbie" or (not textureKey and IsMeetingStoneApplicantNewbie(applicant)) then
        color = MEETINGSTONE_APPLICANT_NEWBIE_BACKGROUND_COLOR
        texturePath = MEETINGSTONE_APPLICANT_ROW_BACKGROUND_TEXTURE.newbie
    end

    local backgroundMode = "full"
    if inactive ~= true and applicant and tonumber(applicant.mkMeetingStoneGroupSize) and applicant.mkMeetingStoneGroupSize > 1 then
        if applicant.mkMeetingStoneGroupIndex == 1 then
            backgroundMode = "top"
        elseif applicant.mkMeetingStoneGroupIndex == applicant.mkMeetingStoneGroupSize then
            backgroundMode = "bottom"
        else
            backgroundMode = "hidden"
        end
    end

    SetMeetingStoneApplicantBackgroundTexture(row, texturePath, backgroundMode, true)
    SetMeetingStoneApplicantHoverColor(row, color)
end

local function IsMeetingStoneApplicantManualRefresh(refreshSource)
    return refreshSource == "MeetingStoneApplicantRefresh"
end

local function ShouldResetMeetingStoneApplicantScroll(widgets, refreshSource, rowCount)
    if refreshSource == "MeetingStoneApplicantHeaderSort" or IsMeetingStoneApplicantManualRefresh(refreshSource) then
        return true
    end

    local previousRowCount = tonumber(widgets and widgets.mkMeetingStoneApplicantLastRowCount)
    rowCount = tonumber(rowCount) or 0
    return previousRowCount ~= nil and rowCount < previousRowCount
end

local function GetMeetingStoneSuppressedInactiveApplicantKeys(owner)
    if not owner then
        return nil
    end

    owner.mkMeetingStoneSuppressedInactiveApplicantKeys = owner.mkMeetingStoneSuppressedInactiveApplicantKeys or {}
    return owner.mkMeetingStoneSuppressedInactiveApplicantKeys
end



local function GetMeetingStoneApplicantRoleSortValue(applicant)
    local roles = applicant and applicant.roles
    if roles and roles.TANK then
        return "TANK"
    end
    if roles and roles.HEAL then
        return "HEAL"
    end
    if roles and roles.DPS then
        return "DPS"
    end

    return ""
end

local function CreateMeetingStoneApplicantRoleFrame(parent, roleKey)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetSize(MEETINGSTONE_APPLICANT_ROLE_ICON_SIZE, MEETINGSTONE_APPLICANT_ROLE_ICON_SIZE)
    frame.mkRoleIconSize = MEETINGSTONE_APPLICANT_ROLE_ICON_SIZE

    local icon = frame:CreateTexture(nil, "OVERLAY")
    icon:SetAllPoints(frame)
    local atlasName = MEETINGSTONE_APPLICANT_ROLE_ATLAS_BY_KEY[roleKey]
    if atlasName and icon.SetAtlas then
        icon:SetAtlas(atlasName, false)
    elseif atlasName then
        icon:SetTexture(atlasName)
    end
    icon:SetVertexColor(1, 1, 1, 0.95)
    icon:Hide()

    frame.icon = icon
    frame:Hide()
    return frame
end

local function SetMeetingStoneApplicantRoles(row, roles)
    if not (row and row.widgets and row.widgets.roles) then
        return
    end

    for _, roleKey in ipairs(MK:GetRoleOrder()) do
        local roleFrame = row.widgets.roles[roleKey]
        if roleFrame then
            if roles and roles[roleKey] then
                roleFrame:Show()
                if roleFrame.icon then
                    roleFrame.icon:Show()
                end
            else
                roleFrame:Hide()
            end
        end
    end

    if MK and MK.CenterGroupRowRoles then
        MK:CenterGroupRowRoles(row)
    end
end

local function SetMeetingStoneApplicantRowInactiveState(row, inactive)
    if not (row and row.widgets) then
        return
    end

    local widgets = row.widgets
    local textColor = MEETINGSTONE_APPLICANT_INACTIVE_TEXT_COLOR
    local iconColor = MEETINGSTONE_APPLICANT_INACTIVE_ICON_COLOR
    if widgets.accountTypeContainer and widgets.accountTypeContainer.SetAlpha then
        widgets.accountTypeContainer:SetAlpha(inactive and 0.55 or 1)
    end

    for _, fontString in ipairs({ widgets.name, widgets.score, widgets.itemLevel, widgets.message }) do
        if fontString and inactive and fontString.SetTextColor then
            fontString:SetTextColor(textColor[1], textColor[2], textColor[3], textColor[4])
        elseif fontString and fontString ~= widgets.name and fontString.SetTextColor then
            fontString:SetTextColor(1, 1, 1, 1)
        end
    end

    for _, texture in ipairs({ widgets.bnetIcon, widgets.classIcon }) do
        if texture and texture.SetVertexColor then
            if inactive then
                texture:SetVertexColor(iconColor[1], iconColor[2], iconColor[3], iconColor[4])
            else
                texture:SetVertexColor(1, 1, 1, 1)
            end
        end
    end

    if widgets.roles then
        for _, roleFrame in pairs(widgets.roles) do
            if roleFrame and roleFrame.icon and roleFrame.icon.SetVertexColor then
                if inactive then
                    roleFrame.icon:SetVertexColor(iconColor[1], iconColor[2], iconColor[3], iconColor[4])
                else
                    roleFrame.icon:SetVertexColor(1, 1, 1, 0.95)
                end
            end
        end
    end
end

local function SetMeetingStoneApplicantStatusMarker(row, texturePath, text, textColor)
    if not (row and row.widgets and row.widgets.accountTypeContainer and row.widgets.accountTypeIcon and row.widgets.accountTypeLabel) then
        return false
    end

    textColor = textColor or MEETINGSTONE_APPLICANT_LEAVER_TEXT_COLOR
    local container = row.widgets.accountTypeContainer
    local icon = row.widgets.accountTypeIcon
    local label = row.widgets.accountTypeLabel
    local iconSlot = row.widgets.accountTypeIconSlot
    local iconSize = MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SLOT_SIZE
    local containerWidth = (container.GetWidth and container:GetWidth()) or 0
    if containerWidth <= 0 then
        containerWidth = MEETINGSTONE_APPLICANT_COLUMN_WIDTHS.star or 68
    end

    if iconSlot then
        iconSlot:ClearAllPoints()
        iconSlot:SetSize(MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SLOT_SIZE, MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SLOT_SIZE)
        iconSlot:SetPoint("LEFT", container, "LEFT", 0, 0)
        iconSlot:Show()
    end

    icon:ClearAllPoints()
    icon:SetTexture(texturePath)
    icon:SetTexCoord(0, 1, 0, 1)
    icon:SetSize(iconSize, iconSize)
    icon:SetVertexColor(1, 1, 1, 1)

    label:ClearAllPoints()
    label:SetText(TRANSLATE(text))
    label:SetTextColor(
        textColor[1],
        textColor[2],
        textColor[3],
        textColor[4]
    )
    label:SetJustifyH("CENTER")
    label:SetHeight(18)
    label:SetWidth(containerWidth)

    if iconSlot then
        icon:SetPoint("CENTER", iconSlot, "CENTER", 0, 0)
    else
        icon:SetPoint("LEFT", container, "LEFT", 0, 0)
    end
    label:SetPoint("CENTER", container, "CENTER", 0, 0)
    icon:Show()
    return true
end

local function SetMeetingStoneApplicantLeaverMarker(row)
    return SetMeetingStoneApplicantStatusMarker(
        row,
        MEETINGSTONE_APPLICANT_LEAVER_ICON_TEXTURE,
        MEETINGSTONE_APPLICANT_LEAVER_TEXT,
        MEETINGSTONE_APPLICANT_LEAVER_TEXT_COLOR
    )
end

local function SetMeetingStoneApplicantBlacklistMarker(row)
    return SetMeetingStoneApplicantStatusMarker(
        row,
        BLACKLIST_MENU_ICON_TEXTURE,
        MEETINGSTONE_APPLICANT_BLACKLIST_TEXT,
        MEETINGSTONE_APPLICANT_BLACKLIST_TEXT_COLOR
    )
end

local function SetMeetingStoneApplicantAccountType(row, applicant)
    if not (row and row.widgets and row.widgets.accountTypeContainer) then
        return
    end

    if not applicant then
        if MK and MK.SetGroupAccountTypeIcon then
            MK:SetGroupAccountTypeIcon(row, nil)
        end
        row.widgets.accountTypeIcon:Hide()
        if row.widgets.accountTypeIconSlot then
            row.widgets.accountTypeIconSlot:Hide()
        end
        row.widgets.accountTypeLabel:SetText("")
        return
    end

    if applicant.isBlacklisted == true then
        if MK and MK.SetGroupAccountTypeIcon then
            MK:SetGroupAccountTypeIcon(row, nil)
        end
        SetMeetingStoneApplicantBlacklistMarker(row)
        return
    end

    if applicant.isLeaver == true then
        if MK and MK.SetGroupAccountTypeIcon then
            MK:SetGroupAccountTypeIcon(row, nil)
        end
        SetMeetingStoneApplicantLeaverMarker(row)
        return
    end

    local name, realm = SplitMeetingStoneApplicantAccountTypeName(applicant.name)
    SeedMeetingStoneApplicantTestAccountType(applicant)

    if MK and MK.SetGroupAccountTypeIcon then
        MK:SetGroupAccountTypeIcon(row, {
            key = applicant.name,
            data = {
                name = applicant.name,
                realm = realm,
                mkAccountTypeUnavailable = type(realm) ~= "string" or realm == "",
                mkAccountTypeUnavailableText = "无法查询",
                mkAccountTypeIconSizes = MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SIZES,
                mkAccountTypeAlign = "LEFT",
                mkAccountTypeInsetX = 0,
                mkAccountTypeIconSlotSize = MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SLOT_SIZE,
                mkAccountTypeCenterLabel = true,
                mkCompactMeetingStoneAccountTypeText = true,
            },
        })
    end
end

local function QueryMeetingStoneApplicantAccountTypes(applicants)
    if not (AccountType and AccountType.QueueQueries) then
        return
    end

    local queryEntries = {}
    for _, applicant in ipairs(applicants or {}) do
        if applicant and applicant.isLeaver ~= true and applicant.isBlacklisted ~= true and applicant.mkMeetingStoneTestRow ~= true then
            local name, realm = SplitMeetingStoneApplicantAccountTypeName(applicant.name)
            if name and realm then
                queryEntries[#queryEntries + 1] = {
                    name = name,
                    realm = realm,
                }
            end
        end
    end

    AccountType.QueueQueries(queryEntries, {
        requireRealm = true,
    })
end

local function GetMeetingStoneApplicantColumnLayout(availableWidth)
    local layout = { widths = MEETINGSTONE_APPLICANT_COLUMN_WIDTHS }
    local baseTotal = MEETINGSTONE_APPLICANT_LAYOUT_SIDE_INSET * 2
    for _, key in ipairs(MEETINGSTONE_APPLICANT_COLUMN_ORDER) do
        baseTotal = baseTotal + (MEETINGSTONE_APPLICANT_COLUMN_WIDTHS[key] or 0)
    end

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

    local sideInset = MEETINGSTONE_APPLICANT_LAYOUT_SIDE_INSET * scale
    local offset = sideInset
    local totalGap = math.max(0, (#MEETINGSTONE_APPLICANT_COLUMN_ORDER - 1) * MEETINGSTONE_APPLICANT_COLUMN_GAP)
    for index, key in ipairs(MEETINGSTONE_APPLICANT_COLUMN_ORDER) do
        local width = (MEETINGSTONE_APPLICANT_COLUMN_WIDTHS[key] or 0) * scale
        if key == "description" then
            width = math.max(1, width + extraDescriptionWidth - totalGap)
        end
        layout[key] = {
            x = offset,
            width = width,
        }
        offset = offset + width
        if index < #MEETINGSTONE_APPLICANT_COLUMN_ORDER then
            offset = offset + MEETINGSTONE_APPLICANT_COLUMN_GAP
        end
    end
    layout.totalWidth = offset + sideInset
    layout.sideInset = sideInset
    layout.scale = scale
    return layout
end

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

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

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

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

    local height = MEETINGSTONE_APPLICANT_HEADER_ACCENT_HEIGHT
    local halfHeight = height / 2
    accent:SetSize(GetMeetingStoneHeaderAccentPixelWidth(accent), height)

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

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

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

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

    accent.top = top
    accent.bottom = bottom
    LayoutMeetingStoneHeaderAccent(accent)

    return accent
end

local function EnsureMeetingStoneHeaderGapAccents(frame)
    frame.gapAccents = frame.gapAccents or {}
    local requiredCount = math.max(0, #MEETINGSTONE_APPLICANT_COLUMN_ORDER - 1)
    for index = #frame.gapAccents + 1, requiredCount do
        frame.gapAccents[index] = CreateMeetingStoneHeaderAccent(frame)
    end
    return frame.gapAccents
end

function ApplicantList.ApplyHeaderLayout(frame, availableWidth)
    if not frame then
        return
    end

    local layout = GetMeetingStoneApplicantColumnLayout(availableWidth or (frame.GetWidth and frame:GetWidth()) or nil)
    local headerHeight = frame.GetHeight and math.max(frame:GetHeight() or GROUP_LIST_HEADER_HEIGHT, 1) or GROUP_LIST_HEADER_HEIGHT
    local gapAccents = EnsureMeetingStoneHeaderGapAccents(frame)
    for _, key in ipairs(MEETINGSTONE_APPLICANT_COLUMN_ORDER) do
        local label = frame[key]
        if label and layout[key] then
            label:ClearAllPoints()
            label:SetPoint("LEFT", frame, "LEFT", layout[key].x, 0)
            label:SetSize(layout[key].width, headerHeight)
            if label.label and core.ApplyFont then
                core.ApplyFont(label.label, layout.scale < 0.86 and MEETINGSTONE_APPLICANT_HEADER_TEXT_COMPACT_SIZE or MEETINGSTONE_APPLICANT_HEADER_TEXT_SIZE, "")
            end
            if core.UpdateScaledGroupHeaderButtonStyle then
                core.UpdateScaledGroupHeaderButtonStyle(label, layout[key].width, headerHeight)
            end
        end
    end
    for index = 1, #gapAccents do
        local leftKey = MEETINGSTONE_APPLICANT_COLUMN_ORDER[index]
        local leftColumn = layout[leftKey]
        local accent = gapAccents[index]
        if leftColumn and accent then
            accent:ClearAllPoints()
            LayoutMeetingStoneHeaderAccent(accent)
            accent:SetPoint("CENTER", frame, "LEFT", SnapMeetingStoneHeaderAccentOffset(frame, leftColumn.x + leftColumn.width), 0)
            accent:Show()
        elseif accent then
            accent:Hide()
        end
    end
    frame.mkApplicantColumnLayout = layout
    frame:SetWidth(layout.totalWidth)
end

function ApplicantList.CreateHeader(parent, owner)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetHeight(GROUP_LIST_HEADER_HEIGHT)

    for _, key in ipairs(MEETINGSTONE_APPLICANT_COLUMN_ORDER) do
        local label = CreateFrame("Button", nil, frame)
        if core.ApplyScaledGroupHeaderButtonStyle then
            core.ApplyScaledGroupHeaderButtonStyle(label)
        elseif core.ApplyGroupHeaderButtonStyle then
            core.ApplyGroupHeaderButtonStyle(label)
        end
        label.sortKey = key
        label.mkMeetingStoneApplicantSortable = MEETINGSTONE_APPLICANT_SORTABLE_COLUMNS[key] == true
        label:EnableMouse(label.mkMeetingStoneApplicantSortable)
        if MEETINGSTONE_APPLICANT_SORTABLE_COLUMNS[key] then
            label:SetScript("OnClick", function(button)
                if not owner then
                    return
                end

                owner.meetingStoneApplicantSort = owner.meetingStoneApplicantSort or {}
                if owner.meetingStoneApplicantSort.key == button.sortKey then
                    owner.meetingStoneApplicantSort.direction = owner.meetingStoneApplicantSort.direction == "desc" and "asc" or "desc"
                else
                    owner.meetingStoneApplicantSort.key = button.sortKey
                    owner.meetingStoneApplicantSort.direction = "desc"
                end

                if owner.RefreshMeetingStoneApplicantsOnly then
                    owner:RefreshMeetingStoneApplicantsOnly("MeetingStoneApplicantHeaderSort", button.sortKey)
                elseif owner.RefreshMainWindow then
                    owner:RefreshMainWindow("MeetingStoneApplicantHeaderSort", button.sortKey)
                end
            end)
        end

        local text = label:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        text:SetPoint("LEFT", label, "LEFT", 3, 0)
        text:SetPoint("RIGHT", label, "RIGHT", -3, 0)
        text:SetJustifyH("CENTER")
        text:SetJustifyV("MIDDLE")
        text:SetWordWrap(false)
        text:SetText(TRANSLATE(MEETINGSTONE_APPLICANT_HEADER_LABELS[key]))
        if core.ApplyFont then
            core.ApplyFont(text, MEETINGSTONE_APPLICANT_HEADER_TEXT_SIZE, "")
        end

        label.label = text
        if core.SetScaledGroupHeaderButtonText then
            core.SetScaledGroupHeaderButtonText(label, text)
        end
        frame[key] = label
    end

    ApplicantList.ApplyHeaderLayout(frame)
    return frame
end

local function ApplyMeetingStoneApplicantRowLayout(row, availableWidth)
    if not row or not row.widgets then
        return
    end

    local layout = GetMeetingStoneApplicantColumnLayout(availableWidth or (row.GetWidth and row:GetWidth()) or nil)
    local widgets = row.widgets
    local function setText(key, widget)
        if widget and layout[key] then
            widget:ClearAllPoints()
            widget:SetPoint("LEFT", row, "LEFT", layout[key].x, 0)
            widget:SetSize(layout[key].width, 18)
            if widget.SetJustifyH then
                widget:SetJustifyH(widget.mkMeetingStoneApplicantJustifyH or "CENTER")
            end
            if core.ApplyFont then
                core.ApplyFont(widget, layout.scale < 0.86 and 10 or 13, "")
            end
        end
    end
    local function setIcon(key, widget)
        if widget and layout[key] then
            local iconSize = widget.mkMeetingStoneApplicantIconSize or MEETINGSTONE_APPLICANT_LIST_ICON_SIZE
            widget:ClearAllPoints()
            widget:SetPoint("CENTER", row, "LEFT", layout[key].x + (layout[key].width / 2), 0)
            widget:SetSize(iconSize, iconSize)
        end
    end
    local function setContainer(key, widget, height)
        if widget and layout[key] then
            widget:ClearAllPoints()
            widget:SetPoint("LEFT", row, "LEFT", layout[key].x, 0)
            widget:SetSize(layout[key].width, height or 18)
        end
    end

    setContainer("star", widgets.accountTypeContainer, 18)
    setIcon("bnet", widgets.bnetIcon)
    setText("name", widgets.name)
    setContainer("role", widgets.rolesContainer, MEETINGSTONE_APPLICANT_ROLE_ICON_SIZE)
    setIcon("class", widgets.classIcon)
    setText("score", widgets.score)
    setText("itemLevel", widgets.itemLevel)
    setText("description", widgets.message)

    if widgets.actionContainer and widgets.inviteButtonHost and widgets.declineButtonHost and widgets.inviteButton and widgets.declineButton and layout.action then
        local gap = layout.scale < 0.86 and 2 or MEETINGSTONE_APPLICANT_ACTION_BUTTON.gap
        local buttonSize = MEETINGSTONE_APPLICANT_ACTION_BUTTON.size
        local buttonScale = MEETINGSTONE_APPLICANT_ACTION_BUTTON.scale
        local visualSize = buttonSize * buttonScale
        local totalWidth = (visualSize * 2) + gap
        local firstCenterX = ((layout.action.width - totalWidth) / 2) + (visualSize / 2)
        local secondCenterX = firstCenterX + visualSize + gap

        widgets.actionContainer:ClearAllPoints()
        widgets.actionContainer:SetPoint("LEFT", row, "LEFT", layout.action.x, 0)
        widgets.actionContainer:SetSize(layout.action.width, GROUP_LIST_ROW_HEIGHT)

        widgets.inviteButtonHost:ClearAllPoints()
        widgets.inviteButtonHost:SetPoint("CENTER", widgets.actionContainer, "LEFT", firstCenterX, 0)
        widgets.inviteButtonHost:SetSize(visualSize, visualSize)
        widgets.declineButtonHost:ClearAllPoints()
        widgets.declineButtonHost:SetPoint("CENTER", widgets.actionContainer, "LEFT", secondCenterX, 0)
        widgets.declineButtonHost:SetSize(visualSize, visualSize)

        widgets.inviteButton:ClearAllPoints()
        widgets.inviteButton:SetPoint("CENTER", widgets.inviteButtonHost, "CENTER", 0, 0)
        widgets.inviteButton:SetSize(buttonSize, buttonSize)
        widgets.inviteButton:SetScale(buttonScale)
        widgets.declineButton:ClearAllPoints()
        widgets.declineButton:SetPoint("CENTER", widgets.declineButtonHost, "CENTER", 0, 0)
        widgets.declineButton:SetSize(buttonSize, buttonSize)
        widgets.declineButton:SetScale(buttonScale)

        if widgets.actionStatus then
            widgets.actionStatus:ClearAllPoints()
            widgets.actionStatus:SetPoint("LEFT", widgets.actionContainer, "LEFT", 0, 0)
            widgets.actionStatus:SetPoint("RIGHT", widgets.actionContainer, "RIGHT", 0, 0)
            widgets.actionStatus:SetHeight(18)
        end
    end

    row.mkApplicantColumnLayout = layout
end

local function GetMeetingStoneApplicantDescription(comment)
    if type(comment) ~= "string" or comment == "" then
        return ""
    end

    if DecodeDescriptionData then
        local ok, decoded = pcall(DecodeDescriptionData, comment)
        if ok and type(decoded) == "string" and decoded ~= "" then
            return decoded
        end
    end

    return comment
end

local function GetMeetingStoneApplicantSortValue(applicant, sortKey)
    if sortKey == "score" then
        return tonumber(applicant and applicant.score) or 0
    end

    if sortKey == "itemLevel" then
        return tonumber(applicant and applicant.itemLevel) or 0
    end

    if sortKey == "role" then
        return GetMeetingStoneApplicantRoleSortValue(applicant)
    end

    if sortKey == "class" then
        return tonumber(applicant and applicant.specID) or tostring(applicant and (applicant.classFile or applicant.class) or "")
    end

    return tostring(applicant and applicant.shortName or "")
end

local function GetMeetingStoneApplicantOriginalOrder(applicant)
    return tonumber(applicant and applicant.mkMeetingStoneSortIndex)
        or tonumber(applicant and applicant.displayOrderID)
        or tonumber(applicant and applicant.sourceIndex)
        or tonumber(applicant and applicant.memberIndex)
        or 0
end

local function SortMeetingStoneApplicantRows(owner, applicantRows)
    local sortState = owner and owner.meetingStoneApplicantSort
    local sortKey = sortState and sortState.key
    if not MEETINGSTONE_APPLICANT_SORTABLE_COLUMNS[sortKey] then
        return
    end

    local descending = sortState.direction ~= "asc"
    for index, applicant in ipairs(applicantRows) do
        applicant.mkMeetingStoneSortIndex = index
    end

    table.sort(applicantRows, function(left, right)
        if left == right then
            return false
        end

        local leftValue = GetMeetingStoneApplicantSortValue(left, sortKey)
        local rightValue = GetMeetingStoneApplicantSortValue(right, sortKey)
        if leftValue == rightValue then
            return GetMeetingStoneApplicantOriginalOrder(left) < GetMeetingStoneApplicantOriginalOrder(right)
        end

        if type(leftValue) == "number" and type(rightValue) == "number" then
            if descending then
                return leftValue > rightValue
            end
            return leftValue < rightValue
        end

        leftValue = string.lower(tostring(leftValue))
        rightValue = string.lower(tostring(rightValue))
        if descending then
            return leftValue > rightValue
        end
        return leftValue < rightValue
    end)
end

local function FormatMeetingStoneApplicantScore(totalScore, bestScore)
    local score = tonumber(totalScore) or 0
    if score <= 0 then
        return "-"
    end

    local scoreColor = C_ChallengeMode and C_ChallengeMode.GetDungeonScoreRarityColor and C_ChallengeMode.GetDungeonScoreRarityColor(score) or HIGHLIGHT_FONT_COLOR
    local scoreText = scoreColor and scoreColor.WrapTextInColorCode and scoreColor:WrapTextInColorCode(tostring(score)) or tostring(score)
    if type(bestScore) == "table" and tonumber(bestScore.mapScore) and bestScore.mapScore > 0 then
        local mapScore = math.floor(bestScore.mapScore + 0.5)
        local mapColor = C_ChallengeMode and C_ChallengeMode.GetSpecificDungeonOverallScoreRarityColor and C_ChallengeMode.GetSpecificDungeonOverallScoreRarityColor(mapScore) or HIGHLIGHT_FONT_COLOR
        local mapText = mapColor and mapColor.WrapTextInColorCode and mapColor:WrapTextInColorCode(tostring(mapScore)) or tostring(mapScore)
        local level = tonumber(bestScore.bestRunLevel) or 0
        local levelText = bestScore.finishedSuccess and string.format("|cff00ff00%d%s|r", level, TRANSLATE("层")) or string.format("|cff7f7f7f%d%s|r", level, TRANSLATE("层"))
        return string.format("%s / %s / %s", scoreText, mapText, levelText)
    end

    return string.format("%s / |cff7f7f7f%s|r", scoreText, TRANSLATE("无"))
end

local function GetMeetingStoneApplicantSpecName(specID, fallbackSpecName)
    local fallback = type(fallbackSpecName) == "string" and fallbackSpecName ~= "" and fallbackSpecName or nil
    if not specID or not GetSpecializationInfoByID then
        return fallback
    end

    local sex = UnitSex and UnitSex("player") or nil
    local specName = select(2, GetSpecializationInfoByID(specID, sex))
    if type(specName) == "string" and specName ~= "" then
        return specName
    end

    return fallback
end

local function GetMeetingStoneApplicantClassSpecText(applicant)
    local classText = applicant and applicant.class or nil
    local specName = applicant and GetMeetingStoneApplicantSpecName(applicant.specID, applicant.specName) or nil
    if specName and classText then
        if CLUB_FINDER_LOOKING_FOR_CLASS_SPEC then
            return CLUB_FINDER_LOOKING_FOR_CLASS_SPEC:format(specName, classText)
        end
        return specName .. classText
    end

    return classText or "-"
end

local function GetMeetingStoneApplicantSpecIconTexture(applicant)
    if core.GetSpecIconTexture then
        return core.GetSpecIconTexture(applicant and applicant.specID, applicant and applicant.classFile)
    end

    local specID = tonumber(applicant and applicant.specID)
    local specIconFile = specID and MEETINGSTONE_APPLICANT_SPEC_ICON_BY_ID[specID]
    if specIconFile then
        return MK_ICON_PATH .. specIconFile
    end

    local classFile = applicant and applicant.classFile
    if type(classFile) == "string" and classFile ~= "" then
        return MK_ICON_PATH .. string.lower(classFile) .. "_flatborder2.tga"
    end

    return nil
end

local function AddMeetingStoneApplicantTooltipSeparator(tooltip)
    if tooltip.AddSeparator then
        tooltip:AddSeparator()
        return
    end
    if tooltip.AddSepatator then
        tooltip:AddSepatator()
        return
    end
    tooltip:AddLine(" ")
end

local function AddMeetingStoneApplicantProvingGrounds(tooltip, stats)
    if type(stats) ~= "table" then
        return
    end

    local hasStats = false
    for _, value in pairs(stats) do
        if value and value ~= 0 then
            hasStats = true
            break
        end
    end
    if not hasStats then
        return
    end

    AddMeetingStoneApplicantTooltipSeparator(tooltip)
        tooltip:AddLine(LFG_LIST_PROVING_GROUND_TITLE or TRANSLATE("试炼场"), 1, 0.82, 0)
    for _, group in ipairs(MEETINGSTONE_APPLICANT_PROVING_GROUND_DATA) do
        for _, entry in ipairs(group) do
            if entry.id and stats[entry.id] and entry.text then
                tooltip:AddLine(entry.text, 1, 1, 1)
                break
            end
        end
    end
end

local function AddMeetingStoneApplicantBlacklistTooltipLine(tooltip, applicant)
    if not (tooltip and applicant and applicant.isBlacklisted == true) then
        return
    end

    local note = type(applicant.blacklistRecord) == "table" and applicant.blacklistRecord.note or nil
    note = type(note) == "string" and note or ""
    local text = string.format(TRANSLATE("%s：%s"), TRANSLATE("黑名单"), note ~= "" and note or TRANSLATE("无备注"))
    AddMeetingStoneApplicantTooltipSeparator(tooltip)
    tooltip:AddLine(
        text,
        MEETINGSTONE_APPLICANT_BLACKLIST_TOOLTIP_COLOR[1],
        MEETINGSTONE_APPLICANT_BLACKLIST_TOOLTIP_COLOR[2],
        MEETINGSTONE_APPLICANT_BLACKLIST_TOOLTIP_COLOR[3],
        true
    )
end

local function ShowMeetingStoneApplicantTooltip(row)
    local applicant = row and row.mkMeetingStoneApplicant
    if not applicant then
        return
    end

    local tooltip = GameTooltip
    tooltip:SetOwner(row, "ANCHOR_RIGHT")
    tooltip:ClearLines()

    local classColors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    local classColor = applicant.classFile and classColors and classColors[applicant.classFile] or HIGHLIGHT_FONT_COLOR
    if applicant.name then
        tooltip:AddLine(applicant.name, classColor.r or 1, classColor.g or 1, classColor.b or 1)
        tooltip:AddLine(string.format(UNIT_TYPE_LEVEL_TEMPLATE or TRANSLATE("等级%d %s"), tonumber(applicant.level) or 0, GetMeetingStoneApplicantClassSpecText(applicant)), 1, 1, 1)
    else
        tooltip:AddLine(UNKNOWN or "-", 1, 1, 1)
    end

    tooltip:AddLine(string.format(LFG_LIST_ITEM_LEVEL_CURRENT or TRANSLATE("物品等级: %d"), math.floor((tonumber(applicant.itemLevel) or 0) + 0.5)), 1, 1, 1)
    local currentSpecName = GetMeetingStoneApplicantSpecName(applicant.specID, applicant.specName)
    if currentSpecName then
        local specColor = GREEN_FONT_COLOR or { r = 0, g = 1, b = 0 }
        tooltip:AddLine(string.format(TRANSLATE("当前专精：%s"), currentSpecName), specColor.r or 0, specColor.g or 1, specColor.b or 0)
    end
    if applicant.honorLevel and applicant.useHonorLevel and LFG_LIST_HONOR_LEVEL_CURRENT_PVP then
        tooltip:AddLine(string.format(LFG_LIST_HONOR_LEVEL_CURRENT_PVP, applicant.honorLevel), 1, 1, 1)
    end

    local score = tonumber(applicant.score) or 0
    if applicant.isMythicPlusActivity or score > 0 then
        local scoreColor = C_ChallengeMode and C_ChallengeMode.GetDungeonScoreRarityColor and C_ChallengeMode.GetDungeonScoreRarityColor(score) or HIGHLIGHT_FONT_COLOR
        local scoreText = scoreColor and scoreColor.WrapTextInColorCode and scoreColor:WrapTextInColorCode(tostring(score)) or tostring(score)
        tooltip:AddLine(string.format(TRANSLATE("大秘评分: %s"), scoreText), 1, 0.82, 0)

        local bestScore = applicant.bestScore
        if type(bestScore) == "table" and tonumber(bestScore.mapScore) and bestScore.mapScore > 0 then
            local mapScore = math.floor(bestScore.mapScore + 0.5)
            local mapColor = C_ChallengeMode and C_ChallengeMode.GetSpecificDungeonOverallScoreRarityColor and C_ChallengeMode.GetSpecificDungeonOverallScoreRarityColor(mapScore) or HIGHLIGHT_FONT_COLOR
            local mapText = mapColor and mapColor.WrapTextInColorCode and mapColor:WrapTextInColorCode(tostring(mapScore)) or tostring(mapScore)
            local level = tonumber(bestScore.bestRunLevel) or 0
            local levelText = bestScore.finishedSuccess and string.format("|cff00ff00%d%s|r", level, TRANSLATE("层")) or string.format("|cff7f7f7f%d%s|r", level, TRANSLATE("层"))
            tooltip:AddLine(string.format(TRANSLATE("当前副本: %s / %s"), mapText, levelText), 1, 0.82, 0)
        else
            tooltip:AddLine(TRANSLATE("当前副本: |cff7f7f7f无信息|r"), 1, 0.82, 0)
        end
    end

    if applicant.comment and applicant.comment ~= "" then
        tooltip:AddLine(" ")
        tooltip:AddLine(applicant.comment, GREEN_FONT_COLOR.r, GREEN_FONT_COLOR.g, GREEN_FONT_COLOR.b, true)
    end

    AddMeetingStoneApplicantProvingGrounds(tooltip, applicant.stats)
    AddMeetingStoneApplicantBlacklistTooltipLine(tooltip, applicant)
    tooltip:Show()
end

local function HideMeetingStoneApplicantTooltip(row)
    if GameTooltip and GameTooltip.GetOwner and GameTooltip:GetOwner() == row then
        GameTooltip:Hide()
    end
end

local function AnchorMeetingStoneApplicantActionButtonIcon(button, x, y)
    local visual = button and (button.mkMeetingStoneActionIcon or button.mkMeetingStoneActionFallbackText)
    if not visual then
        return
    end

    visual:ClearAllPoints()
    visual:SetPoint("CENTER", button, "CENTER", x or 0, y or 0)
end

local function UpdateMeetingStoneApplicantActionButtonVisual(button)
    if not button then
        return
    end

    if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
        core.MeetingStoneButtonArt.Update(button)
    end

    local enabled = not (button.IsEnabled and not button:IsEnabled())
    local pressed = enabled and button.mkMeetingStoneButtonPressed == true
    local hovered = enabled and button.IsMouseOver and button:IsMouseOver()
    local color = MEETINGSTONE_APPLICANT_ACTION_VISUAL_COLORS.normal
    if not enabled then
        color = MEETINGSTONE_APPLICANT_ACTION_VISUAL_COLORS.disabled
    elseif pressed then
        color = MEETINGSTONE_APPLICANT_ACTION_VISUAL_COLORS.pressed
    elseif hovered then
        color = MEETINGSTONE_APPLICANT_ACTION_VISUAL_COLORS.hover
    end

    if button.mkMeetingStoneActionIcon and button.mkMeetingStoneActionIcon.SetVertexColor then
        button.mkMeetingStoneActionIcon:SetVertexColor(color[1], color[2], color[3], color[4])
    end
    if button.mkMeetingStoneActionFallbackText and button.mkMeetingStoneActionFallbackText.SetTextColor then
        button.mkMeetingStoneActionFallbackText:SetTextColor(color[1], color[2], color[3], color[4])
    end

    AnchorMeetingStoneApplicantActionButtonIcon(button, pressed and 1 or 0, pressed and -1 or 0)
end

local function CreateMeetingStoneApplicantActionButton(parent, labelText, tooltip, atlas, fallbackText)
    local button = CreateMeetingStoneSystemButton(
        parent,
        nil,
        MEETINGSTONE_APPLICANT_ACTION_BUTTON.size,
        MEETINGSTONE_APPLICANT_ACTION_BUTTON.size,
        ""
    )
    button:SetFrameLevel(parent:GetFrameLevel() + 4)
    button:SetScale(MEETINGSTONE_APPLICANT_ACTION_BUTTON.scale)

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

    local icon = button:CreateTexture(nil, "OVERLAY")
    icon:SetSize(MEETINGSTONE_APPLICANT_ACTION_BUTTON.iconSize, MEETINGSTONE_APPLICANT_ACTION_BUTTON.iconSize)
    icon:SetPoint("CENTER", button, "CENTER", 0, 0)
    icon:SetVertexColor(1, 1, 1, 1)
    if atlas and TrySetMeetingStoneAtlas(icon, atlas, false) then
        button.mkMeetingStoneActionIcon = icon
    else
        icon:Hide()
        local fallback = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        fallback:SetText(fallbackText or labelText or "")
        fallback:SetTextColor(1, 0.96, 0.86, 1)
        fallback:SetJustifyH("CENTER")
        fallback:SetJustifyV("MIDDLE")
        if core.ApplyFont then
            core.ApplyFont(fallback, 18, "OUTLINE")
        end
        button.mkMeetingStoneActionFallbackText = fallback
    end
    AnchorMeetingStoneApplicantActionButtonIcon(button, 0, 0)

    button.tooltip = tooltip
    button:SetScript("OnEnter", function(self)
        UpdateMeetingStoneApplicantActionButtonVisual(self)
        if self.tooltip then
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(self.tooltip)
            GameTooltip:Show()
        end
    end)
    button:SetScript("OnLeave", function(self)
        self.mkMeetingStoneButtonPressed = nil
        UpdateMeetingStoneApplicantActionButtonVisual(self)
        GameTooltip_Hide()
    end)
    button:SetScript("OnMouseDown", function(self, mouseButton)
        if mouseButton == "LeftButton" then
            self.mkMeetingStoneButtonPressed = true
            UpdateMeetingStoneApplicantActionButtonVisual(self)
        end
    end)
    button:SetScript("OnMouseUp", function(self)
        self.mkMeetingStoneButtonPressed = nil
        UpdateMeetingStoneApplicantActionButtonVisual(self)
    end)
    UpdateMeetingStoneApplicantActionButtonVisual(button)
    return button
end

local function SetMeetingStoneApplicantActionButtonEnabled(button, enabled)
    if not button then
        return
    end

    if enabled then
        button:Enable()
    else
        button.mkMeetingStoneButtonPressed = nil
        button:Disable()
    end
    if button.EnableMouse then
        button:EnableMouse(enabled == true)
    end
    if button.SetMouseClickEnabled then
        pcall(button.SetMouseClickEnabled, button, enabled == true)
    end
    if button.SetMouseMotionEnabled then
        pcall(button.SetMouseMotionEnabled, button, enabled == true)
    end
    if button.SetAlpha then
        button:SetAlpha(enabled and 1 or 0.42)
    end
    UpdateMeetingStoneApplicantActionButtonVisual(button)
end

local function GetMeetingStoneApplicantContextMenuName(applicantID, memberIndex, applicant)
    if applicant and applicant.mkMeetingStoneTestRow and type(applicant.name) == "string" and applicant.name ~= "" then
        return applicant.name
    end

    return GetMeetingStoneApplicantMemberInfo(applicantID, memberIndex or 1)
end

local function MarkMeetingStoneApplicantTestRowBlacklisted(applicant, record)
    if not (applicant and applicant.mkMeetingStoneTestRow) then
        return
    end

    applicant.isBlacklisted = true
    applicant.blacklistRecord = record
    applicant.blacklistKey = record and record.key or applicant.blacklistKey
end

local function ShowMeetingStoneApplicantContextMenu(anchor, applicantID, memberIndex, applicant)
    if not MenuUtil then
        return
    end

    local isTestRow = applicant and applicant.mkMeetingStoneTestRow == true
    if not isTestRow and not (C_LFGList and C_LFGList.GetApplicantMemberInfo) then
        return
    end

    local menuToken = LockMeetingStoneApplicantContextMenu(anchor)
    local menu = MenuUtil.CreateContextMenu(anchor, function(_, rootDescription)
        rootDescription:SetTag("MENU_MK_MEETINGSTONE_APPLICANT")

        local name = GetMeetingStoneApplicantContextMenuName(applicantID, memberIndex, applicant)
        local hasName = type(name) == "string" and name ~= ""
        rootDescription:CreateTitle(name or "")

        local whisperButton = rootDescription:CreateButton(WHISPER, function()
            if hasName and ChatFrameUtil and ChatFrameUtil.SendTell then
                ChatFrameUtil.SendTell(name)
            end
        end)
        local reportButton = rootDescription:CreateButton(LFG_LIST_REPORT_PLAYER or REPORT_PLAYER or TRANSLATE("举报玩家"), function()
            if not isTestRow and LFGList_ReportApplicant then
                pcall(LFGList_ReportApplicant, applicantID, name or "")
            end
        end)
        local blacklistButton = rootDescription:CreateButton(BLACKLIST_MENU_ICON_MARKUP .. BLACKLIST_MENU_COLOR_CODE .. TRANSLATE("加入黑名单") .. BLACKLIST_MENU_RESET_CODE, function()
            if hasName and MK and MK.AddPlayerToBlacklist then
                local ok, record = MK:AddPlayerToBlacklist(
                    name,
                    nil,
                    MK.GetBlacklistNoteManual and MK:GetBlacklistNoteManual() or "",
                    "meetingstone_applicant_menu"
                )
                if ok then
                    MarkMeetingStoneApplicantTestRowBlacklisted(applicant, record)
                    if MK.PrintBlacklistAdded then
                        MK:PrintBlacklistAdded(record)
                    end
                    if MK.RefreshMeetingStoneApplicantsOnly then
                        MK:RefreshMeetingStoneApplicantsOnly("MeetingStoneApplicantBlacklist")
                    elseif MK.RefreshMainWindow then
                        MK:RefreshMainWindow("MeetingStoneApplicantBlacklist")
                    end
                end
            end
        end)

        if not hasName then
            whisperButton:SetEnabled(false)
            reportButton:SetEnabled(false)
            blacklistButton:SetEnabled(false)
        elseif isTestRow then
            whisperButton:SetEnabled(false)
            reportButton:SetEnabled(false)
        end
    end)
    if menu and menu.SetClosedCallback then
        menu:SetClosedCallback(function()
            FinishMeetingStoneApplicantContextMenu(anchor, menuToken)
        end)
    else
        FinishMeetingStoneApplicantContextMenu(anchor, menuToken)
    end
end

local function CreateMeetingStoneApplicantRow(parent)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(GROUP_LIST_ROW_HEIGHT)
    row:EnableMouse(true)
    row:SetScript("OnEnter", function(self)
        SetMeetingStoneApplicantRowHover(self, true)
        ShowMeetingStoneApplicantTooltip(self)
    end)
    row:SetScript("OnLeave", function(self)
        SetMeetingStoneApplicantRowHover(self, false)
        HideMeetingStoneApplicantTooltip(self)
    end)
    row:SetScript("OnMouseUp", function(self, button)
        if button == "RightButton" and CanManageMeetingStoneApplicantActions() then
            local applicant = self.mkMeetingStoneApplicant
            if self.applicantID or (applicant and applicant.mkMeetingStoneTestRow) then
                ShowMeetingStoneApplicantContextMenu(self, self.applicantID, self.memberIndex, applicant)
            end
        end
    end)

    local backgroundPieces = CreateMeetingStoneApplicantBackgroundTextures(row, 0)
    local backgroundTransitionPieces = CreateMeetingStoneApplicantBackgroundTextures(row, 1)
    SetMeetingStoneApplicantBackgroundPiecesShown(backgroundTransitionPieces, false)

    local hoverLeft, hover, hoverRight = CreateMeetingStoneApplicantHoverTextures(row)

    local layout = GetMeetingStoneApplicantColumnLayout()
    local function addText(key, justify)
        local fs = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        fs:SetPoint("LEFT", row, "LEFT", layout[key].x, 0)
        fs:SetWidth(layout[key].width)
        fs:SetHeight(18)
        fs.mkMeetingStoneApplicantJustifyH = justify or "CENTER"
        fs:SetJustifyH(fs.mkMeetingStoneApplicantJustifyH)
        if core.ApplyFont then
            core.ApplyFont(fs, 12, "")
        end
        return fs
    end

    local function addTexture(key, width, height)
        local texture = row:CreateTexture(nil, "OVERLAY")
        texture:SetPoint("CENTER", row, "LEFT", layout[key].x + (layout[key].width / 2), 0)
        texture:SetSize(width or MEETINGSTONE_APPLICANT_LIST_ICON_SIZE, height or MEETINGSTONE_APPLICANT_LIST_ICON_SIZE)
        texture.mkMeetingStoneApplicantIconSize = width or MEETINGSTONE_APPLICANT_LIST_ICON_SIZE
        texture:Hide()
        return texture
    end

    local accountTypeContainer = CreateFrame("Frame", nil, row)
    accountTypeContainer:SetPoint("LEFT", row, "LEFT", layout.star.x, 0)
    accountTypeContainer:SetSize(layout.star.width, 18)
    local accountTypeIconSlot = CreateFrame("Frame", nil, accountTypeContainer)
    accountTypeIconSlot:SetSize(MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SLOT_SIZE, MEETINGSTONE_APPLICANT_ACCOUNT_TYPE_ICON_SLOT_SIZE)
    accountTypeIconSlot:SetPoint("LEFT", accountTypeContainer, "LEFT", 0, 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 rolesContainer = CreateFrame("Frame", nil, row)
    rolesContainer:SetPoint("LEFT", row, "LEFT", layout.role.x, 0)
    rolesContainer:SetSize(layout.role.width, MEETINGSTONE_APPLICANT_ROLE_ICON_SIZE)
    local roles = {}
    for _, roleKey in ipairs(MK:GetRoleOrder()) do
        roles[roleKey] = CreateMeetingStoneApplicantRoleFrame(rolesContainer, roleKey)
    end

    local actionContainer = CreateFrame("Frame", nil, row)
    actionContainer:SetFrameLevel(row:GetFrameLevel() + 3)
    local inviteButtonHost = CreateFrame("Frame", nil, actionContainer)
    inviteButtonHost:SetFrameLevel(actionContainer:GetFrameLevel() + 1)
    local declineButtonHost = CreateFrame("Frame", nil, actionContainer)
    declineButtonHost:SetFrameLevel(actionContainer:GetFrameLevel() + 1)

    local inviteButton = CreateMeetingStoneApplicantActionButton(
        inviteButtonHost,
        TRANSLATE("邀请"),
        TRANSLATE("邀请"),
        MEETINGSTONE_APPLICANT_ACTION_BUTTON.inviteAtlas,
        "✓"
    )
    local declineButton = CreateMeetingStoneApplicantActionButton(
        declineButtonHost,
        TRANSLATE("拒绝"),
        TRANSLATE("拒绝"),
        MEETINGSTONE_APPLICANT_ACTION_BUTTON.declineAtlas,
        "×"
    )
    local actionStatus = actionContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    actionStatus:SetJustifyH("CENTER")
    actionStatus:SetJustifyV("MIDDLE")
    actionStatus:SetTextColor(
        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[1],
        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[2],
        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[3],
        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[4]
    )
    if core.ApplyFont then
        core.ApplyFont(actionStatus, 12, "")
    end
    actionStatus:Hide()

    row.widgets = {
        backgroundPieces = backgroundPieces,
        backgroundTransitionPieces = backgroundTransitionPieces,
        background = backgroundPieces.middle,
        accountTypeContainer = accountTypeContainer,
        accountTypeIconSlot = accountTypeIconSlot,
        accountTypeIcon = accountTypeIcon,
        accountTypeLabel = accountTypeLabel,
        bnetIcon = addTexture("bnet", MEETINGSTONE_APPLICANT_BNET_ICON_SIZE, MEETINGSTONE_APPLICANT_BNET_ICON_SIZE),
        name = addText("name"),
        rolesContainer = rolesContainer,
        roles = roles,
        classIcon = addTexture("class", MEETINGSTONE_APPLICANT_CLASS_ICON_SIZE, MEETINGSTONE_APPLICANT_CLASS_ICON_SIZE),
        score = addText("score"),
        itemLevel = addText("itemLevel"),
        message = addText("description", "LEFT"),
        actionContainer = actionContainer,
        inviteButtonHost = inviteButtonHost,
        declineButtonHost = declineButtonHost,
        inviteButton = inviteButton,
        declineButton = declineButton,
        actionStatus = actionStatus,
        hoverLeft = hoverLeft,
        hover = hover,
        hoverRight = hoverRight,
    }

    ApplyMeetingStoneApplicantRowLayout(row)

    inviteButton:HookScript("OnEnter", function()
        SetMeetingStoneApplicantRowHover(row, true)
    end)
    inviteButton:HookScript("OnLeave", function()
        SetMeetingStoneApplicantRowHover(row, false)
    end)
    declineButton:HookScript("OnEnter", function()
        SetMeetingStoneApplicantRowHover(row, true)
    end)
    declineButton:HookScript("OnLeave", function()
        SetMeetingStoneApplicantRowHover(row, false)
    end)

    inviteButton:SetScript("OnClick", function(button)
        if button.mkMeetingStoneTestRow then
            return
        end
        if button.applicantID and C_LFGList and C_LFGList.InviteApplicant then
            if PlaySound and SOUNDKIT then
                PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
            end
            if not IsInRaid(LE_PARTY_CATEGORY_HOME)
                and GetNumGroupMembers
                and C_LFGList.GetNumInvitedApplicantMembers
                and button.numMembers
                and GetNumGroupMembers(LE_PARTY_CATEGORY_HOME) + button.numMembers + C_LFGList.GetNumInvitedApplicantMembers() > MAX_PARTY_MEMBERS + 1
            then
                StaticPopup_Show("LFG_LIST_INVITING_CONVERT_TO_RAID", nil, nil, button.applicantID)
            else
                local ok = pcall(C_LFGList.InviteApplicant, button.applicantID)
                if ok then
                    if C_LFGList.RefreshApplicants then
                        pcall(C_LFGList.RefreshApplicants)
                    end
                    if MK and MK.RefreshMeetingStoneApplicantsOnly then
                        MK:RefreshMeetingStoneApplicantsOnly("MeetingStoneApplicantInvite")
                    elseif MK and MK.RefreshMainWindow then
                        MK:RefreshMainWindow("MeetingStoneApplicantInvite")
                    end
                end
            end
        end
    end)
    declineButton:SetScript("OnClick", function(button)
        if button.mkMeetingStoneTestRow then
            return
        end
        if button.applicantID and C_LFGList then
            if PlaySound and SOUNDKIT then
                PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
            end
            DeclineMeetingStoneApplicant(button.applicantID)
        end
    end)

    return row
end

function MK:EnsureMeetingStoneApplicantRows(count)
    local widgets = self.widgets
    if not widgets or not widgets.meetingStoneApplicantRowsFrame then
        return
    end

    widgets.meetingStoneApplicantRows = widgets.meetingStoneApplicantRows or {}
    for index = #widgets.meetingStoneApplicantRows + 1, count do
        widgets.meetingStoneApplicantRows[index] = CreateMeetingStoneApplicantRow(widgets.meetingStoneApplicantRowsFrame)
    end
end

local function GetMeetingStoneApplicantViewportWidth(widgets)
    local scrollFrame = widgets and widgets.meetingStoneApplicantScrollFrame
    if scrollFrame and scrollFrame.GetWidth then
        local width = scrollFrame:GetWidth() or 0
        if width > 0 then
            return width
        end
    end

    local rowsFrame = widgets and widgets.meetingStoneApplicantRowsFrame
    if rowsFrame and rowsFrame.GetWidth then
        return rowsFrame:GetWidth() or 0
    end

    return 0
end

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

local function UpdateMeetingStoneApplicantScrollState(widgets, rowCount, resetScroll)
    local scrollFrame = widgets and widgets.meetingStoneApplicantScrollFrame
    local content = widgets and widgets.meetingStoneApplicantRowsFrame
    local scrollBar = widgets and widgets.meetingStoneApplicantScrollBar
    if not (scrollFrame and content and scrollBar) then
        return
    end

    rowCount = tonumber(rowCount) or 0
    local viewportWidth = math.max(scrollFrame:GetWidth() or 0, 1)
    local viewportHeight = math.max(scrollFrame:GetHeight() or 0, 1)
    local rowsHeight = math.max(0, rowCount * GROUP_LIST_ROW_HEIGHT)
    local contentHeight = math.max(rowsHeight, viewportHeight, 1)

    content:SetWidth(viewportWidth)
    content:SetHeight(contentHeight)
    if scrollFrame.UpdateScrollChildRect then
        scrollFrame:UpdateScrollChildRect()
    end

    local maxScroll = scrollFrame.GetVerticalScrollRange and scrollFrame:GetVerticalScrollRange() or math.max(rowsHeight - viewportHeight, 0)
    maxScroll = math.max(maxScroll or 0, 0)
    local currentValue = 0
    if resetScroll == true then
        currentValue = 0
    elseif scrollBar.mkGetScrollValue then
        currentValue = scrollBar.mkGetScrollValue() or 0
    elseif scrollBar.GetValue then
        currentValue = scrollBar:GetValue() or 0
    end
    currentValue = math.min(math.max(currentValue, 0), maxScroll)

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

    if maxScroll > 0 then
        scrollBar:Show()
        SetMeetingStoneApplicantScrollValue(scrollFrame, scrollBar, currentValue)
    else
        SetMeetingStoneApplicantScrollValue(scrollFrame, scrollBar, 0)
        scrollBar:Hide()
    end
    widgets.mkMeetingStoneApplicantLastRowCount = rowCount
end

function ApplicantList.Refresh(owner, widgets, refreshSource)
    if not owner or not widgets then
        return
    end

    if IsMeetingStoneApplicantManualRefresh(refreshSource) then
        owner.meetingStoneApplicantSort = nil
    end

    local applicantViewportWidth = GetMeetingStoneApplicantViewportWidth(widgets)
    if widgets.meetingStoneApplicantRowsFrame and widgets.meetingStoneApplicantRowsFrame.SetWidth and applicantViewportWidth > 0 then
        widgets.meetingStoneApplicantRowsFrame:SetWidth(applicantViewportWidth)
    end
    local applicantAvailableWidth = applicantViewportWidth > 0 and math.max(applicantViewportWidth - 12, 1) or nil
    if widgets.meetingStoneApplicantHeader then
        ApplicantList.ApplyHeaderLayout(widgets.meetingStoneApplicantHeader, applicantAvailableWidth)
    end
    if refreshSource == "MeetingStoneApplicantHeaderSort" then
        UpdateMeetingStoneApplicantScrollState(widgets, widgets.mkMeetingStoneApplicantLastRowCount or 0, true)
    end

    local meetingStoneAvailable = not owner.IsMeetingStoneManagementAvailable or owner:IsMeetingStoneManagementAvailable()
    if not meetingStoneAvailable then
        owner.mkMeetingStoneSuppressedInactiveApplicantKeys = nil
        for _, row in ipairs(widgets.meetingStoneApplicantRows or {}) do
            SetMeetingStoneApplicantRowHover(row, false)
            HideMeetingStoneApplicantTooltip(row)
            row.mkMeetingStoneApplicant = nil
            row.mkMeetingStoneTooltipAnchor = nil
            row.mkMeetingStoneTestRow = nil
            row.mkMeetingStoneApplicantGroupKey = nil
            row.applicantID = nil
            row.memberIndex = nil
            if row.EnableMouse then
                row:EnableMouse(false)
            end
            if row.widgets then
                if row.widgets.inviteButton then
                    row.widgets.inviteButton.applicantID = nil
                    row.widgets.inviteButton.numMembers = nil
                    row.widgets.inviteButton.mkMeetingStoneTestRow = nil
                    SetMeetingStoneApplicantActionButtonEnabled(row.widgets.inviteButton, false)
                end
                if row.widgets.declineButton then
                    row.widgets.declineButton.applicantID = nil
                    row.widgets.declineButton.mkMeetingStoneTestRow = nil
                    SetMeetingStoneApplicantActionButtonEnabled(row.widgets.declineButton, false)
                end
                if row.widgets.actionStatus then
                    row.widgets.actionStatus:SetText("")
                    row.widgets.actionStatus:Hide()
                end
            end
            row:Hide()
        end
        if widgets.meetingStoneApplicantEmpty then
            UpdateMeetingStoneApplicantEmptyBlockLayout(widgets, false)
            widgets.meetingStoneApplicantEmpty:SetTextColor(
                MEETINGSTONE_APPLICANT_MISSING_ADDON_TEXT_COLOR[1],
                MEETINGSTONE_APPLICANT_MISSING_ADDON_TEXT_COLOR[2],
                MEETINGSTONE_APPLICANT_MISSING_ADDON_TEXT_COLOR[3],
                MEETINGSTONE_APPLICANT_MISSING_ADDON_TEXT_COLOR[4]
            )
            widgets.meetingStoneApplicantEmpty:SetText(TRANSLATE(MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY))
            widgets.meetingStoneApplicantEmpty:Show()
        end
        SetMeetingStoneApplicantEmptyAnimationShown(widgets, false)
        if owner.SetMeetingStoneQueueStatusGlow then
            owner:SetMeetingStoneQueueStatusGlow(false)
        end
        if owner.FadeOutMeetingStoneEmptyApplicantEye then
            owner:FadeOutMeetingStoneEmptyApplicantEye()
        end
        UpdateMeetingStoneApplicantScrollState(widgets, 0, true)
        return
    end

    local canManageApplicants = CanManageMeetingStoneApplicantActions()
    local applicants = GetMeetingStoneApplicants()
    local activeInfo = GetMeetingStoneApplicantActiveEntryInfo()
    ApplicantList.CacheCurrentApplicants(owner)
    local hasActiveEntry = HasMeetingStoneApplicantActiveEntry()
        or activeInfo ~= nil
        or #applicants > 0
    local suppressedInactiveKeys = GetMeetingStoneSuppressedInactiveApplicantKeys(owner)
    local manualRefresh = IsMeetingStoneApplicantManualRefresh(refreshSource)
    local visibleApplicantIDs = {}
    local visibleApplicantCount = 0
    local applicantRows = {}
    if hasActiveEntry then
        local activityID = activeInfo and activeInfo.activityIDs and activeInfo.activityIDs[1]
        local isMythicPlusActivity = activeInfo and activeInfo.isMythicPlusActivity
        local applicantRowsByKey = {}
        local applicantRowOrder = {}
        local sourceIndex = 0
        local rosterMemberKeys = BuildMeetingStoneRosterMemberKeys()
        for _, applicantID in ipairs(applicants) do
            local info = GetMeetingStoneApplicantInfo(applicantID)
            local isInactive = IsMeetingStoneApplicantInactive(info)
            local shouldRetainInactive = ShouldRetainMeetingStoneInactiveApplicant(info)
            if type(info) == "table" and (not isInactive or shouldRetainInactive) then
                local numMembers = info and info.numMembers or 0
                for memberIndex = 1, numMembers do
                    local name, class, localizedClass, level, itemLevel, honorLevel, tank, healer, damage, assignedRole, relationship, dungeonScore, _, _, _, specID, isLeaver = GetMeetingStoneApplicantMemberInfo(applicantID, memberIndex)
                    local alreadyInRoster = IsMeetingStoneApplicantAlreadyInRoster(name, rosterMemberKeys)
                    local dedupKey = GetMeetingStoneApplicantDedupKey(name, memberIndex, applicantID)
                    if isInactive then
                        if manualRefresh then
                            suppressedInactiveKeys[dedupKey] = true
                        end
                    elseif suppressedInactiveKeys[dedupKey] then
                        suppressedInactiveKeys[dedupKey] = nil
                    end

                    if not alreadyInRoster and not (isInactive and suppressedInactiveKeys[dedupKey]) then
                        UpdateMeetingStoneApplicantLFGLeaver(owner, name, isLeaver)
                        if not isInactive and not visibleApplicantIDs[applicantID] then
                            visibleApplicantIDs[applicantID] = true
                            visibleApplicantCount = visibleApplicantCount + 1
                        end

                        local isBlacklisted, blacklistRecord, blacklistKey = false, nil, nil
                        if not isInactive then
                            isBlacklisted, blacklistRecord, blacklistKey = GetMeetingStoneApplicantBlacklistInfo(owner, name)
                        end
                        local bestScore = GetMeetingStoneApplicantDungeonScore(applicantID, memberIndex, activityID)
                        local stats = GetMeetingStoneApplicantMemberStats(applicantID, memberIndex)
                        local role = tank and "TANK" or healer and "HEALER" or damage and "DAMAGER" or assignedRole
                        local roleKey = GetMeetingStoneApplicantRoleKey(role)
                        local assignedRoleKey = GetMeetingStoneApplicantRoleKey(assignedRole)
                        local roles = {
                            TANK = tank == true,
                            HEAL = healer == true,
                            DPS = damage == true,
                        }
                        if assignedRoleKey and not (roles.TANK or roles.HEAL or roles.DPS) then
                            roles[assignedRoleKey] = true
                        elseif roleKey and not (roles.TANK or roles.HEAL or roles.DPS) then
                            roles[roleKey] = true
                        end

                        sourceIndex = sourceIndex + 1
                        local applicant = {
                            applicantID = applicantID,
                            memberIndex = memberIndex,
                            numMembers = numMembers,
                            name = name or "-",
                            shortName = name and Ambiguate and Ambiguate(name, "short") or name or "-",
                            role = role,
                            roles = roles,
                            level = level or 0,
                            honorLevel = honorLevel,
                            useHonorLevel = activityID and IsUseHonorLevel and IsUseHonorLevel(activityID),
                            relationship = relationship,
                            isLeaver = isLeaver == true,
                            isBlacklisted = isBlacklisted == true,
                            blacklistRecord = blacklistRecord,
                            blacklistKey = blacklistKey,
                            class = localizedClass or class or "-",
                            classFile = class,
                            specID = specID,
                            score = dungeonScore or 0,
                            bestScore = bestScore,
                            stats = stats,
                            activityID = activityID,
                            isMythicPlusActivity = isMythicPlusActivity,
                            itemLevel = itemLevel or 0,
                            comment = GetMeetingStoneApplicantDescription(info and info.comment or ""),
                            applicationStatus = info.applicationStatus,
                            pendingApplicationStatus = info.pendingApplicationStatus,
                            displayOrderID = info.displayOrderID,
                            statusRank = GetMeetingStoneApplicantStatusRank(info),
                            sourceIndex = sourceIndex,
                            isInactive = isInactive,
                            inactiveStatus = isInactive and info.applicationStatus or nil,
                            actionStatusText = isInactive and GetMeetingStoneInactiveApplicantActionLabel(info) or nil,
                        }
                        if ShouldReplaceMeetingStoneApplicantRow(applicant, applicantRowsByKey[dedupKey]) then
                            if not applicantRowsByKey[dedupKey] then
                                applicantRowOrder[#applicantRowOrder + 1] = dedupKey
                            end
                            applicantRowsByKey[dedupKey] = applicant
                        end
                    end
                end
            end
        end

        for _, dedupKey in ipairs(applicantRowOrder) do
            local applicant = applicantRowsByKey[dedupKey]
            if applicant then
                applicantRows[#applicantRows + 1] = applicant
            end
        end

        QueryMeetingStoneApplicantAccountTypes(applicantRows)

    else
        owner.mkMeetingStoneSuppressedInactiveApplicantKeys = nil
    end

    local testApplicants = CreateMeetingStoneApplicantTestRows(owner, nil, false)
    if type(testApplicants) == "table" then
        for _, testApplicant in ipairs(testApplicants) do
            applicantRows[#applicantRows + 1] = testApplicant
        end
    end

    SortMeetingStoneApplicantRows(owner, applicantRows)
    ArrangeMeetingStoneApplicantRowsByGroup(applicantRows)
    local animateRows = refreshSource ~= "MeetingStoneApplicantHeaderSort"
    owner:EnsureMeetingStoneApplicantRows(#applicantRows)
    for index, applicant in ipairs(applicantRows) do
        local row = widgets.meetingStoneApplicantRows[index]
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", widgets.meetingStoneApplicantRowsFrame, "TOPLEFT", 6, -((index - 1) * GROUP_LIST_ROW_HEIGHT))
        row:SetPoint("TOPRIGHT", widgets.meetingStoneApplicantRowsFrame, "TOPRIGHT", -6, -((index - 1) * GROUP_LIST_ROW_HEIGHT))
        ApplyMeetingStoneApplicantRowLayout(row, applicantAvailableWidth)
        SetMeetingStoneApplicantAccountType(row, applicant)
        SetMeetingStoneApplicantRowBackground(row, applicant, applicant.isInactive == true)
        if applicant.relationship then
            row.widgets.bnetIcon:SetTexture(MK_ICON_PATH .. "BattleNet.png")
            row.widgets.bnetIcon:SetTexCoord(0, 1, 0, 1)
            row.widgets.bnetIcon:Show()
        else
            row.widgets.bnetIcon:Hide()
        end

        row.widgets.name:SetText(applicant.shortName)
        local classColors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
        local classColor = applicant.classFile and classColors and classColors[applicant.classFile]
        if classColor then
            row.widgets.name:SetTextColor(classColor.r, classColor.g, classColor.b, 1)
        else
            row.widgets.name:SetTextColor(1, 1, 1, 1)
        end
        SetMeetingStoneApplicantRoles(row, applicant.roles)
        local specIconTexture = GetMeetingStoneApplicantSpecIconTexture(applicant)
        if specIconTexture then
            row.widgets.classIcon:SetTexture(specIconTexture)
        else
            row.widgets.classIcon:SetTexture(nil)
        end
        row.widgets.classIcon:SetTexCoord(0, 1, 0, 1)
        row.widgets.classIcon:SetShown(specIconTexture ~= nil)
        row.widgets.score:SetText(FormatMeetingStoneApplicantScore(applicant.score, applicant.bestScore))
        row.widgets.itemLevel:SetText(tostring(math.floor((tonumber(applicant.itemLevel) or 0) + 0.5)))
        row.widgets.message:SetText(applicant.comment)
        row.applicantID = applicant.applicantID
        row.memberIndex = applicant.memberIndex
        row.mkMeetingStoneTestRow = applicant.mkMeetingStoneTestRow == true
        row.mkMeetingStoneApplicantGroupKey = applicant.mkMeetingStoneGroupKey or applicant.mkMeetingStoneTestGroupKey or applicant.applicantID
        row.mkMeetingStoneApplicant = applicant
        row.mkMeetingStoneTooltipAnchor = widgets.root
        if row.EnableMouse then
            row:EnableMouse(true)
        end
        local isApplicantGroup = tonumber(applicant.mkMeetingStoneGroupSize) and applicant.mkMeetingStoneGroupSize > 1
        local showActionControls = not isApplicantGroup or applicant.mkMeetingStoneGroupIndex == applicant.mkMeetingStoneGroupActionIndex
        local canUseActionControls = canManageApplicants and showActionControls
        if applicant.isInactive then
            if row.widgets.inviteButtonHost then row.widgets.inviteButtonHost:Hide() end
            if row.widgets.declineButtonHost then row.widgets.declineButtonHost:Hide() end
            if row.widgets.inviteButton then
                row.widgets.inviteButton.applicantID = nil
                row.widgets.inviteButton.numMembers = nil
                row.widgets.inviteButton.mkMeetingStoneTestRow = nil
                SetMeetingStoneApplicantActionButtonEnabled(row.widgets.inviteButton, false)
            end
            if row.widgets.declineButton then
                row.widgets.declineButton.applicantID = nil
                row.widgets.declineButton.mkMeetingStoneTestRow = nil
                SetMeetingStoneApplicantActionButtonEnabled(row.widgets.declineButton, false)
            end
            if row.widgets.actionStatus then
                if showActionControls then
                    row.widgets.actionStatus:SetText(applicant.actionStatusText or "")
                    row.widgets.actionStatus:SetTextColor(
                        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[1],
                        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[2],
                        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[3],
                        MEETINGSTONE_APPLICANT_CANCELLED_TEXT_COLOR[4]
                    )
                    row.widgets.actionStatus:Show()
                else
                    row.widgets.actionStatus:SetText("")
                    row.widgets.actionStatus:Hide()
                end
            end
        else
            if row.widgets.inviteButtonHost then row.widgets.inviteButtonHost:SetShown(showActionControls) end
            if row.widgets.declineButtonHost then row.widgets.declineButtonHost:SetShown(showActionControls) end
            if row.widgets.inviteButton then
                if canUseActionControls then
                    row.widgets.inviteButton.applicantID = applicant.applicantID
                    row.widgets.inviteButton.numMembers = applicant.numMembers
                    row.widgets.inviteButton.mkMeetingStoneTestRow = applicant.mkMeetingStoneTestRow == true
                    row.widgets.inviteButton.tooltip = isApplicantGroup and TRANSLATE("整队邀请") or TRANSLATE("邀请")
                    SetMeetingStoneApplicantActionButtonEnabled(row.widgets.inviteButton, true)
                else
                    row.widgets.inviteButton.applicantID = nil
                    row.widgets.inviteButton.numMembers = nil
                    row.widgets.inviteButton.mkMeetingStoneTestRow = nil
                    row.widgets.inviteButton.tooltip = nil
                    SetMeetingStoneApplicantActionButtonEnabled(row.widgets.inviteButton, false)
                end
            end
            if row.widgets.declineButton then
                if canUseActionControls then
                    row.widgets.declineButton.applicantID = applicant.applicantID
                    row.widgets.declineButton.mkMeetingStoneTestRow = applicant.mkMeetingStoneTestRow == true
                    row.widgets.declineButton.tooltip = isApplicantGroup and TRANSLATE("整队拒绝") or TRANSLATE("拒绝")
                    SetMeetingStoneApplicantActionButtonEnabled(row.widgets.declineButton, true)
                else
                    row.widgets.declineButton.applicantID = nil
                    row.widgets.declineButton.mkMeetingStoneTestRow = nil
                    row.widgets.declineButton.tooltip = nil
                    SetMeetingStoneApplicantActionButtonEnabled(row.widgets.declineButton, false)
                end
            end
            if row.widgets.actionStatus then
                row.widgets.actionStatus:SetText("")
                row.widgets.actionStatus:Hide()
            end
        end
        SetMeetingStoneApplicantRowInactiveState(row, applicant.isInactive == true)
        SetMeetingStoneApplicantRowHover(row, row.IsMouseOver and row:IsMouseOver())
        if row.IsMouseOver and row:IsMouseOver() then
            ShowMeetingStoneApplicantTooltip(row)
        end
        row:Show()
        PlayMeetingStoneApplicantRowFadeIn(row, table.concat({
            tostring(row.mkMeetingStoneApplicantGroupKey or ""),
            tostring(applicant.applicantID or ""),
            tostring(applicant.memberIndex or ""),
            tostring(applicant.shortName or ""),
        }, ":"), animateRows)
    end
    for index = #applicantRows + 1, #(widgets.meetingStoneApplicantRows or {}) do
        local row = widgets.meetingStoneApplicantRows[index]
        SetMeetingStoneApplicantRowHover(row, false)
        HideMeetingStoneApplicantTooltip(row)
        row.mkMeetingStoneApplicant = nil
        row.mkMeetingStoneTooltipAnchor = nil
        row.mkMeetingStoneTestRow = nil
        row.mkMeetingStoneApplicantGroupKey = nil
        row.mkMeetingStoneFadeKey = nil
        row.mkMeetingStoneApplicantContextMenuLocked = nil
        row.mkMeetingStoneApplicantContextMenuToken = nil
        row.applicantID = nil
        row.memberIndex = nil
        SetMeetingStoneApplicantRowBackground(row, nil, false)
        SetMeetingStoneApplicantAccountType(row, nil)
        SetMeetingStoneApplicantRoles(row, nil)
        if row.widgets.inviteButton then
            row.widgets.inviteButton.applicantID = nil
            row.widgets.inviteButton.numMembers = nil
            row.widgets.inviteButton.mkMeetingStoneTestRow = nil
            row.widgets.inviteButton.tooltip = nil
            SetMeetingStoneApplicantActionButtonEnabled(row.widgets.inviteButton, false)
        end
        if row.widgets.declineButton then
            row.widgets.declineButton.applicantID = nil
            row.widgets.declineButton.mkMeetingStoneTestRow = nil
            row.widgets.declineButton.tooltip = nil
            SetMeetingStoneApplicantActionButtonEnabled(row.widgets.declineButton, false)
        end
        if row.widgets.actionStatus then
            row.widgets.actionStatus:SetText("")
            row.widgets.actionStatus:Hide()
        end
        if row.widgets.inviteButtonHost then row.widgets.inviteButtonHost:Show() end
        if row.widgets.declineButtonHost then row.widgets.declineButtonHost:Show() end
        SetMeetingStoneApplicantRowInactiveState(row, false)
        row:Hide()
    end

    UpdateMeetingStoneApplicantScrollState(
        widgets,
        #applicantRows,
        ShouldResetMeetingStoneApplicantScroll(widgets, refreshSource, #applicantRows)
    )

    if widgets.meetingStoneApplicantEmpty then
        if #applicantRows == 0 then
            local emptyText, showEmptyAnimation = GetMeetingStoneApplicantEmptyState(owner, hasActiveEntry)
            if emptyText then
                widgets.meetingStoneApplicantEmpty:SetTextColor(
                    MEETINGSTONE_APPLICANT_EMPTY_TEXT_COLOR[1],
                    MEETINGSTONE_APPLICANT_EMPTY_TEXT_COLOR[2],
                    MEETINGSTONE_APPLICANT_EMPTY_TEXT_COLOR[3],
                    MEETINGSTONE_APPLICANT_EMPTY_TEXT_COLOR[4]
                )
                widgets.meetingStoneApplicantEmpty:SetText(emptyText)
                UpdateMeetingStoneApplicantEmptyBlockLayout(widgets, showEmptyAnimation == true)
                widgets.meetingStoneApplicantEmpty:Show()
                SetMeetingStoneApplicantEmptyAnimationShown(widgets, showEmptyAnimation == true)
            else
                UpdateMeetingStoneApplicantEmptyBlockLayout(widgets, false)
                widgets.meetingStoneApplicantEmpty:Hide()
                SetMeetingStoneApplicantEmptyAnimationShown(widgets, false)
            end
        else
            UpdateMeetingStoneApplicantEmptyBlockLayout(widgets, false)
            widgets.meetingStoneApplicantEmpty:Hide()
            SetMeetingStoneApplicantEmptyAnimationShown(widgets, false)
        end
    end

    owner:UpdateMeetingStoneEmptyApplicantEye(#applicantRows, visibleApplicantCount)
end
