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

local GROUP_ROLE_BY_ASSIGNED_ROLE = {
    TANK = "TANK",
    HEALER = "HEAL",
    DAMAGER = "DPS",
}

local GROUP_ROLE_KEYS = {
    TANK = true,
    HEAL = true,
    DPS = true,
}

local function NormalizeGroupRoleKey(role)
    return GROUP_ROLE_BY_ASSIGNED_ROLE[role] or (GROUP_ROLE_KEYS[role] and role) or nil
end

local function HasAnyGroupRole(roles)
    if type(roles) ~= "table" then
        return false
    end

    for roleKey in pairs(GROUP_ROLE_KEYS) do
        if roles[roleKey] == true then
            return true
        end
    end

    return false
end

local function SnapshotBool(value)
    return value and "1" or "0"
end

local function SnapshotValue(value)
    if value == nil then
        return "-"
    end

    return tostring(value)
end

local function SnapshotBestRuns(bestRuns)
    if type(bestRuns) ~= "table" or type(bestRuns.runs) ~= "table" then
        return ""
    end

    local parts = {
        SnapshotValue(bestRuns.score),
    }
    for _, run in ipairs(bestRuns.runs) do
        parts[#parts + 1] = table.concat({
            SnapshotValue(run.mapID),
            SnapshotValue(run.level),
            SnapshotValue(run.score),
            SnapshotValue(run.durationMS),
            SnapshotBool(run.timed),
        }, ":")
    end

    return table.concat(parts, ",")
end

local function GetGroupListKind(listKind)
    return listKind == "carpool" and "carpool" or "group"
end

local function GetGroupSortConfig(listKind)
    if GetGroupListKind(listKind) == "carpool" then
        return core.CARPOOL_SORT_DEFAULT or core.GROUP_SORT_DEFAULT,
            core.CARPOOL_SORT_INITIAL_DIRECTION or core.GROUP_SORT_INITIAL_DIRECTION,
            "carpoolSort"
    end

    return core.GROUP_SORT_DEFAULT, core.GROUP_SORT_INITIAL_DIRECTION, "groupSort"
end

local function GetGroupSortState(owner, listKind)
    local sortDefault, sortInitialDirection, stateKey = GetGroupSortConfig(listKind)
    if not owner[stateKey] then
        local savedSort = owner.db and owner.db.profile and owner.db.profile[stateKey]
        local savedKey = type(savedSort) == "table" and savedSort.key or nil
        local savedDirection = type(savedSort) == "table" and savedSort.direction or nil

        if not sortInitialDirection[savedKey] then
            savedKey = sortDefault.key
        end

        if savedDirection ~= "asc" and savedDirection ~= "desc" then
            savedDirection = sortDefault.direction
        end

        owner[stateKey] = {
            key = savedKey,
            direction = savedDirection,
        }
    end

    return owner[stateKey]
end

local function ResetGroupSortState(owner, listKind)
    if not owner then
        return
    end

    local sortDefault, _, stateKey = GetGroupSortConfig(listKind)
    owner[stateKey] = {
        key = sortDefault.key,
        direction = sortDefault.direction,
    }

    if owner.db and owner.db.profile then
        owner.db.profile[stateKey] = {
            key = sortDefault.key,
            direction = sortDefault.direction,
        }
    end

    local tabKey = GetGroupListKind(listKind)
    if owner.lastRenderedSnapshotByTab then
        owner.lastRenderedSnapshotByTab[tabKey] = nil
    end
end

function MKA:SetGroupSort(key, listKind)
    listKind = GetGroupListKind(listKind or self.activeTab)
    local _, sortInitialDirection, stateKey = GetGroupSortConfig(listKind)
    if not sortInitialDirection[key] then
        return
    end

    local sortState = GetGroupSortState(self, listKind)
    if sortState.key == key then
        sortState.direction = sortState.direction == "asc" and "desc" or "asc"
    else
        sortState.key = key
        sortState.direction = sortInitialDirection[key] or "asc"
    end

    if self.db and self.db.profile then
        self.db.profile[stateKey] = {
            key = sortState.key,
            direction = sortState.direction,
        }
    end

    if self.widgets and self.widgets.groupHeaderFrame and core.UpdateGroupHeaderSortIndicators then
        core.UpdateGroupHeaderSortIndicators(self, self.widgets.groupHeaderFrame)
    end

    if self.IsMainWindowShown and self:IsMainWindowShown() then
        self:RefreshMainWindow("SetGroupSort", string.format("%s:%s", tostring(sortState.key), tostring(sortState.direction)))
    end
end

core.GetGroupListKind = GetGroupListKind
core.GetGroupSortConfig = GetGroupSortConfig
core.GetGroupSortState = GetGroupSortState
core.ResetGroupSortState = ResetGroupSortState

local function NormalizeCharacterIdentifier(value)
    if type(value) ~= "string" then
        return nil
    end

    local normalized = value:match("^%s*(.-)%s*$")
    if not normalized or normalized == "" then
        return nil
    end

    return normalized:lower()
end

local function GetCharacterIdentifierAliases(value)
    local aliases = {}
    local normalized = NormalizeCharacterIdentifier(value)
    if not normalized then
        return aliases
    end

    aliases[normalized] = true

    local plainName = Ambiguate and Ambiguate(value, "none") or value:match("^[^-]+") or value
    local normalizedPlainName = NormalizeCharacterIdentifier(plainName)
    if normalizedPlainName then
        aliases[normalizedPlainName] = true
    end

    return aliases
end

local function GetGroupRoleKeyForUnit(unit)
    if type(unit) ~= "string" or unit == "" or not UnitGroupRolesAssigned then
        return nil
    end

    return NormalizeGroupRoleKey(UnitGroupRolesAssigned(unit))
end

local function BuildGroupRoleTable(roleKey)
    local roles = {}
    if roleKey then
        roles[roleKey] = true
    end
    return roles
end

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

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

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

    return false
end

local function GetGroupRosterMembers()
    local members = {}

    if not MKA:IsInGroupForComm() then
        return members
    end

    local groupPrefix = IsInRaid() and "raid" or "party"
    local memberCount = GetNumGroupMembers()

    if not IsInRaid() and IsInGroup() then
        memberCount = math.max(memberCount - 1, 0)
    end

    for index = 1, memberCount do
        local unit = groupPrefix .. index
        if UnitExists(unit) then
            local fullName = GetUnitName and GetUnitName(unit, true) or nil
            local playerFullName = GetUnitName and GetUnitName("player", true) or UnitName("player")
            if fullName and fullName ~= playerFullName then
                local shortName, realm = strsplit("-", fullName)
                members[#members + 1] = {
                    unit = unit,
                    fullName = fullName,
                    name = shortName or fullName,
                    realm = realm,
                    class = select(2, UnitClass(unit)),
                    connected = not UnitIsConnected or UnitIsConnected(unit),
                    roleKey = GetGroupRoleKeyForUnit(unit),
                }
            end
        end
    end

    return members
end

local function FindGroupRosterMemberForEntry(characterEntry)
    for _, member in ipairs(GetGroupRosterMembers()) do
        if IdentifierMatchesCharacterEntry(member.fullName, characterEntry)
            or IdentifierMatchesCharacterEntry(member.name, characterEntry)
        then
            return member
        end
    end

    return nil
end

core.GetCharacterIdentifierAliases = GetCharacterIdentifierAliases
core.IdentifierMatchesCharacterEntry = IdentifierMatchesCharacterEntry
core.GetGroupRosterMembers = GetGroupRosterMembers
core.FindGroupRosterMemberForEntry = FindGroupRosterMemberForEntry

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

    return TRANSLATE("WARBAND.LABEL")
end

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

    return name
end

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

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

local function FormatWarbandGroupCharacterName(owner, character)
    local characterName = (character and character.name) or TRANSLATE("TAB.CHARACTERS")
    local ownerName = GetShortNameFromCharacterKey(owner) or GetCurrentPlayerShortName()
    local suffix = string.format(TRANSLATE("WARBAND.OWNER_FORMAT"), ownerName)
    local classColor = GetClassColorString(character and character.class)
    return string.format("%s-%s", ColorText(characterName, classColor), ColorText(suffix, "ff58d7ff"))
end

local function GetWarbandSourceDisplayName(characterEntry)
    local sourceName = characterEntry and characterEntry.warbandSourceName
    if type(sourceName) == "string" and sourceName ~= "" then
        return sourceName
    end

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

core.GetCurrentPlayerShortName = GetCurrentPlayerShortName
core.GetCurrentPlayerOwnerKey = GetCurrentPlayerOwnerKey
core.GetShortNameFromCharacterKey = GetShortNameFromCharacterKey
core.GetWarbandSourceDisplayName = GetWarbandSourceDisplayName

local function NormalizeGroupSortText(value)
    return string.lower(tostring(value or ""))
end

local function GetGroupEntryRoleSortText(owner, entry)
    local roles = owner.GetCharacterEntryDisplayRoles and owner:GetCharacterEntryDisplayRoles(entry)
        or owner:NormalizeRoles(entry and entry.data or {})
    local values = {}
    for _, roleKey in ipairs(owner:GetRoleOrder()) do
        if roles[roleKey] then
            values[#values + 1] = roleKey
        end
    end
    return table.concat(values, ",")
end

local function GetGroupEntrySortValue(owner, entry, sortKey, activeGroupAliases)
    local character = entry and entry.data or {}

    if sortKey == "presence" then
        if owner:IsCharacterEntryOfflineInGroup(entry) then
            return 1
        end

        return owner:IsCharacterEntryCurrentlyInGroup(entry, activeGroupAliases) and 2 or 0
    end

    if sortKey == "name" then
        return NormalizeGroupSortText(character.name or entry.key)
    end

    if sortKey == "armor" then
        return NormalizeGroupSortText(owner:GetArmorType(character))
    end

    if sortKey == "key" then
        return NormalizeGroupSortText(owner:GetLocalizedDungeonName(character.mapID, character.dungeonName))
    end

    if sortKey == "rating" then
        return tonumber(character.rating) or 0
    end

    if sortKey == "roles" then
        return NormalizeGroupSortText(GetGroupEntryRoleSortText(owner, entry))
    end

    return NormalizeGroupSortText(character.name or entry.key)
end

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

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

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

    return false
end

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

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

local function SortGroupViewCharacters(owner, entries, activeGroupAliases)
    local sortState = GetGroupSortState(owner)
    local sortKey = sortState.key or core.GROUP_SORT_DEFAULT.key
    local sortDirection = sortState.direction or core.GROUP_SORT_DEFAULT.direction
    local pinCurrentPlayerFirst = sortKey == core.GROUP_SORT_DEFAULT.key
        and sortDirection == core.GROUP_SORT_DEFAULT.direction

    table.sort(entries, function(left, right)
        if pinCurrentPlayerFirst then
            local leftIsCurrent = IsCurrentPlayerGroupEntry(left)
            local rightIsCurrent = IsCurrentPlayerGroupEntry(right)
            if leftIsCurrent ~= rightIsCurrent then
                return leftIsCurrent
            end
        end

        local leftValue = GetGroupEntrySortValue(owner, left, sortKey, activeGroupAliases)
        local rightValue = GetGroupEntrySortValue(owner, right, sortKey, activeGroupAliases)

        if leftValue ~= rightValue then
            if sortDirection == "desc" then
                return leftValue > rightValue
            end

            return leftValue < rightValue
        end

        if sortKey == "key" then
            local leftLevel = (left.data and tonumber(left.data.keyLevel)) or 0
            local rightLevel = (right.data and tonumber(right.data.keyLevel)) or 0
            if leftLevel ~= rightLevel then
                return leftLevel > rightLevel
            end
        end

        local leftName = NormalizeGroupSortText(left.data and left.data.name or left.key)
        local rightName = NormalizeGroupSortText(right.data and right.data.name or right.key)
        if leftName == rightName then
            return NormalizeGroupSortText(left.owner) < NormalizeGroupSortText(right.owner)
        end

        return leftName < rightName
    end)
end

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

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

    return false
end

local function IsCurrentPlayerCarpoolSource(characterEntry, currentOwnerKey, currentOwnerName)
    return IdentifiersCanMatch(characterEntry and characterEntry.owner, currentOwnerKey)
        or IdentifiersCanMatch(characterEntry and characterEntry.warbandSourceName, currentOwnerName)
end

local function GetCarpoolSourceSortValues(characterEntry, currentOwnerKey, currentOwnerName)
    local sourceName = GetWarbandSourceDisplayName(characterEntry)
    local sourceKey = NormalizeGroupSortText(characterEntry and characterEntry.owner or sourceName)
    local sourceNameKey = NormalizeGroupSortText(sourceName)
    local priority = IsCurrentPlayerCarpoolSource(characterEntry, currentOwnerKey, currentOwnerName) and 0 or 1

    return priority, sourceNameKey, sourceKey
end

local function CompareGroupSortValues(leftValue, rightValue, sortDirection)
    if leftValue == rightValue then
        return nil
    end

    if sortDirection == "desc" then
        return leftValue > rightValue
    end

    return leftValue < rightValue
end

local function GetCarpoolEntryRoleSortText(owner, entry)
    local roles = owner.GetCharacterEntryDisplayRoles and owner:GetCharacterEntryDisplayRoles(entry)
        or owner:NormalizeRoles(entry and entry.data or {})
    local values = {}
    for index, roleKey in ipairs(owner:GetRoleOrder()) do
        if roles[roleKey] then
            values[#values + 1] = string.format("%02d:%s", index, roleKey)
        end
    end
    return table.concat(values, ",")
end

local function GetCarpoolEntrySortValue(owner, entry, sortKey, activeGroupAliases)
    local character = entry and entry.data or {}

    if sortKey == "key" then
        return tonumber(character.keyLevel) or 0
    end

    if sortKey == "roles" then
        return NormalizeGroupSortText(GetCarpoolEntryRoleSortText(owner, entry))
    end

    return GetGroupEntrySortValue(owner, entry, sortKey, activeGroupAliases)
end

local function SortCarpoolViewCharacters(owner, entries, currentOwnerKey, currentOwnerName, activeGroupAliases)
    local sortState = GetGroupSortState(owner, "carpool")
    local sortKey = sortState.key or core.CARPOOL_SORT_DEFAULT.key
    local sortDirection = sortState.direction or core.CARPOOL_SORT_DEFAULT.direction
    local sortBySource = sortKey == "meetingStone"

    table.sort(entries, function(left, right)
        local leftPriority, leftSourceName, leftSourceKey = GetCarpoolSourceSortValues(left, currentOwnerKey, currentOwnerName)
        local rightPriority, rightSourceName, rightSourceKey = GetCarpoolSourceSortValues(right, currentOwnerKey, currentOwnerName)

        if sortBySource then
            if leftPriority ~= rightPriority then
                return leftPriority < rightPriority
            end

            local sourceNameCompare = CompareGroupSortValues(leftSourceName, rightSourceName, sortDirection)
            if sourceNameCompare ~= nil then
                return sourceNameCompare
            end

            local sourceKeyCompare = CompareGroupSortValues(leftSourceKey, rightSourceKey, sortDirection)
            if sourceKeyCompare ~= nil then
                return sourceKeyCompare
            end
        else
            local leftValue = GetCarpoolEntrySortValue(owner, left, sortKey, activeGroupAliases)
            local rightValue = GetCarpoolEntrySortValue(owner, right, sortKey, activeGroupAliases)
            local valueCompare = CompareGroupSortValues(leftValue, rightValue, sortDirection)
            if valueCompare ~= nil then
                return valueCompare
            end

            if sortKey == "key" then
                local leftDungeonName = NormalizeGroupSortText(owner:GetLocalizedDungeonName(left and left.data and left.data.mapID, left and left.data and left.data.dungeonName))
                local rightDungeonName = NormalizeGroupSortText(owner:GetLocalizedDungeonName(right and right.data and right.data.mapID, right and right.data and right.data.dungeonName))
                if leftDungeonName ~= rightDungeonName then
                    return leftDungeonName < rightDungeonName
                end
            end
        end

        local leftName = NormalizeGroupSortText(left and left.data and left.data.name or left and left.key)
        local rightName = NormalizeGroupSortText(right and right.data and right.data.name or right and right.key)
        if leftName ~= rightName then
            return leftName < rightName
        end

        if leftSourceName ~= rightSourceName then
            return leftSourceName < rightSourceName
        end

        if leftSourceKey ~= rightSourceKey then
            return leftSourceKey < rightSourceKey
        end

        return NormalizeGroupSortText(left and left.key) < NormalizeGroupSortText(right and right.key)
    end)
end

function MKA:GetSharedGroupViewCharacters()
    ---@type BKCharacterEntry[]
    local entries = {}
    local activeGroupAliases = self:GetActiveGroupCharacterAliases()
    local currentPlayerEntryAdded = false

    local function addEntry(characterEntry)
        if IsCurrentPlayerGroupEntry(characterEntry) then
            currentPlayerEntryAdded = true
        end
        entries[#entries + 1] = characterEntry
    end

    if self.GetCharacters then
        for _, characterEntry in ipairs(self:GetCharacters(true)) do
            local isCurrentPlayerEntry = IsCurrentPlayerGroupEntry(characterEntry)
            if self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
                and (self:IsCharacterShownInGroup(characterEntry.data) or isCurrentPlayerEntry)
            then
                characterEntry.warbandTag = self.db.profile.warbandTag or ""
                characterEntry.owner = characterEntry.owner or GetCurrentPlayerOwnerKey()
                addEntry(characterEntry)
            end
        end
    end

    if not currentPlayerEntryAdded and self.db and self.db.global and self.db.global.characters then
        local currentOwnerKey = GetCurrentPlayerOwnerKey()
        local currentCharacter = self.db.global.characters[currentOwnerKey]
        if currentCharacter then
            addEntry({
                key = currentOwnerKey,
                owner = currentOwnerKey,
                warbandTag = self.db.profile.warbandTag or "",
                data = currentCharacter,
            })
        end
    end

    if self.GetGroupCharacters then
        for _, characterEntry in ipairs(self:GetGroupCharacters()) do
            if self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases) then
                if not (currentPlayerEntryAdded and IsCurrentPlayerGroupEntry(characterEntry)) then
                    addEntry(characterEntry)
                end
            end
        end
    end

    if self.IsInGroupForComm and self:IsInGroupForComm() then
        for _, member in ipairs(GetGroupRosterMembers()) do
            local alreadyShown = false
            for _, characterEntry in ipairs(entries) do
                if IdentifierMatchesCharacterEntry(member.fullName, characterEntry)
                    or IdentifierMatchesCharacterEntry(member.name, characterEntry)
                then
                    alreadyShown = true
                    break
                end
            end

            if not alreadyShown then
                local isOffline = member.connected == false
                entries[#entries + 1] = {
                    key = member.fullName,
                    owner = member.fullName,
                    source = "roster",
                    warbandTag = "",
                    isRosterOnly = true,
                    isOffline = isOffline or nil,
                    data = {
                        name = member.fullName,
                        class = member.class,
                        realm = member.realm,
                        hasKeystone = false,
                        showInGroup = true,
                        hide = false,
                        roles = BuildGroupRoleTable(member.roleKey),
                        source = "roster",
                        isRosterOnly = true,
                        isOffline = isOffline or nil,
                    },
                }
            end
        end
    end

    SortGroupViewCharacters(self, entries, activeGroupAliases)

    return entries
end

function MKA:GetCarpoolViewCharacters()
    ---@type BKCharacterEntry[]
    local entries = {}
    local activeGroupAliases = self:GetActiveGroupCharacterAliases()
    local currentOwnerKey = GetCurrentPlayerOwnerKey()
    local currentOwnerName = GetCurrentPlayerShortName()

    if self.GetCharacters then
        for _, characterEntry in ipairs(self:GetCharacters(true)) do
            if self:IsCharacterShownInGroup(characterEntry.data)
                and not self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
            then
                characterEntry.owner = characterEntry.owner or currentOwnerKey
                characterEntry.warbandSourceName = currentOwnerName
                characterEntry.warbandTag = self.db and self.db.profile and self.db.profile.warbandTag or ""
                entries[#entries + 1] = characterEntry
            end
        end
    end

    if self.GetGroupCharacters then
        for _, characterEntry in ipairs(self:GetGroupCharacters()) do
            if not self:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
                and not (characterEntry.isRosterOnly or characterEntry.data and characterEntry.data.isRosterOnly)
            then
                characterEntry.warbandSourceName = GetShortNameFromCharacterKey(characterEntry.owner)
                entries[#entries + 1] = characterEntry
            end
        end
    end

    SortCarpoolViewCharacters(self, entries, currentOwnerKey, currentOwnerName, activeGroupAliases)
    return entries
end

function MKA:GetActiveGroupCharacterAliases()
    local aliases = {}

    local function addAliases(value)
        for alias in pairs(GetCharacterIdentifierAliases(value)) do
            aliases[alias] = true
        end
    end

    addAliases(UnitName("player"))
    addAliases(GetUnitName and GetUnitName("player", true))

    if not self:IsInGroupForComm() then
        return aliases
    end

    local groupPrefix = IsInRaid() and "raid" or "party"
    local memberCount = GetNumGroupMembers()

    if not IsInRaid() and IsInGroup() then
        memberCount = math.max(memberCount - 1, 0)
    end

    for index = 1, memberCount do
        local unit = groupPrefix .. index
        if UnitExists(unit) then
            addAliases(UnitName(unit))
            addAliases(GetUnitName and GetUnitName(unit, true))
        end
    end

    return aliases
end

function MKA:IsCharacterEntryCurrentlyInGroup(characterEntry, activeAliases)
    local character = characterEntry and characterEntry.data or nil
    if not character then
        return false
    end

    local aliases = activeAliases or self:GetActiveGroupCharacterAliases()
    for alias in pairs(GetCharacterIdentifierAliases(character.name or characterEntry.key)) do
        if aliases[alias] then
            return true
        end
    end

    return false
end

function MKA:GetCharacterEntryGroupAssignedRoleKey(characterEntry)
    if IsCurrentPlayerGroupEntry(characterEntry) then
        return GetGroupRoleKeyForUnit("player")
    end

    local member = FindGroupRosterMemberForEntry(characterEntry)
    return member and member.roleKey or nil
end

function MKA:GetCharacterEntryDisplayRoles(characterEntry)
    local assignedRoleKey = self:GetCharacterEntryGroupAssignedRoleKey(characterEntry)
    if assignedRoleKey then
        return BuildGroupRoleTable(assignedRoleKey), "roster"
    end

    local applicantRating = self.GetMeetingStoneApplicantRating and self:GetMeetingStoneApplicantRating(characterEntry)
    local applicantRoleKey = NormalizeGroupRoleKey(applicantRating and applicantRating.role)
    if applicantRoleKey then
        return BuildGroupRoleTable(applicantRoleKey), "meetingstone_applicant"
    end
    if HasAnyGroupRole(applicantRating and applicantRating.roles) then
        return applicantRating.roles, "meetingstone_applicant"
    end

    return self:NormalizeRoles(characterEntry and characterEntry.data or nil), "stored"
end

function MKA:GetCharacterEntryGroupRosterMember(characterEntry)
    return FindGroupRosterMemberForEntry(characterEntry)
end

function MKA:IsCharacterEntryOfflineInGroup(characterEntry)
    if characterEntry and characterEntry.isOffline == true then
        return true
    end

    local character = characterEntry and characterEntry.data or nil
    if character and character.isRosterOnly == true and character.isOffline == true then
        return true
    end

    local member = self:GetCharacterEntryGroupRosterMember(characterEntry)
    return member and member.connected == false or false
end

local function BuildGroupListTabSnapshot(owner, entries, listKind)
    local parts = {}
    local activeGroupAliases = owner:GetActiveGroupCharacterAliases()
    local sortDefault = listKind == "carpool" and core.CARPOOL_SORT_DEFAULT or core.GROUP_SORT_DEFAULT
    local sortState = GetGroupSortState(owner, listKind)
    local activeSortKey = sortState.key or sortDefault.key
    local activeSortDirection = sortState.direction or sortDefault.direction

    parts[#parts + 1] = table.concat({
        SnapshotValue(activeSortKey),
        SnapshotValue(activeSortDirection),
        SnapshotBool(IsGroupLeader and IsGroupLeader()),
        SnapshotBool(not owner.IsMeetingStoneManagementAvailable or owner:IsMeetingStoneManagementAvailable()),
    }, "~")

    for _, characterEntry in ipairs(entries) do
        local character = characterEntry.data or {}
        local roles = owner.GetCharacterEntryDisplayRoles and owner:GetCharacterEntryDisplayRoles(characterEntry)
            or owner:NormalizeRoles(character)
        local warbandTag = characterEntry.warbandTag or ""
        local isCurrentGroupEntry = owner:IsCharacterEntryCurrentlyInGroup(characterEntry, activeGroupAliases)
        local isOffline = owner:IsCharacterEntryOfflineInGroup(characterEntry)
        local isRosterOnly = characterEntry.isRosterOnly or character.isRosterOnly
        local displayRating = owner.GetGroupRatingDisplayScore and owner:GetGroupRatingDisplayScore(characterEntry) or character.rating
        local ratingLookupState = owner.GetGroupRatingLookupState
            and owner:GetGroupRatingLookupState(characterEntry, {
                isOffline = isOffline,
                isRosterOnly = isRosterOnly,
            })
            or ""

        parts[#parts + 1] = table.concat({
            SnapshotValue(characterEntry.key),
            SnapshotValue(characterEntry.owner),
            SnapshotValue(warbandTag),
            SnapshotValue(character.name),
            SnapshotValue(character.class),
            SnapshotValue(character.keyLevel),
            SnapshotValue(character.mapID),
            SnapshotValue(character.dungeonName),
            SnapshotValue(character.keystoneLink),
            SnapshotBool(character.hasKeystone),
            SnapshotValue(character.rating),
            SnapshotValue(displayRating),
            SnapshotValue(ratingLookupState),
            SnapshotBool(owner:IsCharacterShownInGroup(character)),
            SnapshotBool(roles.TANK),
            SnapshotBool(roles.HEAL),
            SnapshotBool(roles.DPS),
            SnapshotValue(SnapshotBestRuns(character.bestRuns)),
            SnapshotBool(isCurrentGroupEntry),
            SnapshotBool(isOffline),
            SnapshotBool(isRosterOnly),
            SnapshotValue(characterEntry.warbandSourceName),
        }, "~")
    end

    return table.concat(parts, "||")
end

function MKA:BuildGroupTabSnapshot()
    return BuildGroupListTabSnapshot(self, self:GetSharedGroupViewCharacters(), "group")
end

function MKA:BuildCarpoolTabSnapshot()
    return BuildGroupListTabSnapshot(self, self:GetCarpoolViewCharacters(), "carpool")
end


core.GroupListData = {
    GetListKind = GetGroupListKind,
    GetSortState = GetGroupSortState,
    ResetSortState = ResetGroupSortState,
    GetWarbandSourceDisplayName = GetWarbandSourceDisplayName,
}
