local _, addonTable = ...
---@type MKA
local MKA = addonTable.MKA
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end
local MeetingStoneConstants = core.MeetingStoneConstants or {}
local MEETINGSTONE_FALLBACK_CHALLENGE_MAP_IDS = MeetingStoneConstants.MEETINGSTONE_FALLBACK_CHALLENGE_MAP_IDS or {}
local MEETINGSTONE_FALLBACK_DUNGEON_GROUP_IDS = MeetingStoneConstants.MEETINGSTONE_FALLBACK_DUNGEON_GROUP_IDS or {}

core.FindGroupSearch = core.FindGroupSearch or {}
local FindGroupSearch = core.FindGroupSearch

local FIND_GROUP_CATEGORY_ID = 2
local FIND_GROUP_MYTHIC_PLUS_FILTER = 1
local FIND_GROUP_MAX_MEMBERS = 5
local FIND_GROUP_MAX_AVAILABLE_SLOTS = 4
local FIND_GROUP_AUTO_ROLE_CHECK_SECONDS = 10
local FIND_GROUP_RESULT_BATCH_SIZE = 18
local FIND_GROUP_DETAIL_RESULT_BATCH_SIZE = 2
local FIND_GROUP_RESULT_REFRESH_INTERVAL = 0.1
local FIND_GROUP_ACCOUNT_QUERY_BATCH_SIZE = 6
local FIND_GROUP_ACCOUNT_QUERY_DELAY = 0.12
local FIND_GROUP_NEWBIE_RECHECK_BATCH_SIZE = 12
local FIND_GROUP_NEWBIE_RECHECK_DELAY = 1
local FIND_GROUP_NEWBIE_RECHECK_LIMIT = 10
local FIND_GROUP_VISIBLE_DETAIL_BATCH_SIZE = 2
local FIND_GROUP_VISIBLE_DETAIL_DELAY = 0.05
local FIND_GROUP_INITIAL_DISPLAY_ROWS = 10
local FIND_GROUP_INITIAL_NEWBIE_WAIT_SECONDS = 1
local FIND_GROUP_APPLICATION_TIMEOUT_SECONDS = 5 * 60
local FIND_GROUP_APPLICATION_TICK_SECONDS = 1
local FIND_GROUP_CANCELLED_HIDE_DELAY = 3
local FIND_GROUP_PENDING_REPORT_TIMEOUT_SECONDS = 2 * 60
local FIND_GROUP_REPORT_BLOCK_CACHE_MAX_ENTRIES = 300

local FIND_GROUP_DEFAULT_SETTINGS = {
    selectedActivityIDs = {},
    haveHealer = false,
    haveTank = false,
    noHealer = false,
    noTank = false,
    noDps = false,
    minAvailableSlots = 1,
    minScore = 0,
    searchNewbie = false,
    quickJoin = false,
    autoJoin = false,
    sortKey = "",
    sortDirection = "desc",
    reportBlockedResults = {},
}

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

local FIND_GROUP_DIRECTIONAL_SORT_KEYS = {
    requirement = true,
    score = true,
}

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

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

local FIND_GROUP_RELATIONSHIP_PRIORITY = {
    bnet = 0,
    friend = 1,
    guild = 2,
}

local FIND_GROUP_ACTIVE_APPLICATION_STATUS = {
    applied = true,
    invited = true,
    inviteaccepted = true,
}

local FIND_GROUP_INACTIVE_APPLICATION_STATUS = {
    cancelled = true,
    declined = true,
    failed = true,
    invitedeclined = true,
    timedout = true,
}

local GetProfile
local ScheduleApplicationCountdown
local ConfirmPendingGroupReport

local function NormalizeFindGroupMinAvailableSlots(value)
    return math.max(1, math.min(FIND_GROUP_MAX_AVAILABLE_SLOTS, math.floor(tonumber(value) or 1)))
end

local function CopyDefaults(target)
    target = type(target) == "table" and target or {}
    for key, value in pairs(FIND_GROUP_DEFAULT_SETTINGS) do
        if type(value) == "table" then
            if type(target[key]) ~= "table" then
                target[key] = {}
            end
        elseif target[key] == nil then
            target[key] = value
        end
    end
    target.minAvailableSlots = NormalizeFindGroupMinAvailableSlots(target.minAvailableSlots)
    return target
end

local function GetNow()
    return GetTime and GetTime() or time()
end

local function NormalizeReportCacheText(value)
    value = tostring(value or "")
    value = value:gsub("^%s+", ""):gsub("%s+$", "")
    value = value:gsub("%s+", " ")
    return value:lower()
end

local function BuildReportCacheKey(title, leaderName, activityID, comment)
    title = NormalizeReportCacheText(title)
    leaderName = NormalizeReportCacheText(leaderName)
    activityID = tostring(tonumber(activityID) or 0)
    comment = NormalizeReportCacheText(comment)
    if title == "" and leaderName == "" and comment == "" then
        return nil
    end
    return table.concat({ activityID, leaderName, title, comment }, "\031")
end

local function BuildReportRecordFromResult(result, reportType)
    if type(result) ~= "table" then
        return nil
    end
    local cacheKey = result.reportCacheKey or BuildReportCacheKey(result.title, result.leaderName, result.activityID, result.comment)
    if not cacheKey then
        return nil
    end
    return {
        resultID = tonumber(result.resultID),
        reportType = reportType,
        reportCacheKey = cacheKey,
        title = result.title,
        leaderName = result.leaderName,
        activityID = tonumber(result.activityID) or 0,
        dungeonName = result.dungeonName,
        comment = result.comment,
        blockedAt = GetNow(),
    }
end

local function PruneReportBlockedResults(blockedResults)
    if type(blockedResults) ~= "table" then
        return
    end

    local entries = {}
    for cacheKey, record in pairs(blockedResults) do
        entries[#entries + 1] = {
            cacheKey = cacheKey,
            blockedAt = type(record) == "table" and tonumber(record.blockedAt) or 0,
        }
    end
    if #entries <= FIND_GROUP_REPORT_BLOCK_CACHE_MAX_ENTRIES then
        return
    end

    table.sort(entries, function(left, right)
        return (left.blockedAt or 0) < (right.blockedAt or 0)
    end)
    for index = 1, #entries - FIND_GROUP_REPORT_BLOCK_CACHE_MAX_ENTRIES do
        blockedResults[entries[index].cacheKey] = nil
    end
end

local function SafeList(value)
    return type(value) == "table" and value or {}
end

local function NormalizeResultList(state)
    local results = state and state.results
    if type(results) ~= "table" then
        if state then
            state.results = {}
            return state.results
        end
        return {}
    end

    local indexes = {}
    local maxIndex = 0
    for index, result in pairs(results) do
        if type(index) == "number" and index > 0 and index == math.floor(index) then
            maxIndex = math.max(maxIndex, index)
            if result ~= nil then
                indexes[#indexes + 1] = index
            end
        end
    end

    if maxIndex == #indexes then
        return results
    end

    table.sort(indexes)
    local compacted = {}
    for _, index in ipairs(indexes) do
        compacted[#compacted + 1] = results[index]
    end
    for index = 1, maxIndex do
        results[index] = nil
    end
    for index, result in ipairs(compacted) do
        results[index] = result
    end
    return results
end

local function ScheduleLater(delay, callback)
    if C_Timer and C_Timer.After then
        C_Timer.After(delay or 0, callback)
        return
    end
    callback()
end

local function IsFindGroupWindowActive(owner)
    if not owner or owner.activeTab ~= "findgroup" then
        return false
    end
    if owner.IsMainWindowShown and not owner:IsMainWindowShown() then
        return false
    end
    return true
end

local function RefreshFindGroupWindow(owner, refreshSource, refreshDetail)
    if IsFindGroupWindowActive(owner) and owner.RefreshMainWindow then
        owner:RefreshMainWindow(refreshSource, refreshDetail)
    end
end

local function GetState(owner)
    owner.mkaFindGroupState = owner.mkaFindGroupState or {
        results = {},
        resultByID = {},
        blockedTitles = {},
        version = 0,
    }
    return owner.mkaFindGroupState
end

local function GetReportBlockedResults(owner)
    local settings = GetProfile and GetProfile(owner) or nil
    if not settings then
        return nil
    end
    settings.reportBlockedResults = type(settings.reportBlockedResults) == "table" and settings.reportBlockedResults or {}
    PruneReportBlockedResults(settings.reportBlockedResults)
    return settings.reportBlockedResults
end

local function IsReportBlocked(owner, cacheKey)
    local blockedResults = cacheKey and GetReportBlockedResults(owner)
    return type(blockedResults) == "table" and blockedResults[cacheKey] ~= nil
end

local function StoreReportBlockedResult(owner, record)
    if not (record and record.reportCacheKey) then
        return false
    end
    local blockedResults = GetReportBlockedResults(owner)
    if not blockedResults then
        return false
    end
    blockedResults[record.reportCacheKey] = {
        reportType = record.reportType,
        title = record.title,
        leaderName = record.leaderName,
        activityID = record.activityID,
        dungeonName = record.dungeonName,
        comment = record.comment,
        blockedAt = record.blockedAt or GetNow(),
    }
    PruneReportBlockedResults(blockedResults)
    return true
end

local function CancelSearchProcessing(state)
    if not state then
        return
    end
    state.processingToken = (tonumber(state.processingToken) or 0) + 1
    state.processing = false
    state.resultQueue = nil
    state.resultQueueIndex = nil
    state.activityMap = nil
    state.selectedMap = nil
    state.hasSelection = nil
    state.accountQueryQueue = nil
    state.accountQuerySet = nil
    state.accountQueryIndex = nil
    state.accountQueryScheduled = false
    state.newbiePendingResults = nil
    state.newbiePendingByID = nil
    state.newbieSearching = false
    state.newbieRecheckScheduled = false
    state.searchCompleted = false
    state.searchCompletedEmpty = false
    state.detailQueue = nil
    state.detailQueueSet = nil
    state.detailQueueScheduled = false
    state.lastResultBatchRefreshAt = nil
end

local function GetRequirementValue(result)
    return tonumber(result and result.requiredItemLevel) or 0
end

local function GetMemberCountValue(result)
    return tonumber(result and result.totalMembers) or 0
end

local function GetRelationshipPriority(result)
    return FIND_GROUP_RELATIONSHIP_PRIORITY[result and result.relationship] or 3
end

local function HasPendingApplicationStatus(pendingStatus)
    return pendingStatus ~= nil and pendingStatus ~= false and pendingStatus ~= "" and pendingStatus ~= "none"
end

local function IsActiveApplicationStatus(status, pendingStatus)
    if HasPendingApplicationStatus(pendingStatus) then
        return true
    end
    return FIND_GROUP_ACTIVE_APPLICATION_STATUS[status] == true
end

local function IsInactiveApplicationStatus(status)
    return FIND_GROUP_INACTIVE_APPLICATION_STATUS[status] == true
end

local function IsMeaningfulApplicationStatus(status, pendingStatus)
    return IsActiveApplicationStatus(status, pendingStatus) or IsInactiveApplicationStatus(status)
end

local function GetApplicationAppliedAt(record, now, duration)
    if record and tonumber(record.appliedAt) then
        return tonumber(record.appliedAt)
    end
    duration = tonumber(duration)
    if duration and duration > 0 and duration < FIND_GROUP_APPLICATION_TIMEOUT_SECONDS then
        return (now or GetNow()) - duration
    end
    return now or GetNow()
end

local function GetApplicationExpiresAt(appliedAt)
    appliedAt = tonumber(appliedAt)
    return appliedAt and (appliedAt + FIND_GROUP_APPLICATION_TIMEOUT_SECONDS) or nil
end

local function GetApplicationPriority(result)
    if IsActiveApplicationStatus(result and result.applicationStatus, result and result.pendingApplicationStatus) then
        return 0
    end
    if result and result.applicationStatus == "declined" then
        return 1
    end
    return 2
end

local function IsFrozenResult(result)
    return result and (result.applicationStatus == "cancelled" or result.reportRemovalPending == true)
end

local function CompareApplicationPriority(left, right)
    local leftPriority = GetApplicationPriority(left)
    local rightPriority = GetApplicationPriority(right)
    if leftPriority ~= rightPriority then
        return leftPriority < rightPriority
    end
    return nil
end

local function CompareDefault(left, right)
    local applicationComparison = CompareApplicationPriority(left, right)
    if applicationComparison ~= nil then
        return applicationComparison
    end
    local leftRelationshipPriority = GetRelationshipPriority(left)
    local rightRelationshipPriority = GetRelationshipPriority(right)
    if leftRelationshipPriority ~= rightRelationshipPriority then
        return leftRelationshipPriority < rightRelationshipPriority
    end
    if (tonumber(left.score) or 0) ~= (tonumber(right.score) or 0) then
        return (tonumber(left.score) or 0) > (tonumber(right.score) or 0)
    end
    return (tonumber(left.resultID) or 0) > (tonumber(right.resultID) or 0)
end

local function CompareNumberValue(leftValue, rightValue, direction)
    leftValue = tonumber(leftValue) or 0
    rightValue = tonumber(rightValue) or 0
    if leftValue == rightValue then
        return nil
    end
    if direction == "asc" then
        return leftValue < rightValue
    end
    return leftValue > rightValue
end

local function SortResults(owner)
    local state = GetState(owner)
    local results = NormalizeResultList(state)
    if type(results) ~= "table" or #results <= 1 then
        return
    end

    local hasFrozenResult = false
    for _, result in ipairs(results) do
        if IsFrozenResult(result) then
            hasFrozenResult = true
            break
        end
    end
    local frozenByIndex
    local sortTarget = results
    if hasFrozenResult then
        frozenByIndex = {}
        sortTarget = {}
        for index, result in ipairs(results) do
            if IsFrozenResult(result) then
                frozenByIndex[index] = result
            else
                sortTarget[#sortTarget + 1] = result
            end
        end
    end

    local function restoreFrozenResults()
        if not hasFrozenResult then
            return
        end
        local sortedIndex = 1
        for index = 1, #results do
            if frozenByIndex[index] then
                results[index] = frozenByIndex[index]
            else
                results[index] = sortTarget[sortedIndex]
                sortedIndex = sortedIndex + 1
            end
        end
    end

    local settings = GetProfile(owner) or {}
    local sortKey = settings.sortKey
    local sortDirection = settings.sortDirection == "asc" and "asc" or "desc"
    if not FIND_GROUP_SORTABLE_KEYS[sortKey] then
        table.sort(sortTarget, CompareDefault)
        restoreFrozenResults()
        return
    end

    table.sort(sortTarget, function(left, right)
        local applicationComparison = CompareApplicationPriority(left, right)
        if applicationComparison ~= nil then
            return applicationComparison
        end
        if sortKey == "dungeon" then
            local leftName = tostring(left.dungeonName or "")
            local rightName = tostring(right.dungeonName or "")
            if leftName ~= rightName then
                return leftName < rightName
            end
            if (tonumber(left.activityID) or 0) ~= (tonumber(right.activityID) or 0) then
                return (tonumber(left.activityID) or 0) < (tonumber(right.activityID) or 0)
            end
        elseif sortKey == "members" then
            local comparison = CompareNumberValue(GetMemberCountValue(left), GetMemberCountValue(right), "desc")
            if comparison ~= nil then
                return comparison
            end
        elseif sortKey == "requirement" then
            local comparison = CompareNumberValue(GetRequirementValue(left), GetRequirementValue(right), sortDirection)
            if comparison ~= nil then
                return comparison
            end
        elseif sortKey == "score" then
            local comparison = CompareNumberValue(left.score, right.score, sortDirection)
            if comparison ~= nil then
                return comparison
            end
        end
        return CompareDefault(left, right)
    end)
    restoreFrozenResults()
end

local function BumpStateVersion(owner)
    local state = GetState(owner)
    state.version = (state.version or 0) + 1
end

GetProfile = function(owner)
    local profile = owner and owner.db and owner.db.profile
    if not profile then
        return nil
    end
    profile.findGroup = CopyDefaults(profile.findGroup)
    return profile.findGroup
end

local function GetDungeonOptions(owner)
    if owner and owner.GetMeetingStoneDungeonOptions then
        return owner:GetMeetingStoneDungeonOptions() or {}
    end
    return {}
end

local findGroupChallengeMapNameCache = {}

local function AddFindGroupCandidateChallengeMapID(mapIDs, seen, mapID)
    mapID = tonumber(mapID)
    if mapID and not seen[mapID] then
        seen[mapID] = true
        mapIDs[#mapIDs + 1] = mapID
    end
end

local function GetFindGroupCandidateChallengeMapIDs()
    local mapIDs = {}
    local seen = {}

    if C_ChallengeMode and C_ChallengeMode.GetMapTable then
        local ok, currentSeasonMaps = pcall(C_ChallengeMode.GetMapTable)
        if ok and type(currentSeasonMaps) == "table" then
            for _, mapID in ipairs(currentSeasonMaps) do
                AddFindGroupCandidateChallengeMapID(mapIDs, seen, mapID)
            end
        end
    end

    for _, mapID in ipairs(MEETINGSTONE_FALLBACK_CHALLENGE_MAP_IDS) do
        AddFindGroupCandidateChallengeMapID(mapIDs, seen, mapID)
    end

    return mapIDs
end

local function GetFindGroupChallengeMapName(mapID)
    mapID = tonumber(mapID)
    if not mapID then
        return nil
    end
    if findGroupChallengeMapNameCache[mapID] ~= nil then
        return findGroupChallengeMapNameCache[mapID] or nil
    end

    local mapName
    if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
        local ok, name = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
        if ok and type(name) == "string" and name ~= "" then
            mapName = name
        end
    end

    findGroupChallengeMapNameCache[mapID] = mapName or false
    return mapName
end

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

    value = value:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
    value = value:gsub("史诗钥石", ""):gsub("史詩鑰石", ""):gsub("Mythic Keystone", ""):gsub("Mythic%+", "")
    value = value:gsub("%s+", "")
    value = value:gsub("%(", ""):gsub("%)", ""):gsub("%[", ""):gsub("%]", "")
    value = value:gsub("（", ""):gsub("）", ""):gsub("【", ""):gsub("】", "")
    value = value:gsub("：", ":"):gsub("－", "-"):gsub("—", "-"):gsub("–", "-")
    return string.lower(value)
end

local function DoesFindGroupActivityNameMatchMapName(activityName, mapName)
    if type(activityName) ~= "string" or activityName == "" or type(mapName) ~= "string" or mapName == "" then
        return false
    end

    if activityName == mapName or activityName:find(mapName, 1, true) or mapName:find(activityName, 1, true) then
        return true
    end

    local normalizedActivityName = NormalizeFindGroupDungeonName(activityName)
    local normalizedMapName = NormalizeFindGroupDungeonName(mapName)
    return normalizedMapName ~= ""
        and normalizedActivityName ~= ""
        and (normalizedActivityName == normalizedMapName
            or normalizedActivityName:find(normalizedMapName, 1, true)
            or normalizedMapName:find(normalizedActivityName, 1, true))
end

local function GetFindGroupFallbackChallengeMapIDForGroup(groupID)
    groupID = tonumber(groupID)
    if not groupID then
        return nil
    end

    for index, fallbackGroupID in ipairs(MEETINGSTONE_FALLBACK_DUNGEON_GROUP_IDS) do
        if tonumber(fallbackGroupID) == groupID then
            return tonumber(MEETINGSTONE_FALLBACK_CHALLENGE_MAP_IDS[index])
        end
    end
    return nil
end

local function GetFindGroupChallengeMapIDForActivityInfo(activityInfo)
    if type(activityInfo) ~= "table" then
        return nil
    end

    local fullName = activityInfo.fullName
    local shortName = activityInfo.shortName
    for _, mapID in ipairs(GetFindGroupCandidateChallengeMapIDs()) do
        local mapName = GetFindGroupChallengeMapName(mapID)
        if DoesFindGroupActivityNameMatchMapName(fullName, mapName)
            or DoesFindGroupActivityNameMatchMapName(shortName, mapName)
        then
            return mapID
        end
    end

    return nil
end

local function AddFindGroupSelectionKey(selectedMap, key, option)
    if not key or key == "" then
        return
    end
    selectedMap.keys = selectedMap.keys or {}
    selectedMap.optionByKey = selectedMap.optionByKey or {}
    selectedMap.keys[key] = true
    selectedMap.optionByKey[key] = selectedMap.optionByKey[key] or option
end

local function ForEachFindGroupDungeonSelectionKey(option, activityInfo, activityID, callback)
    activityID = tonumber(activityID) or tonumber(option and option.activityID)
    if activityID and callback("activity:" .. activityID) == false then
        return false
    end

    local groupID = tonumber(option and option.groupID) or tonumber(activityInfo and activityInfo.groupFinderActivityGroupID)
    local challengeMapID = tonumber(option and option.challengeMapID)
        or GetFindGroupChallengeMapIDForActivityInfo(activityInfo)
        or GetFindGroupFallbackChallengeMapIDForGroup(groupID)
    if challengeMapID and callback("challenge:" .. challengeMapID) == false then
        return false
    end
    if groupID and callback("group:" .. groupID) == false then
        return false
    end

    local function addNameKey(name)
        local nameKey = NormalizeFindGroupDungeonName(name)
        if nameKey ~= "" and callback("name:" .. nameKey) == false then
            return false
        end
        return true
    end

    if addNameKey(option and option.activityName) == false then
        return false
    end
    if addNameKey(activityInfo and activityInfo.fullName) == false then
        return false
    end
    if addNameKey(activityInfo and activityInfo.shortName) == false then
        return false
    end
    if challengeMapID and addNameKey(GetFindGroupChallengeMapName(challengeMapID)) == false then
        return false
    end
    return true
end

local function AddFindGroupSelectedDungeonOption(selectedMap, option)
    if type(option) ~= "table" then
        return
    end

    ForEachFindGroupDungeonSelectionKey(option, nil, option.activityID, function(key)
        AddFindGroupSelectionKey(selectedMap, key, option)
    end)
end

local function GetFindGroupSelectedDungeonKey(selectedMap, option, activityInfo, activityID)
    selectedMap = selectedMap or {}
    activityID = tonumber(activityID)
    if activityID and selectedMap[activityID] then
        return "activity:" .. activityID
    end

    local foundKey
    ForEachFindGroupDungeonSelectionKey(option, activityInfo, activityID, function(key)
        if selectedMap.keys and selectedMap.keys[key] then
            foundKey = key
            return false
        end
    end)
    return foundKey
end

local function GetFindGroupOptionForSelectedKey(selectedMap, key)
    return key and selectedMap and selectedMap.optionByKey and selectedMap.optionByKey[key] or nil
end

local function BuildSeasonActivityMap(owner)
    local activityMap = {}
    local selectedMap = {}
    local settings = GetProfile(owner) or {}
    for _, option in ipairs(GetDungeonOptions(owner)) do
        local activityID = tonumber(option.activityID)
        if activityID then
            activityMap[activityID] = option
        end
    end
    for activityID, enabled in pairs(settings.selectedActivityIDs or {}) do
        local numericID = tonumber(activityID)
        if numericID and enabled then
            selectedMap[numericID] = true
            AddFindGroupSelectedDungeonOption(selectedMap, activityMap[numericID])
        end
    end
    return activityMap, selectedMap
end

local function HasSelectedActivities(settings)
    for _, enabled in pairs(settings and settings.selectedActivityIDs or {}) do
        if enabled then
            return true
        end
    end
    return false
end

local function GetActivityIDsFromInfo(info)
    local activityIDs = {}
    local seen = {}
    local function addActivityID(activityID)
        activityID = tonumber(activityID)
        if activityID and not seen[activityID] then
            seen[activityID] = true
            activityIDs[#activityIDs + 1] = activityID
        end
    end

    if type(info) ~= "table" then
        return activityIDs
    end
    addActivityID(info.activityID)
    if type(info.activityIDs) == "table" then
        for _, activityID in ipairs(info.activityIDs) do
            addActivityID(activityID)
        end
    end
    return activityIDs
end

local function GetSearchResultInfo(resultID)
    if not (C_LFGList and C_LFGList.GetSearchResultInfo) then
        return nil
    end
    local ok, info = pcall(C_LFGList.GetSearchResultInfo, resultID)
    if ok and type(info) == "table" then
        return info
    end
    return nil
end

local function GetActivityInfo(activityID)
    if not (activityID and C_LFGList and C_LFGList.GetActivityInfoTable) then
        return nil
    end
    local ok, info = pcall(C_LFGList.GetActivityInfoTable, activityID)
    return ok and type(info) == "table" and info or nil
end

local function ResolveFindGroupResultActivity(activityIDs, activityMap, selectedMap, hasSelection)
    for _, activityID in ipairs(SafeList(activityIDs)) do
        local option = activityMap and activityMap[activityID]
        local activityInfo
        local selectedKey = GetFindGroupSelectedDungeonKey(selectedMap, option, nil, activityID)
        if hasSelection and not selectedKey then
            activityInfo = GetActivityInfo(activityID)
            selectedKey = GetFindGroupSelectedDungeonKey(selectedMap, option, activityInfo, activityID)
        end
        if not option then
            activityInfo = activityInfo or GetActivityInfo(activityID)
            selectedKey = selectedKey or GetFindGroupSelectedDungeonKey(selectedMap, nil, activityInfo, activityID)
            option = GetFindGroupOptionForSelectedKey(selectedMap, selectedKey)
        end
        if option and (not hasSelection or selectedKey) then
            return activityID, option, activityInfo
        end
    end
    return nil, nil, nil
end

local function GetMemberCounts(resultID)
    if not (C_LFGList and C_LFGList.GetSearchResultMemberCounts) then
        return nil
    end
    local ok, counts = pcall(C_LFGList.GetSearchResultMemberCounts, resultID)
    return ok and type(counts) == "table" and counts or nil
end

local function GetApplicationStatus(resultID)
    if not (C_LFGList and C_LFGList.GetApplicationInfo) then
        return nil, nil, nil
    end
    local ok, _, status, pendingStatus, duration = pcall(C_LFGList.GetApplicationInfo, resultID)
    if ok then
        return status, pendingStatus, duration
    end
    return nil, nil, nil
end

local function GetApplicationStates(owner)
    owner.mkaFindGroupApplicationStates = type(owner.mkaFindGroupApplicationStates) == "table"
        and owner.mkaFindGroupApplicationStates
        or {}
    return owner.mkaFindGroupApplicationStates
end

local ApplyApplicationStateToResult

local function ClearApplicationStates(owner)
    if not owner then
        return
    end
    owner.mkaFindGroupApplicationStates = nil
    owner.mkaFindGroupAppliedResultIDs = nil
    owner.mkaFindGroupApplicationGroupMemberCount = nil
    owner.mkaFindGroupCancelledRemovalTokens = nil
    local state = GetState(owner)
    state.applicationCountdownScheduled = false
    for _, result in ipairs(state.results or {}) do
        ApplyApplicationStateToResult(result, nil, nil, nil, nil, nil, nil)
    end
end

local function StoreApplicationState(owner, resultID, status, pendingStatus, duration)
    resultID = tonumber(resultID)
    if not (owner and resultID and IsMeaningfulApplicationStatus(status, pendingStatus)) then
        return nil
    end

    local states = GetApplicationStates(owner)
    local record = states[resultID] or {}
    local now = GetNow()
    record.status = status
    record.pendingStatus = pendingStatus
    record.duration = tonumber(duration) or record.duration
    record.updatedAt = now
    if IsActiveApplicationStatus(status, pendingStatus) then
        record.appliedAt = GetApplicationAppliedAt(record, now, duration)
        record.expiresAt = GetApplicationExpiresAt(record.appliedAt)
    else
        record.expiresAt = nil
    end
    states[resultID] = record
    return record
end

local function GetApplicationState(owner, resultID)
    local states = owner and owner.mkaFindGroupApplicationStates
    resultID = tonumber(resultID)
    return resultID and type(states) == "table" and states[resultID] or nil
end

local function MergeApplicationState(owner, resultID, status, pendingStatus, duration)
    local record = GetApplicationState(owner, resultID)
    if IsActiveApplicationStatus(status, pendingStatus) then
        record = StoreApplicationState(owner, resultID, status, pendingStatus, duration)
    end
    if record then
        return record.status, record.pendingStatus, record.duration, record.appliedAt, record.updatedAt, record.expiresAt
    end
    if IsInactiveApplicationStatus(status) then
        return nil, nil, nil, nil, nil, nil
    end
    return status, pendingStatus, duration, nil, nil, nil
end

ApplyApplicationStateToResult = function(result, status, pendingStatus, duration, appliedAt, updatedAt, expiresAt)
    if not result then
        return
    end
    result.applicationStatus = status
    result.pendingApplicationStatus = pendingStatus
    result.applicationDuration = duration
    result.applicationAppliedAt = appliedAt
    result.applicationUpdatedAt = updatedAt
    result.applicationExpiresAt = expiresAt
end

local function GetSearchResultPlayerInfo(resultID, memberIndex)
    if not (C_LFGList and C_LFGList.GetSearchResultPlayerInfo) then
        return nil
    end
    local ok, info = pcall(C_LFGList.GetSearchResultPlayerInfo, resultID, memberIndex)
    return ok and type(info) == "table" and info or nil
end

local function NormalizeRoleKey(role)
    return ROLE_KEY_BY_SYSTEM_ROLE[role] or role
end

local function GetDisplayName(name)
    if type(name) ~= "string" or name == "" then
        return "-"
    end
    if Ambiguate then
        return Ambiguate(name, "short")
    end
    return name:gsub("%-.+$", "")
end

local function SplitFindGroupMemberName(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 GetMemberAccountTypeQuery(member)
    if not (member and member.name) then
        return nil, nil, nil
    end

    local name, embeddedRealm = SplitFindGroupMemberName(member.name)
    if not name then
        return nil, nil, nil
    end

    local realm = member.realm or embeddedRealm
    local key = realm and (name .. "-" .. realm) or name
    return name, realm, key
end

local function GetResultRelationship(info)
    local bnet = tonumber(info and info.numBNetFriends) or 0
    local friend = tonumber(info and info.numCharFriends) or 0
    local guild = tonumber(info and info.numGuildMates) or 0
    if bnet > 0 then
        return "bnet"
    end
    if friend > 0 then
        return "friend"
    end
    if guild > 0 then
        return "guild"
    end
    return nil
end

local function GetLeaderScore(info)
    local score = tonumber(info and info.leaderOverallDungeonScore)
    if score and score > 0 then
        return score
    end
    local scoreInfo = info and info.leaderDungeonScoreInfo
    if type(scoreInfo) == "table" then
        local candidate = scoreInfo[1] or scoreInfo
        score = tonumber(candidate and (candidate.mapScore or candidate.score or candidate.overallScore))
        if score and score > 0 then
            return score
        end
    end
    return 0
end

local function AddRoleEntry(entries, member)
    local roleKey = NormalizeRoleKey(member.role)
    if not roleKey then
        return
    end
    entries[#entries + 1] = {
        roleKey = roleKey,
        classFile = member.classFile,
        specName = member.specName,
        specID = member.specID,
    }
end

local function GetOrderedRoleEntries(members)
    local grouped = {
        TANK = {},
        HEAL = {},
        DPS = {},
    }
    for _, member in ipairs(members or {}) do
        local roleKey = NormalizeRoleKey(member.role)
        if grouped[roleKey] then
            AddRoleEntry(grouped[roleKey], member)
        end
    end
    local entries = {}
    for _, roleKey in ipairs({ "TANK", "HEAL", "DPS" }) do
        for _, entry in ipairs(grouped[roleKey]) do
            entries[#entries + 1] = entry
        end
    end
    return entries
end

local function AddGenericRoleEntries(entries, roleKey, count)
    count = math.max(0, math.floor(tonumber(count) or 0))
    for _ = 1, count do
        if #entries >= FIND_GROUP_MAX_MEMBERS then
            return
        end
        entries[#entries + 1] = { roleKey = roleKey }
    end
end

local function GetRoleEntriesFromCounts(counts, totalMembers)
    local entries = {}
    AddGenericRoleEntries(entries, "TANK", counts and counts.TANK)
    AddGenericRoleEntries(entries, "HEAL", counts and counts.HEALER)
    AddGenericRoleEntries(entries, "DPS", counts and counts.DAMAGER)

    local total = math.min(FIND_GROUP_MAX_MEMBERS, tonumber(totalMembers) or 0)
    while #entries < total do
        entries[#entries + 1] = { roleKey = "DPS" }
    end

    return entries
end

local function BuildMembers(resultID, info)
    local members = {}
    local hasLeaver = false
    local leaderClassFile = nil
    local leaderName = info and info.leaderName
    local memberCount = tonumber(info and info.numMembers) or 0

    for index = 1, memberCount do
        local playerInfo = GetSearchResultPlayerInfo(resultID, index)
        if playerInfo then
            local _, embeddedRealm = SplitFindGroupMemberName(playerInfo.name)
            local member = {
                name = playerInfo.name,
                realm = playerInfo.realm or playerInfo.realmName or playerInfo.server or playerInfo.serverName or embeddedRealm,
                displayName = GetDisplayName(playerInfo.name),
                role = playerInfo.assignedRole or playerInfo.role,
                classFile = playerInfo.classFilename or playerInfo.classFileName or playerInfo.classFile,
                className = playerInfo.className or playerInfo.localizedClassName,
                specName = playerInfo.specName,
                specID = playerInfo.specID or playerInfo.specId,
                isLeader = playerInfo.isLeader == true,
                isLeaver = playerInfo.isLeaver == true,
            }
            if member.isLeaver then
                hasLeaver = true
            end
            if member.isLeader or (leaderName and member.name == leaderName) then
                leaderClassFile = member.classFile
            end
            members[#members + 1] = member
        end
    end

    return members, hasLeaver, leaderClassFile
end

local function GetAvailableSlots(info, counts)
    local maxMembers = tonumber(info and info.maxMembers) or FIND_GROUP_MAX_MEMBERS
    if maxMembers <= 0 or maxMembers > FIND_GROUP_MAX_MEMBERS then
        maxMembers = FIND_GROUP_MAX_MEMBERS
    end
    local total = (tonumber(counts and counts.TANK) or 0)
        + (tonumber(counts and counts.HEALER) or 0)
        + (tonumber(counts and counts.DAMAGER) or 0)
    if total <= 0 then
        total = tonumber(info and info.numMembers) or 0
    end
    return math.max(0, maxMembers - total), total, maxMembers
end

local function PassesRoleFilters(settings, counts, availableSlots)
    counts = counts or {}
    local tank = tonumber(counts.TANK) or 0
    local healer = tonumber(counts.HEALER) or 0
    local damager = tonumber(counts.DAMAGER) or 0

    if settings.haveHealer and healer <= 0 then
        return false
    end
    if settings.noHealer and healer > 0 then
        return false
    end
    if settings.haveTank and tank <= 0 then
        return false
    end
    if settings.noTank and tank > 0 then
        return false
    end
    if settings.noDps and damager >= 3 then
        return false
    end
    if availableSlots < NormalizeFindGroupMinAvailableSlots(settings.minAvailableSlots) then
        return false
    end
    return true
end

local function PassesScoreFilter(settings, info)
    local minScore = tonumber(settings.minScore) or 0
    if minScore <= 0 then
        return true
    end
    return GetLeaderScore(info) >= minScore
end

local function IsTitleBlocked(owner, title)
    local state = GetState(owner)
    return title and title ~= "" and state.blockedTitles and state.blockedTitles[title] == true
end

local function IsAccountTypeInfoNewbie(accountTypeInfo)
    if accountTypeInfo and accountTypeInfo.status == "newbie" then
        return true
    end
    for _, badge in ipairs(accountTypeInfo and accountTypeInfo.badges or {}) do
        if badge and badge.key == "newbie" then
            return true
        end
    end
    return false
end

local function GetMemberNewbieState(member)
    local meetingStoneData = core.MeetingStoneData
    if not (member and member.name and meetingStoneData and meetingStoneData.GetAccountTypeInfo) then
        return false
    end
    local name, realm = GetMemberAccountTypeQuery(member)
    if not name then
        return false
    end
    local accountTypeInfo = meetingStoneData:GetAccountTypeInfo(name, realm)
    if IsAccountTypeInfoNewbie(accountTypeInfo) then
        return true
    end
    if accountTypeInfo and (accountTypeInfo.queryPending == true or accountTypeInfo.status == "pending") then
        return "pending"
    end
    return false
end

local function RefreshResultType(result, searchState)
    if not result then
        return nil, false
    end

    local hasNewbie = false
    local hasPendingNewbie = false
    for _, member in ipairs(result.members or {}) do
        local memberState = GetMemberNewbieState(member)
        member.isNewbie = memberState == true
        member.hasPendingNewbie = false
        if memberState == true then
            hasNewbie = true
        elseif memberState == "pending" then
            member.hasPendingNewbie = true
            hasPendingNewbie = true
        else
            local _, _, accountKey = GetMemberAccountTypeQuery(member)
            if accountKey and searchState and searchState.accountQuerySet and searchState.accountQuerySet[accountKey] then
                member.hasPendingNewbie = true
                hasPendingNewbie = true
            end
        end
    end

    result.hasNewbie = hasNewbie
    result.hasPendingNewbie = hasPendingNewbie
    if result.hasLeaver then
        result.resultType = "leaver"
    elseif hasNewbie then
        result.resultType = "newbie"
    else
        result.resultType = result.relationship
    end

    return hasNewbie, hasPendingNewbie
end

local function ApplyDetailCacheToResult(result, cache)
    if not (result and cache) then
        return result
    end

    result.members = cache.members or {}
    result.roleEntries = cache.roleEntries or {}
    result.hasLeaver = cache.hasLeaver == true
    result.hasNewbie = cache.hasNewbie == true
    result.hasPendingNewbie = cache.hasPendingNewbie == true
    result.detailLoaded = true
    result.resultType = cache.resultType or result.relationship
    if cache.leaderClassFile then
        result.leaderClassFile = cache.leaderClassFile
    end
    return result
end

local function GetDetailCache(state)
    state.detailCache = type(state.detailCache) == "table" and state.detailCache or {}
    return state.detailCache
end

local function StoreResultDetails(state, result)
    if not (state and result and result.resultID and result.detailLoaded == true) then
        return
    end

    GetDetailCache(state)[result.resultID] = {
        members = result.members or {},
        roleEntries = result.roleEntries or {},
        hasLeaver = result.hasLeaver == true,
        hasNewbie = result.hasNewbie == true,
        hasPendingNewbie = result.hasPendingNewbie == true,
        leaderClassFile = result.leaderClassFile,
        resultType = result.resultType,
        loadedAt = GetNow(),
    }
end

local function ClearDetailCacheForResult(state, resultID)
    if state and state.detailCache and resultID then
        state.detailCache[resultID] = nil
    end
end

local function ClearDetailQueue(state)
    if not state then
        return
    end
    state.detailQueue = nil
    state.detailQueueSet = nil
    state.detailQueueScheduled = false
end

local function ClearDetailState(state)
    ClearDetailQueue(state)
    state.detailCache = {}
end

local function BuildResult(owner, resultID, activityMap, selectedMap, hasSelection, detailMode)
    local settings = GetProfile(owner) or CopyDefaults({})
    local info = GetSearchResultInfo(resultID)
    local activityID, option, activityInfo = ResolveFindGroupResultActivity(
        GetActivityIDsFromInfo(info),
        activityMap,
        selectedMap,
        hasSelection
    )
    if not activityID or not option then
        return nil
    end
    if info and info.isDelisted == true then
        return nil
    end

    local title = info.name or ""
    if IsTitleBlocked(owner, title) then
        return nil
    end
    local comment = info.comment or ""
    local reportCacheKey = BuildReportCacheKey(title, info.leaderName, activityID, comment)
    if IsReportBlocked(owner, reportCacheKey) then
        return nil
    end

    local counts = GetMemberCounts(resultID)
    local availableSlots, totalMembers, maxMembers = GetAvailableSlots(info, counts)
    if not PassesRoleFilters(settings, counts, availableSlots) then
        return nil
    end
    if not PassesScoreFilter(settings, info) then
        return nil
    end

    local members, hasLeaver, leaderClassFile = {}, false, nil
    local roleEntries
    if detailMode == true then
        members, hasLeaver, leaderClassFile = BuildMembers(resultID, info)
        roleEntries = GetOrderedRoleEntries(members)
    else
        roleEntries = GetRoleEntriesFromCounts(counts, totalMembers)
    end

    activityInfo = activityInfo or GetActivityInfo(activityID)
    local dungeonName = option.activityName or activityInfo and (activityInfo.fullName or activityInfo.shortName) or ""
    local score = GetLeaderScore(info)
    local relationship = GetResultRelationship(info)

    local applicationStatus, pendingApplicationStatus, applicationDuration = GetApplicationStatus(resultID)
    local applicationAppliedAt, applicationUpdatedAt, applicationExpiresAt
    applicationStatus, pendingApplicationStatus, applicationDuration, applicationAppliedAt, applicationUpdatedAt, applicationExpiresAt = MergeApplicationState(
        owner,
        resultID,
        applicationStatus,
        pendingApplicationStatus,
        applicationDuration
    )
    local result = {
        resultID = resultID,
        activityID = activityID,
        title = title ~= "" and title or dungeonName,
        dungeonName = dungeonName,
        comment = comment,
        leaderName = info.leaderName,
        leaderDisplayName = GetDisplayName(info.leaderName),
        leaderClassFile = leaderClassFile,
        score = score,
        requiredItemLevel = tonumber(info.requiredItemLevel) or 0,
        age = tonumber(info.age) or 0,
        relationship = relationship,
        counts = counts or {},
        availableSlots = availableSlots,
        totalMembers = totalMembers,
        maxMembers = maxMembers,
        members = members,
        roleEntries = roleEntries,
        hasLeaver = hasLeaver,
        hasNewbie = false,
        hasPendingNewbie = false,
        detailLoaded = detailMode == true,
        resultType = relationship,
        applicationStatus = applicationStatus,
        pendingApplicationStatus = pendingApplicationStatus,
        applicationDuration = applicationDuration,
        applicationAppliedAt = applicationAppliedAt,
        applicationUpdatedAt = applicationUpdatedAt,
        applicationExpiresAt = applicationExpiresAt,
        reportCacheKey = reportCacheKey,
    }
    if IsActiveApplicationStatus(applicationStatus, pendingApplicationStatus) and ScheduleApplicationCountdown then
        ScheduleApplicationCountdown(owner)
    end

    if detailMode == true then
        RefreshResultType(result)
        StoreResultDetails(GetState(owner), result)
    else
        local state = GetState(owner)
        local cached = state.detailCache and state.detailCache[resultID]
        if cached then
            ApplyDetailCacheToResult(result, cached)
        end
    end

    return result
end

local ScheduleAccountQueryBatch

local function ProcessAccountQueryBatch(owner, state, token)
    if not state or token ~= state.processingToken then
        return
    end
    local meetingStoneData = core.MeetingStoneData
    if not (meetingStoneData and meetingStoneData.QueryLeader) then
        return
    end

    local queue = state.accountQueryQueue or {}
    local index = tonumber(state.accountQueryIndex) or 1
    local processed = 0
    while processed < FIND_GROUP_ACCOUNT_QUERY_BATCH_SIZE and index <= #queue do
        local entry = queue[index]
        index = index + 1
        processed = processed + 1
        local name = type(entry) == "table" and entry.name or entry
        local realm = type(entry) == "table" and entry.realm or nil
        local key = type(entry) == "table" and entry.key or name
        if name then
            pcall(meetingStoneData.QueryLeader, meetingStoneData, name, realm)
            if state.accountQuerySet then
                state.accountQuerySet[key] = nil
            end
        end
    end

    state.accountQueryIndex = index
    if index <= #queue then
        ScheduleAccountQueryBatch(owner, state, token)
        return
    end

    state.accountQueryQueue = nil
    state.accountQuerySet = nil
    state.accountQueryIndex = nil
end

ScheduleAccountQueryBatch = function(owner, state, token)
    if not state or state.accountQueryScheduled then
        return
    end
    state.accountQueryScheduled = true
    ScheduleLater(FIND_GROUP_ACCOUNT_QUERY_DELAY, function()
        if token ~= state.processingToken then
            return
        end
        state.accountQueryScheduled = false
        ProcessAccountQueryBatch(owner, state, token)
    end)
end

local function QueueAccountTypesForResult(owner, state, token, result)
    local meetingStoneData = core.MeetingStoneData
    if not (meetingStoneData and meetingStoneData.QueryLeader) then
        return false
    end
    if not state then
        return false
    end
    local queued = false
    state.accountQueryQueue = state.accountQueryQueue or {}
    state.accountQuerySet = state.accountQuerySet or {}
    state.accountQueryIndex = state.accountQueryIndex or 1
    local awaitingQueuedName = false
    for _, member in ipairs(result and result.members or {}) do
        local name, realm, accountKey = GetMemberAccountTypeQuery(member)
        if name and accountKey and not member.isLeaver then
            if not state.accountQuerySet[accountKey] then
                state.accountQuerySet[accountKey] = true
                state.accountQueryQueue[#state.accountQueryQueue + 1] = {
                    name = name,
                    realm = realm,
                    key = accountKey,
                }
                queued = true
            else
                awaitingQueuedName = true
            end
        end
    end
    if queued then
        ScheduleAccountQueryBatch(owner, state, token)
    end
    return queued or awaitingQueuedName
end

local ScheduleSearchResultBatch
local ScheduleNewbieRecheckBatch
local ScheduleDetailQueue
local TryCompleteInitialDisplay

local function ClearSearchQueue(state)
    state.resultQueue = nil
    state.resultQueueIndex = nil
    state.activityMap = nil
    state.selectedMap = nil
    state.hasSelection = nil
end

local function AddResultToState(state, result)
    if not (state and result and result.resultID) then
        return false
    end
    if state.resultByID and state.resultByID[result.resultID] then
        return false
    end
    state.results[#state.results + 1] = result
    state.resultByID[result.resultID] = result
    return true
end

local function LoadResultDetails(owner, state, resultID)
    resultID = tonumber(resultID)
    local result = resultID and state and state.resultByID and state.resultByID[resultID]
    if not result then
        return nil, false
    end

    local cached = state.detailCache and state.detailCache[resultID]
    if cached then
        ApplyDetailCacheToResult(result, cached)
        return result, true
    end
    if result.detailLoaded == true then
        StoreResultDetails(state, result)
        return result, false
    end

    local members, hasLeaver, leaderClassFile = BuildMembers(resultID, {
        leaderName = result.leaderName,
        numMembers = result.totalMembers,
    })
    result.members = members
    result.roleEntries = GetOrderedRoleEntries(members)
    result.hasLeaver = hasLeaver
    if leaderClassFile then
        result.leaderClassFile = leaderClassFile
    end
    result.detailLoaded = true
    RefreshResultType(result, state)
    StoreResultDetails(state, result)
    return result, true
end

local function ProcessDetailQueue(owner, token)
    local state = GetState(owner)
    if token ~= state.processingToken then
        return
    end
    state.detailQueueScheduled = false
    if not IsFindGroupWindowActive(owner) then
        ClearDetailQueue(state)
        state.initialDisplayPending = false
        state.initialDisplayReady = false
        return
    end

    local queue = state.detailQueue or {}
    local processed = 0
    local changed = false
    while processed < FIND_GROUP_VISIBLE_DETAIL_BATCH_SIZE and #queue > 0 do
        local resultID = table.remove(queue, 1)
        if state.detailQueueSet then
            state.detailQueueSet[resultID] = nil
        end
        local _, didChange = LoadResultDetails(owner, state, resultID)
        if didChange then
            changed = true
        end
        processed = processed + 1
    end

    if #queue <= 0 then
        state.detailQueue = nil
        state.detailQueueSet = nil
    end
    local initialDisplayReady = TryCompleteInitialDisplay and TryCompleteInitialDisplay(owner, state, token)
    if changed or initialDisplayReady then
        BumpStateVersion(owner)
        RefreshFindGroupWindow(owner, initialDisplayReady and "FindGroupInitialReady" or "FindGroupDetailBatch", tostring(processed))
    end
    if state.detailQueue and #state.detailQueue > 0 then
        ScheduleDetailQueue(owner, token)
    end
end

ScheduleDetailQueue = function(owner, token)
    local state = GetState(owner)
    if state.detailQueueScheduled then
        return
    end
    state.detailQueueScheduled = true
    ScheduleLater(FIND_GROUP_VISIBLE_DETAIL_DELAY, function()
        ProcessDetailQueue(owner, token)
    end)
end

local function QueueNewbiePendingResult(owner, state, token, result)
    if not (state and result and result.resultID) then
        return
    end
    state.newbiePendingResults = state.newbiePendingResults or {}
    state.newbiePendingByID = state.newbiePendingByID or {}
    if state.newbiePendingByID[result.resultID] or (state.resultByID and state.resultByID[result.resultID]) then
        return
    end

    result.newbieRecheckCount = 0
    state.newbiePendingByID[result.resultID] = result
    state.newbiePendingResults[#state.newbiePendingResults + 1] = result
    state.newbieSearching = true
    ScheduleNewbieRecheckBatch(owner, state, token)
end

local function HasNewbiePendingResults(state)
    return type(state and state.newbiePendingResults) == "table" and #state.newbiePendingResults > 0
end

local function CompleteNewbieRecheck(owner, state, token)
    if not state or token ~= state.processingToken then
        return
    end
    state.searching = false
    state.newbieSearching = false
    state.newbieRecheckScheduled = false
    state.newbiePendingResults = nil
    state.newbiePendingByID = nil
    state.searchCompleted = true
    state.searchCompletedEmpty = #(state.results or {}) <= 0
    SortResults(owner)
    if TryCompleteInitialDisplay then
        TryCompleteInitialDisplay(owner, state, token)
    end
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupNewbieComplete", tostring(#(state.results or {})))
end

local function ProcessNewbieRecheckBatch(owner, state, token)
    if not state or token ~= state.processingToken then
        return
    end
    if not IsFindGroupWindowActive(owner) then
        CancelSearchProcessing(state)
        state.searching = false
        state.initialDisplayPending = false
        state.initialDisplayReady = false
        return
    end

    local queue = state.newbiePendingResults or {}
    local nextQueue = {}
    local processed = 0
    for _, result in ipairs(queue) do
        if processed < FIND_GROUP_NEWBIE_RECHECK_BATCH_SIZE then
            processed = processed + 1
            local hasNewbie, hasPendingNewbie = RefreshResultType(result, state)
            StoreResultDetails(state, result)
            if hasNewbie then
                AddResultToState(state, result)
                if state.newbiePendingByID then
                    state.newbiePendingByID[result.resultID] = nil
                end
            elseif hasPendingNewbie and (tonumber(result.newbieRecheckCount) or 0) < FIND_GROUP_NEWBIE_RECHECK_LIMIT then
                result.newbieRecheckCount = (tonumber(result.newbieRecheckCount) or 0) + 1
                nextQueue[#nextQueue + 1] = result
            else
                ClearDetailCacheForResult(state, result.resultID)
                if state.newbiePendingByID then
                    state.newbiePendingByID[result.resultID] = nil
                end
            end
        else
            nextQueue[#nextQueue + 1] = result
        end
    end

    state.newbiePendingResults = nextQueue
    SortResults(owner)
    local initialDisplayReady = TryCompleteInitialDisplay and TryCompleteInitialDisplay(owner, state, token)
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, initialDisplayReady and "FindGroupInitialReady" or "FindGroupNewbieBatch", tostring(#(state.results or {})))

    if #nextQueue > 0 then
        ScheduleNewbieRecheckBatch(owner, state, token)
        return
    end

    if state.processing ~= true then
        CompleteNewbieRecheck(owner, state, token)
    else
        state.newbieSearching = HasNewbiePendingResults(state)
    end
end

ScheduleNewbieRecheckBatch = function(owner, state, token)
    if not state or state.newbieRecheckScheduled then
        return
    end
    state.newbieRecheckScheduled = true
    ScheduleLater(FIND_GROUP_NEWBIE_RECHECK_DELAY, function()
        if token ~= state.processingToken then
            return
        end
        state.newbieRecheckScheduled = false
        ProcessNewbieRecheckBatch(owner, state, token)
    end)
end

local function CompleteSearchResultProcessing(owner, state, token)
    if token ~= state.processingToken then
        return
    end
    ClearSearchQueue(state)
    local hasPendingNewbie = HasNewbiePendingResults(state)
    state.processing = false
    state.searching = hasPendingNewbie
    state.lastError = nil
    state.lastResultBatchRefreshAt = nil
    state.newbieSearching = hasPendingNewbie
    state.searchCompleted = not hasPendingNewbie
    state.searchCompletedEmpty = not hasPendingNewbie and #(state.results or {}) <= 0 or false
    SortResults(owner)
    if TryCompleteInitialDisplay then
        TryCompleteInitialDisplay(owner, state, token)
    end
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupResultsComplete", tostring(#(state.results or {})))

    if hasPendingNewbie then
        ScheduleNewbieRecheckBatch(owner, state, token)
    else
        state.newbieSearching = false
    end
end

local function ProcessSearchResultBatch(owner, token)
    local state = GetState(owner)
    if token ~= state.processingToken then
        return
    end
    if not IsFindGroupWindowActive(owner) then
        CancelSearchProcessing(state)
        state.searching = false
        state.initialDisplayPending = false
        state.initialDisplayReady = false
        return
    end

    local queue = state.resultQueue or {}
    local index = tonumber(state.resultQueueIndex) or 1
    local settings = GetProfile(owner) or {}
    local searchNewbie = settings.searchNewbie == true
    local batchSize = searchNewbie and FIND_GROUP_DETAIL_RESULT_BATCH_SIZE or FIND_GROUP_RESULT_BATCH_SIZE
    local processed = 0
    while processed < batchSize and index <= #queue do
        local resultID = queue[index]
        index = index + 1
        processed = processed + 1

        local result = BuildResult(owner, resultID, state.activityMap or {}, state.selectedMap or {}, state.hasSelection == true, searchNewbie)
        if result then
            if searchNewbie then
                local queuedAccountQuery = QueueAccountTypesForResult(owner, state, token, result)
                local hasNewbie, hasPendingNewbie = RefreshResultType(result, state)
                StoreResultDetails(state, result)
                if hasNewbie then
                    AddResultToState(state, result)
                elseif queuedAccountQuery or hasPendingNewbie then
                    QueueNewbiePendingResult(owner, state, token, result)
                else
                    ClearDetailCacheForResult(state, result.resultID)
                end
            else
                AddResultToState(state, result)
            end
        end
    end

    state.resultQueueIndex = index
    SortResults(owner)
    local initialDisplayReady = TryCompleteInitialDisplay and TryCompleteInitialDisplay(owner, state, token)
    if index <= #queue then
        local now = GetNow()
        if initialDisplayReady or not state.lastResultBatchRefreshAt or now - state.lastResultBatchRefreshAt >= FIND_GROUP_RESULT_REFRESH_INTERVAL then
            state.lastResultBatchRefreshAt = now
            BumpStateVersion(owner)
            RefreshFindGroupWindow(owner, initialDisplayReady and "FindGroupInitialReady" or "FindGroupResultsBatch", tostring(#(state.results or {})))
        end
        ScheduleSearchResultBatch(owner, token)
        return
    end

    CompleteSearchResultProcessing(owner, state, token)
end

ScheduleSearchResultBatch = function(owner, token)
    ScheduleLater(0, function()
        ProcessSearchResultBatch(owner, token)
    end)
end

local function RefreshUpdatedResult(owner, resultID, refreshSource)
    if not IsFindGroupWindowActive(owner) then
        return false
    end

    local state = GetState(owner)
    resultID = tonumber(resultID)
    if not (resultID and state.resultByID and state.resultByID[resultID]) then
        return false
    end

    local previousResult = state.resultByID[resultID]
    local activityMap, selectedMap = BuildSeasonActivityMap(owner)
    local settings = GetProfile(owner) or {}
    local detailMode = settings.searchNewbie == true or (previousResult and previousResult.detailLoaded == true)
    ClearDetailCacheForResult(state, resultID)
    local result = BuildResult(owner, resultID, activityMap, selectedMap, HasSelectedActivities(settings), detailMode)
    if result then
        if settings.searchNewbie == true then
            QueueAccountTypesForResult(owner, state, state.processingToken, result)
            RefreshResultType(result, state)
            StoreResultDetails(state, result)
            if result.hasNewbie ~= true then
                result = nil
            end
        end
    end

    if result then
        local replaced = false
        for index, current in ipairs(state.results or {}) do
            if current.resultID == resultID then
                state.results[index] = result
                replaced = true
                break
            end
        end
        if not replaced then
            state.results[#state.results + 1] = result
        end
        state.resultByID[resultID] = result
    else
        ClearDetailCacheForResult(state, resultID)
        for index = #(state.results or {}), 1, -1 do
            local current = state.results[index]
            if current and current.resultID == resultID then
                table.remove(state.results, index)
            end
        end
        state.resultByID[resultID] = nil
    end

    SortResults(owner)
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupResultUpdated", tostring(refreshSource or resultID))
    return true
end

function FindGroupSearch.GetSettings(owner)
    return GetProfile(owner)
end

function FindGroupSearch.GetState(owner)
    return GetState(owner)
end

function FindGroupSearch.GetDungeonOptions(owner)
    return GetDungeonOptions(owner)
end

function FindGroupSearch.GetSelectedDungeonText(owner)
    local settings = GetProfile(owner) or {}
    local selectedCount = 0
    local selectedName
    local options = GetDungeonOptions(owner)
    local optionCount = #options
    for _, option in ipairs(options) do
        local activityID = tonumber(option.activityID)
        if activityID and settings.selectedActivityIDs and settings.selectedActivityIDs[activityID] then
            selectedCount = selectedCount + 1
            selectedName = option.activityName
        end
    end
    if selectedCount <= 0 then
        return TRANSLATE("FIND_GROUP.DUNGEON.DEFAULT")
    end
    if optionCount > 0 and selectedCount >= optionCount then
        return TRANSLATE("FIND_GROUP.DUNGEON.ALL_SELECTED")
    end
    if selectedCount == 1 then
        return selectedName or TRANSLATE("FIND_GROUP.DUNGEON.SELECTED_ONE")
    end
    return string.format(TRANSLATE("FIND_GROUP.DUNGEON.SELECTED_COUNT_FORMAT"), selectedCount)
end

function FindGroupSearch.HasSelectedDungeons(owner)
    return HasSelectedActivities(GetProfile(owner) or {})
end

function FindGroupSearch.IsDungeonSelected(owner, activityID)
    local settings = GetProfile(owner) or {}
    activityID = tonumber(activityID)
    return activityID and settings.selectedActivityIDs and settings.selectedActivityIDs[activityID] == true
end

function FindGroupSearch.ToggleDungeon(owner, activityID)
    local settings = GetProfile(owner)
    if not settings then
        return
    end
    activityID = tonumber(activityID)
    if not activityID then
        return
    end
    settings.selectedActivityIDs[activityID] = not settings.selectedActivityIDs[activityID] or nil
    BumpStateVersion(owner)
end

function FindGroupSearch.ClearDungeonSelection(owner)
    local settings = GetProfile(owner)
    if not settings then
        return
    end
    wipe(settings.selectedActivityIDs)
    BumpStateVersion(owner)
end

function FindGroupSearch.SelectAllDungeons(owner)
    local settings = GetProfile(owner)
    if not settings then
        return
    end
    wipe(settings.selectedActivityIDs)
    for _, option in ipairs(GetDungeonOptions(owner)) do
        local activityID = tonumber(option.activityID)
        if activityID then
            settings.selectedActivityIDs[activityID] = true
        end
    end
    BumpStateVersion(owner)
end

function FindGroupSearch.SetBooleanFilter(owner, key, value)
    local settings = GetProfile(owner)
    if not settings then
        return
    end
    settings[key] = value == true
    if key == "haveHealer" and settings[key] then
        settings.noHealer = false
    elseif key == "noHealer" and settings[key] then
        settings.haveHealer = false
    elseif key == "haveTank" and settings[key] then
        settings.noTank = false
    elseif key == "noTank" and settings[key] then
        settings.haveTank = false
    end
    BumpStateVersion(owner)
end

function FindGroupSearch.SetNumericFilter(owner, key, value)
    local settings = GetProfile(owner)
    if not settings then
        return
    end
    local numberValue = tonumber(value) or 0
    if key == "minAvailableSlots" then
        numberValue = NormalizeFindGroupMinAvailableSlots(numberValue)
    elseif key == "minScore" then
        numberValue = math.max(0, math.floor(numberValue))
    end
    settings[key] = numberValue
    BumpStateVersion(owner)
end

function FindGroupSearch.GetSort(owner)
    local settings = GetProfile(owner) or {}
    local sortKey = settings.sortKey or ""
    if not FIND_GROUP_SORTABLE_KEYS[sortKey] then
        return "", settings.sortDirection == "asc" and "asc" or "desc"
    end
    return sortKey, settings.sortDirection == "asc" and "asc" or "desc"
end

function FindGroupSearch.SetSort(owner, key)
    local settings = GetProfile(owner)
    if not settings or not FIND_GROUP_SORTABLE_KEYS[key] then
        return
    end

    if FIND_GROUP_DIRECTIONAL_SORT_KEYS[key] then
        if settings.sortKey == key then
            settings.sortDirection = settings.sortDirection == "desc" and "asc" or "desc"
        else
            settings.sortDirection = "desc"
        end
    else
        settings.sortDirection = "desc"
    end
    settings.sortKey = key
    SortResults(owner)
    BumpStateVersion(owner)
end

function FindGroupSearch.SortResults(owner)
    SortResults(owner)
end

function FindGroupSearch.Reset(owner)
    local profile = owner and owner.db and owner.db.profile
    if not profile then
        return
    end
    local previous = GetProfile(owner) or {}
    profile.findGroup = CopyDefaults({
        quickJoin = previous.quickJoin == true,
        autoJoin = previous.autoJoin == true,
        reportBlockedResults = type(previous.reportBlockedResults) == "table" and previous.reportBlockedResults or {},
    })
    local state = GetState(owner)
    CancelSearchProcessing(state)
    ClearDetailState(state)
    ClearApplicationStates(owner)
    state.results = {}
    state.resultByID = {}
    state.blockedTitles = {}
    state.searching = false
    state.processing = false
    state.lastError = nil
    state.lastSearchAt = nil
    state.searchResultsRequestID = nil
    state.initialDisplayPending = false
    state.initialDisplayReady = false
    state.initialDisplayStartedAt = nil
    state.searchCompleted = false
    state.searchCompletedEmpty = false
    BumpStateVersion(owner)
end

function FindGroupSearch.BlockTitle(owner, title)
    if type(title) ~= "string" or title == "" then
        return
    end
    local state = GetState(owner)
    state.blockedTitles[title] = true
    for index = #(state.results or {}), 1, -1 do
        local result = state.results[index]
        if result and result.title == title then
            ClearDetailCacheForResult(state, result.resultID)
            state.resultByID[result.resultID] = nil
            table.remove(state.results, index)
        end
    end
    BumpStateVersion(owner)
end

local function MarkReportedResultForRemoval(owner, record, refreshSource)
    if not (owner and record and record.reportCacheKey) then
        return false
    end

    StoreReportBlockedResult(owner, record)
    local state = GetState(owner)
    local resultID = tonumber(record.resultID)
    local result = resultID and state.resultByID and state.resultByID[resultID]
    if result then
        result.reportRemovalPending = true
        result.reportRemovalType = record.reportType
        result.reportRemovalToken = (tonumber(result.reportRemovalToken) or 0) + 1
        result.reportCacheKey = record.reportCacheKey
        state.reportRemovalTokens = type(state.reportRemovalTokens) == "table" and state.reportRemovalTokens or {}
        state.reportRemovalTokens[resultID] = result.reportRemovalToken
    end

    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, refreshSource or "FindGroupReportedResult", tostring(resultID or ""))
    return true
end

function FindGroupSearch.CompleteReportRemoval(owner, resultID, token)
    local state = GetState(owner)
    resultID = tonumber(resultID)
    if not (resultID and state and state.resultByID) then
        return false
    end

    local result = state.resultByID[resultID]
    if not (result and result.reportRemovalPending == true) then
        return false
    end
    if token and tonumber(result.reportRemovalToken) ~= tonumber(token) then
        return false
    end

    ClearDetailCacheForResult(state, resultID)
    state.resultByID[resultID] = nil
    if state.reportRemovalTokens then
        state.reportRemovalTokens[resultID] = nil
    end
    for index = #(state.results or {}), 1, -1 do
        local current = state.results[index]
        if current and current.resultID == resultID then
            table.remove(state.results, index)
        end
    end
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupReportedResultRemoved", tostring(resultID))
    return true
end

local function ExtractReportResultID(...)
    for index = 1, select("#", ...) do
        local value = select(index, ...)
        local valueType = type(value)
        if valueType == "number" then
            return value
        elseif valueType == "table" then
            local candidate = value.searchResultID or value.resultID or value.listingID or value.lfgListID or value.id
            if tonumber(candidate) then
                return tonumber(candidate)
            end
        end
    end
    return nil
end

ConfirmPendingGroupReport = function(owner, resultID)
    local pending = owner and owner.mkaFindGroupPendingGroupReport
    if type(pending) ~= "table" then
        return false
    end

    local now = GetNow()
    if (now - (tonumber(pending.startedAt) or now)) > FIND_GROUP_PENDING_REPORT_TIMEOUT_SECONDS then
        owner.mkaFindGroupPendingGroupReport = nil
        return false
    end

    resultID = tonumber(resultID)
    if resultID and pending.resultID and resultID ~= tonumber(pending.resultID) then
        return false
    end

    owner.mkaFindGroupPendingGroupReport = nil
    pending.blockedAt = now
    return MarkReportedResultForRemoval(owner, pending, "FindGroupReportGroupConfirmed")
end

local function EnsureReportHooks(owner)
    if not owner or owner.mkaFindGroupReportHooksInstalled then
        return
    end
    owner.mkaFindGroupReportHooksInstalled = true

    if hooksecurefunc and C_LFGList and C_LFGList.ReportSearchResult then
        pcall(hooksecurefunc, C_LFGList, "ReportSearchResult", function(...)
            ConfirmPendingGroupReport(owner, ExtractReportResultID(...))
        end)
    end

    if hooksecurefunc and C_ReportSystem and C_ReportSystem.SendReport then
        pcall(hooksecurefunc, C_ReportSystem, "SendReport", function(...)
            ConfirmPendingGroupReport(owner, ExtractReportResultID(...))
        end)
    end
end

local function HookPendingReportDialog(owner)
    if not owner then
        return
    end
    ScheduleLater(0, function()
        local pending = owner.mkaFindGroupPendingGroupReport
        if type(pending) ~= "table" then
            return
        end
        for _, frameName in ipairs({ "ReportFrame", "ReportPlayerFrame", "PlayerReportFrame" }) do
            local frame = _G and _G[frameName]
            if frame and frame.HookScript and not frame.mkaFindGroupReportHideHooked then
                frame.mkaFindGroupReportHideHooked = true
                frame:HookScript("OnHide", function()
                    local current = owner.mkaFindGroupPendingGroupReport
                    local token = current and current.pendingToken
                    if not token then
                        return
                    end
                    ScheduleLater(0.1, function()
                        local latest = owner.mkaFindGroupPendingGroupReport
                        if latest and latest.pendingToken == token then
                            owner.mkaFindGroupPendingGroupReport = nil
                        end
                    end)
                end)
            end
        end
    end)
end

function FindGroupSearch.ReportAdvertisement(owner, result)
    local record = BuildReportRecordFromResult(result, "advertisement")
    if not record then
        return false
    end

    if LFGList_ReportAdvertisement and result and result.resultID then
        pcall(LFGList_ReportAdvertisement, result.resultID, result.leaderName)
    end
    return MarkReportedResultForRemoval(owner, record, "FindGroupReportAdvertisement")
end

function FindGroupSearch.ReportGroup(owner, result)
    local record = BuildReportRecordFromResult(result, "group")
    if not record then
        return false
    end
    if not (LFGList_ReportListing and result and result.resultID) then
        return false
    end

    EnsureReportHooks(owner)
    record.startedAt = GetNow()
    record.pendingToken = (tonumber(owner.mkaFindGroupPendingReportToken) or 0) + 1
    owner.mkaFindGroupPendingReportToken = record.pendingToken
    owner.mkaFindGroupPendingGroupReport = record
    local ok = pcall(LFGList_ReportListing, result.resultID, result.leaderName)
    if not ok then
        owner.mkaFindGroupPendingGroupReport = nil
        return false
    end
    HookPendingReportDialog(owner)
    return true
end

function FindGroupSearch.IsMemberNewbie(member)
    return GetMemberNewbieState(member) == true
end

function FindGroupSearch.GetResultType(result)
    if not result then
        return nil
    end
    return result.resultType
end

function FindGroupSearch.GetApplicationDisplayState(result)
    if IsActiveApplicationStatus(result and result.applicationStatus, result and result.pendingApplicationStatus) then
        return "pending"
    end
    if result and result.applicationStatus == "declined" then
        return "declined"
    end
    if IsInactiveApplicationStatus(result and result.applicationStatus) then
        return "cancelled"
    end
    return nil
end

function FindGroupSearch.GetApplicationRemainingSeconds(result)
    if not IsActiveApplicationStatus(result and result.applicationStatus, result and result.pendingApplicationStatus) then
        return nil
    end

    local expiresAt = tonumber(result and result.applicationExpiresAt)
    if not expiresAt then
        expiresAt = GetApplicationExpiresAt(result and result.applicationAppliedAt)
    end
    if not expiresAt then
        return FIND_GROUP_APPLICATION_TIMEOUT_SECONDS
    end
    return math.max(0, math.ceil(expiresAt - GetNow()))
end

function FindGroupSearch.QueueResultDetails(owner, resultID, highPriority)
    local state = GetState(owner)
    resultID = tonumber(resultID)
    local result = resultID and state.resultByID and state.resultByID[resultID]
    if not result or result.detailLoaded == true then
        return result
    end

    local cached = state.detailCache and state.detailCache[resultID]
    if cached then
        ApplyDetailCacheToResult(result, cached)
        BumpStateVersion(owner)
        RefreshFindGroupWindow(owner, "FindGroupDetailCache", tostring(resultID))
        return result
    end

    state.detailQueue = state.detailQueue or {}
    state.detailQueueSet = state.detailQueueSet or {}
    if state.detailQueueSet[resultID] then
        if highPriority == true then
            for index, queuedResultID in ipairs(state.detailQueue) do
                if queuedResultID == resultID then
                    table.remove(state.detailQueue, index)
                    table.insert(state.detailQueue, 1, resultID)
                    break
                end
            end
        end
        return result
    end
    state.detailQueueSet[resultID] = true
    if highPriority == true then
        table.insert(state.detailQueue, 1, resultID)
    else
        state.detailQueue[#state.detailQueue + 1] = resultID
    end

    ScheduleDetailQueue(owner, state.processingToken)
    return result
end

local function IsInitialDisplayCollectionReady(state)
    local results = state and state.results or {}
    local resultCount = #results
    if resultCount >= FIND_GROUP_INITIAL_DISPLAY_ROWS then
        return true
    end
    if state and state.processing == true then
        return false
    end
    if HasNewbiePendingResults(state) then
        local startedAt = tonumber(state.initialDisplayStartedAt)
        return resultCount > 0
            and startedAt ~= nil
            and GetNow() - startedAt >= FIND_GROUP_INITIAL_NEWBIE_WAIT_SECONDS
    end
    return true
end

TryCompleteInitialDisplay = function(owner, state, token)
    state = state or GetState(owner)
    if not (state and state.initialDisplayPending == true and state.initialDisplayReady ~= true) then
        return false
    end
    if token and token ~= state.processingToken then
        return false
    end
    if not IsInitialDisplayCollectionReady(state) then
        return false
    end

    SortResults(owner)
    local results = state.results or {}
    local targetCount = math.min(FIND_GROUP_INITIAL_DISPLAY_ROWS, #results)
    if targetCount <= 0 then
        state.initialDisplayPending = false
        state.initialDisplayReady = true
        return true
    end

    local waitingForDetails = false
    for index = targetCount, 1, -1 do
        local result = results[index]
        if result and result.resultID and result.detailLoaded ~= true then
            result = FindGroupSearch.QueueResultDetails(owner, result.resultID, true) or result
            if result.detailLoaded ~= true then
                waitingForDetails = true
            end
        end
    end
    if waitingForDetails then
        return false
    end

    state.initialDisplayPending = false
    state.initialDisplayReady = true
    return true
end

function FindGroupSearch.EnsureResultDetails(owner, resultID)
    local state = GetState(owner)
    resultID = tonumber(resultID)
    local result = resultID and state.resultByID and state.resultByID[resultID]
    if not result or result.detailLoaded == true then
        return result
    end

    local cached = state.detailCache and state.detailCache[resultID]
    if cached then
        ApplyDetailCacheToResult(result, cached)
        BumpStateVersion(owner)
        return result
    end
    FindGroupSearch.QueueResultDetails(owner, resultID, true)
    return result
end

function FindGroupSearch.RefreshResults(owner, refreshSource)
    local state = GetState(owner)
    local searchRequestID = tonumber(state.searchRequestID) or 0
    if state.searchResultsRequestID == searchRequestID then
        return
    end
    state.searchResultsRequestID = searchRequestID
    CancelSearchProcessing(state)
    ClearDetailState(state)
    state.searching = false
    state.processing = true
    state.lastRefreshSource = refreshSource
    state.lastRefreshAt = GetNow()
    state.results = {}
    state.resultByID = {}
    state.initialDisplayPending = true
    state.initialDisplayReady = false
    state.initialDisplayStartedAt = state.initialDisplayStartedAt or GetNow()
    state.searchCompleted = false
    state.searchCompletedEmpty = false

    if not (C_LFGList and C_LFGList.GetSearchResults) then
        state.processing = false
        state.initialDisplayPending = false
        state.initialDisplayReady = false
        state.lastError = TRANSLATE("FIND_GROUP.ERROR.LFG_UNAVAILABLE")
        BumpStateVersion(owner)
        RefreshFindGroupWindow(owner, "FindGroupResultsFailed")
        return
    end

    local ok, first, second = pcall(C_LFGList.GetSearchResults)
    local resultIDs = type(second) == "table" and second or type(first) == "table" and first or nil
    if not ok or type(resultIDs) ~= "table" then
        state.processing = false
        state.initialDisplayPending = false
        state.initialDisplayReady = false
        state.lastError = TRANSLATE("FIND_GROUP.ERROR.SEARCH_FAILED")
        BumpStateVersion(owner)
        RefreshFindGroupWindow(owner, "FindGroupResultsFailed")
        return
    end

    local activityMap, selectedMap = BuildSeasonActivityMap(owner)
    local settings = GetProfile(owner) or {}
    state.lastError = nil
    state.activityMap = activityMap
    state.selectedMap = selectedMap
    state.hasSelection = HasSelectedActivities(settings)
    state.resultQueue = resultIDs
    state.resultQueueIndex = 1
    state.processingToken = (tonumber(state.processingToken) or 0) + 1
    local token = state.processingToken
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupResultsStart", tostring(#resultIDs))
    ScheduleSearchResultBatch(owner, token)
end

local function GetLanguageSearchFilter()
    if C_LFGList and C_LFGList.GetLanguageSearchFilter then
        local ok, languages = pcall(C_LFGList.GetLanguageSearchFilter)
        if ok then
            return languages
        end
    end
    return nil
end

local function ClearNativeSearchFields()
    if C_LFGList and C_LFGList.ClearSearchTextFields then
        pcall(C_LFGList.ClearSearchTextFields)
    end
end

local function RunNativeSearch()
    local baseFilter = Enum and Enum.LFGListFilter and Enum.LFGListFilter.PvE or 4
    ClearNativeSearchFields()
    local languages = GetLanguageSearchFilter()
    local ok, reason
    if languages ~= nil then
        ok, reason = pcall(C_LFGList.Search, FIND_GROUP_CATEGORY_ID, FIND_GROUP_MYTHIC_PLUS_FILTER, baseFilter, languages)
        if ok then
            return true
        end
    end
    ok, reason = pcall(C_LFGList.Search, FIND_GROUP_CATEGORY_ID, FIND_GROUP_MYTHIC_PLUS_FILTER, baseFilter)
    if ok then
        return true
    end
    ok, reason = pcall(C_LFGList.Search, FIND_GROUP_CATEGORY_ID, FIND_GROUP_MYTHIC_PLUS_FILTER)
    return ok, reason
end

function FindGroupSearch.Search(owner)
    local state = GetState(owner)
    if not FindGroupSearch.HasSelectedDungeons(owner) then
        state.lastError = TRANSLATE("FIND_GROUP.ERROR.SELECT_DUNGEON")
        state.searchCompleted = false
        state.searchCompletedEmpty = false
        BumpStateVersion(owner)
        return false, state.lastError
    end
    CancelSearchProcessing(state)
    ClearDetailState(state)
    ClearApplicationStates(owner)
    state.searching = true
    state.processing = false
    state.lastError = nil
    state.lastSearchAt = GetNow()
    state.searchRequestID = (tonumber(state.searchRequestID) or 0) + 1
    state.searchResultsRequestID = nil
    state.results = {}
    state.resultByID = {}
    state.initialDisplayPending = true
    state.initialDisplayReady = false
    state.initialDisplayStartedAt = GetNow()
    state.searchCompleted = false
    state.searchCompletedEmpty = false
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupSearchStarting")

    FindGroupSearch.EnsureEventFrame(owner)
    if not (C_LFGList and C_LFGList.Search) then
        state.searching = false
        state.initialDisplayPending = false
        state.initialDisplayReady = false
        state.lastError = TRANSLATE("FIND_GROUP.ERROR.LFG_UNAVAILABLE")
        BumpStateVersion(owner)
        return false, state.lastError
    end

    local requestID = state.searchRequestID
    ScheduleLater(0, function()
        local currentState = GetState(owner)
        if tonumber(currentState.searchRequestID) ~= tonumber(requestID) or currentState.searching ~= true then
            return
        end
        local ok, reason = RunNativeSearch()
        if not ok then
            currentState.searching = false
            currentState.initialDisplayPending = false
            currentState.initialDisplayReady = false
            currentState.lastError = tostring(reason or TRANSLATE("FIND_GROUP.ERROR.SEARCH_FAILED"))
            BumpStateVersion(owner)
            RefreshFindGroupWindow(owner, "FindGroupSearchFailed")
            return
        end

        RefreshFindGroupWindow(owner, "FindGroupSearch")
    end)
    return true
end

local function GetApplyRoles()
    local specIndex = GetSpecialization and GetSpecialization()
    local role = specIndex and select(5, GetSpecializationInfo(specIndex))
    role = role or "DAMAGER"
    return role == "TANK", role == "HEALER", role == "DAMAGER"
end

local function IsPlayerInFindGroupManagedGroup()
    if CanManageMeetingStoneListing then
        local ok, canManage = pcall(CanManageMeetingStoneListing)
        if ok then
            return canManage ~= true
        end
    end

    if IsInGroup then
        if LE_PARTY_CATEGORY_HOME then
            local okHome, inHome = pcall(IsInGroup, LE_PARTY_CATEGORY_HOME)
            if okHome and inHome then
                return true
            end
        end
        local okAny, inAny = pcall(IsInGroup)
        if okAny and inAny then
            return true
        end
    end

    if GetNumGroupMembers then
        if LE_PARTY_CATEGORY_HOME then
            local okHome, count = pcall(GetNumGroupMembers, LE_PARTY_CATEGORY_HOME)
            if okHome and tonumber(count) and tonumber(count) > 0 then
                return true
            end
        end
        local okAny, count = pcall(GetNumGroupMembers)
        if okAny and tonumber(count) and tonumber(count) > 0 then
            return true
        end
    end

    return false
end

local function IsPlayerFindGroupLeader()
    if CanManageMeetingStoneListing then
        local ok, canManage = pcall(CanManageMeetingStoneListing)
        if ok then
            return canManage == true
        end
    end

    if UnitIsGroupLeader then
        if LE_PARTY_CATEGORY_HOME then
            local okHome, isLeader = pcall(UnitIsGroupLeader, "player", LE_PARTY_CATEGORY_HOME)
            if okHome and isLeader then
                return true
            end
        end
        local okAny, isLeader = pcall(UnitIsGroupLeader, "player")
        if okAny and isLeader then
            return true
        end
    end

    if IsGroupLeader then
        local ok, isLeader = pcall(IsGroupLeader)
        if ok and isLeader then
            return true
        end
    end

    return false
end

local function CanApplyToFindGroupResult()
    if not IsPlayerInFindGroupManagedGroup() then
        return true
    end
    return IsPlayerFindGroupLeader()
end

local function GetHomeGroupMemberCount()
    if not GetNumGroupMembers then
        return 0
    end
    if LE_PARTY_CATEGORY_HOME then
        local ok, count = pcall(GetNumGroupMembers, LE_PARTY_CATEGORY_HOME)
        if ok then
            return tonumber(count) or 0
        end
    end
    local ok, count = pcall(GetNumGroupMembers)
    return ok and tonumber(count) or 0
end

local function ClearApplicationsIfJoinedGroup(owner)
    local states = owner and owner.mkaFindGroupApplicationStates
    if type(states) ~= "table" or next(states) == nil then
        return false
    end

    local baseline = tonumber(owner.mkaFindGroupApplicationGroupMemberCount) or 0
    local currentCount = GetHomeGroupMemberCount()
    if currentCount <= baseline then
        return false
    end

    ClearApplicationStates(owner)
    SortResults(owner)
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupApplicationJoinedGroup")
    return true
end

local function ApplyStoredApplicationStateToResult(owner, resultID, applicationState)
    local state = GetState(owner)
    resultID = tonumber(resultID)
    local result = resultID and state.resultByID and state.resultByID[resultID]
    if not (result and applicationState) then
        return false
    end

    ApplyApplicationStateToResult(
        result,
        applicationState.status,
        applicationState.pendingStatus,
        applicationState.duration,
        applicationState.appliedAt,
        applicationState.updatedAt,
        applicationState.expiresAt
    )
    if applicationState.status ~= "cancelled" then
        SortResults(owner)
    end
    return true
end

local ScheduleCancelledResultRemoval

local function MarkApplicationCancelled(owner, resultID)
    resultID = tonumber(resultID)
    if not (owner and resultID) then
        return nil
    end
    if owner.mkaFindGroupAppliedResultIDs then
        owner.mkaFindGroupAppliedResultIDs[resultID] = nil
    end
    local applicationState = StoreApplicationState(owner, resultID, "cancelled")
    ApplyStoredApplicationStateToResult(owner, resultID, applicationState)
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupApplicationCancelled", tostring(resultID))
    ScheduleCancelledResultRemoval(owner, resultID)
    return applicationState
end

local function RemoveCancelledApplicationResult(owner, resultID, token)
    local state = GetState(owner)
    resultID = tonumber(resultID)
    if not (resultID and owner and owner.mkaFindGroupCancelledRemovalTokens and owner.mkaFindGroupCancelledRemovalTokens[resultID] == token) then
        return false
    end

    local result = state.resultByID and state.resultByID[resultID]
    if not (result and result.applicationStatus == "cancelled") then
        return false
    end

    owner.mkaFindGroupCancelledRemovalTokens[resultID] = nil
    if owner.mkaFindGroupApplicationStates then
        owner.mkaFindGroupApplicationStates[resultID] = nil
    end
    if owner.mkaFindGroupAppliedResultIDs then
        owner.mkaFindGroupAppliedResultIDs[resultID] = nil
    end
    ClearDetailCacheForResult(state, resultID)
    state.resultByID[resultID] = nil
    for index = #(state.results or {}), 1, -1 do
        local current = state.results[index]
        if current and current.resultID == resultID then
            table.remove(state.results, index)
        end
    end
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupCancelledRemoved", tostring(resultID))
    return true
end

ScheduleCancelledResultRemoval = function(owner, resultID)
    resultID = tonumber(resultID)
    if not (owner and resultID) then
        return
    end

    owner.mkaFindGroupCancelledRemovalTokens = owner.mkaFindGroupCancelledRemovalTokens or {}
    local token = (tonumber(owner.mkaFindGroupCancelledRemovalTokens[resultID]) or 0) + 1
    owner.mkaFindGroupCancelledRemovalTokens[resultID] = token
    ScheduleLater(FIND_GROUP_CANCELLED_HIDE_DELAY, function()
        RemoveCancelledApplicationResult(owner, resultID, token)
    end)
end

local function CancelApplicationInternal(owner, resultID)
    resultID = tonumber(resultID)
    local cancelApplication = C_LFGList and (C_LFGList.CancelApplication or C_LFGList.WithdrawApplication)
    if not (resultID and cancelApplication) then
        return false, TRANSLATE("FIND_GROUP.ERROR.CANCEL_UNAVAILABLE")
    end

    local ok, reason = pcall(cancelApplication, resultID)
    if ok then
        MarkApplicationCancelled(owner, resultID)
        return true
    end
    return false, tostring(reason or TRANSLATE("FIND_GROUP.ERROR.CANCEL_FAILED"))
end

local function ProcessApplicationCountdown(owner)
    local state = GetState(owner)
    state.applicationCountdownScheduled = false

    local records = owner and owner.mkaFindGroupApplicationStates
    if type(records) ~= "table" then
        return
    end

    local now = GetNow()
    local hasActiveApplication = false
    for resultID, record in pairs(records) do
        if IsActiveApplicationStatus(record and record.status, record and record.pendingStatus) then
            record.expiresAt = record.expiresAt or GetApplicationExpiresAt(GetApplicationAppliedAt(record, now, record.duration))
            if record.expiresAt and record.expiresAt <= now then
                CancelApplicationInternal(owner, resultID)
            else
                hasActiveApplication = true
            end
        end
    end

    if hasActiveApplication then
        BumpStateVersion(owner)
        RefreshFindGroupWindow(owner, "FindGroupApplicationCountdown")
        ScheduleApplicationCountdown(owner)
    end
end

ScheduleApplicationCountdown = function(owner)
    local state = GetState(owner)
    if state.applicationCountdownScheduled then
        return
    end
    state.applicationCountdownScheduled = true
    ScheduleLater(FIND_GROUP_APPLICATION_TICK_SECONDS, function()
        ProcessApplicationCountdown(owner)
    end)
end

local function MarkApplicationApplied(owner, resultID, refreshSource)
    resultID = tonumber(resultID)
    if not (owner and resultID) then
        return false
    end

    owner.mkaFindGroupAppliedResultIDs = owner.mkaFindGroupAppliedResultIDs or {}
    owner.mkaFindGroupAppliedResultIDs[resultID] = GetNow()
    owner.mkaFindGroupApplicationGroupMemberCount = GetHomeGroupMemberCount()
    local applicationState = StoreApplicationState(owner, resultID, "applied")
    ApplyStoredApplicationStateToResult(owner, resultID, applicationState)
    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, refreshSource or "FindGroupApplicationApplied", tostring(resultID))
    ScheduleApplicationCountdown(owner)
    return true
end

function FindGroupSearch.ApplyToResult(owner, resultID)
    resultID = tonumber(resultID)
    if not (resultID and C_LFGList and C_LFGList.ApplyToGroup) then
        return false, TRANSLATE("FIND_GROUP.ERROR.APPLY_UNAVAILABLE")
    end
    if not CanApplyToFindGroupResult() then
        return false, TRANSLATE("FIND_GROUP.ERROR.APPLY_LEADER_REQUIRED")
    end
    local tank, healer, damager = GetApplyRoles()
    local ok, reason = pcall(C_LFGList.ApplyToGroup, resultID, tank, healer, damager)
    if ok then
        MarkApplicationApplied(owner, resultID, "FindGroupApplicationApplied")
        return true
    end
    return false, tostring(reason or TRANSLATE("FIND_GROUP.ERROR.APPLY_FAILED"))
end

local function UpdateApplicationStatus(owner, resultID, eventStatus)
    resultID = tonumber(resultID)
    if not (owner and resultID) then
        return
    end

    local status, pendingStatus, duration = GetApplicationStatus(resultID)
    status = status or eventStatus
    if IsInactiveApplicationStatus(status) and status ~= "declined" then
        status = "cancelled"
        pendingStatus = nil
    end
    local applicationState = StoreApplicationState(owner, resultID, status, pendingStatus, duration)
        or GetApplicationState(owner, resultID)

    if IsInactiveApplicationStatus(status) and owner.mkaFindGroupAppliedResultIDs then
        owner.mkaFindGroupAppliedResultIDs[resultID] = nil
    end

    local state = GetState(owner)
    local result = state.resultByID and state.resultByID[resultID]
    if result and applicationState then
        ApplyApplicationStateToResult(
            result,
            applicationState.status,
            applicationState.pendingStatus,
            applicationState.duration,
            applicationState.appliedAt,
            applicationState.updatedAt,
            applicationState.expiresAt
        )
        if applicationState.status ~= "cancelled" then
            SortResults(owner)
        end
    end

    BumpStateVersion(owner)
    RefreshFindGroupWindow(owner, "FindGroupApplicationStatus", tostring(resultID))
    if IsActiveApplicationStatus(status, pendingStatus) then
        ScheduleApplicationCountdown(owner)
    elseif status == "cancelled" then
        ScheduleCancelledResultRemoval(owner, resultID)
    end
end

function FindGroupSearch.IsTrackedApplication(owner, resultID)
    local applications = owner and owner.mkaFindGroupAppliedResultIDs
    resultID = tonumber(resultID)
    return resultID and type(applications) == "table" and applications[resultID] ~= nil
end

function FindGroupSearch.CancelApplication(owner, resultID)
    return CancelApplicationInternal(owner, resultID)
end

local function TryClickLFGInviteDialog(owner, dialog)
    local settings = GetProfile(owner) or {}
    if settings.autoJoin ~= true then
        return
    end
    dialog = dialog or _G.LFGListInviteDialog
    local resultID = tonumber(dialog and dialog.resultID)
    if not FindGroupSearch.IsTrackedApplication(owner, resultID) then
        return
    end
    if not (C_LFGList and C_LFGList.GetApplicationInfo) then
        return
    end
    local ok, _, status = pcall(C_LFGList.GetApplicationInfo, resultID)
    if not ok then
        return
    end
    if status == "invited" and dialog.AcceptButton and dialog.AcceptButton.Click then
        C_Timer.After(0, function()
            local currentSettings = GetProfile(owner)
            if currentSettings and currentSettings.autoJoin == true and FindGroupSearch.IsTrackedApplication(owner, resultID) then
                pcall(dialog.AcceptButton.Click, dialog.AcceptButton)
                owner.mkaFindGroupAutoAcceptedAt = GetNow()
            end
        end)
    elseif status == "inviteaccepted" and dialog.AcknowledgeButton and dialog.AcknowledgeButton.Click then
        C_Timer.After(0, function()
            local currentSettings = GetProfile(owner)
            if currentSettings and currentSettings.autoJoin == true and FindGroupSearch.IsTrackedApplication(owner, resultID) then
                pcall(dialog.AcknowledgeButton.Click, dialog.AcknowledgeButton)
                owner.mkaFindGroupAutoAcceptedAt = GetNow()
            end
        end)
    end
end

local function TryAcceptRecentRoleCheck(owner)
    local settings = GetProfile(owner) or {}
    if settings.autoJoin ~= true then
        return
    end
    local acceptedAt = tonumber(owner and owner.mkaFindGroupAutoAcceptedAt)
    if not acceptedAt or GetNow() - acceptedAt > FIND_GROUP_AUTO_ROLE_CHECK_SECONDS then
        return
    end
    if _G.LFDRoleCheckPopupAccept_OnClick then
        pcall(_G.LFDRoleCheckPopupAccept_OnClick)
    elseif _G.LFDRoleCheckPopup and _G.LFDRoleCheckPopup.AcceptButton then
        pcall(_G.LFDRoleCheckPopup.AcceptButton.Click, _G.LFDRoleCheckPopup.AcceptButton)
    end
end

function FindGroupSearch.EnsureAutoJoinHooks(owner)
    if owner.mkaFindGroupAutoJoinHooked then
        return
    end
    owner.mkaFindGroupAutoJoinHooked = true

    local function hookDialog()
        if _G.LFGListInviteDialog and not _G.LFGListInviteDialog.mkaFindGroupHooked then
            _G.LFGListInviteDialog:HookScript("OnShow", function(dialog)
                TryClickLFGInviteDialog(owner, dialog)
            end)
            _G.LFGListInviteDialog.mkaFindGroupHooked = true
        end
        if _G.LFDRoleCheckPopup and not _G.LFDRoleCheckPopup.mkaFindGroupHooked then
            _G.LFDRoleCheckPopup:HookScript("OnShow", function()
                TryAcceptRecentRoleCheck(owner)
            end)
            _G.LFDRoleCheckPopup.mkaFindGroupHooked = true
        end
    end

    hookDialog()
    if EventUtil and EventUtil.ContinueOnAddOnLoaded then
        EventUtil.ContinueOnAddOnLoaded("Blizzard_GroupFinder", hookDialog)
    end
end

local function EnsureGroupFinderLoaded()
    if _G.LFGListApplicationDialog_Show and _G.LFGListApplicationDialog then
        return true
    end
    if C_AddOns and C_AddOns.LoadAddOn then
        pcall(C_AddOns.LoadAddOn, "Blizzard_GroupFinder")
    elseif _G.LoadAddOn then
        pcall(_G.LoadAddOn, "Blizzard_GroupFinder")
    end
    return _G.LFGListApplicationDialog_Show ~= nil and _G.LFGListApplicationDialog ~= nil
end

function FindGroupSearch.EnsureNativeApplicationHooks(owner)
    if not owner then
        return
    end

    local function markNativeApplication(resultID)
        resultID = tonumber(resultID)
        local pendingResultID = tonumber(owner.mkaFindGroupNativeApplicationResultID)
        if not (resultID and pendingResultID and resultID == pendingResultID) then
            return
        end

        owner.mkaFindGroupNativeApplicationResultID = nil
        MarkApplicationApplied(owner, resultID, "FindGroupNativeApplicationApplied")
    end

    local function hookApplyToGroup()
        if owner.mkaFindGroupNativeApplyHooked then
            return
        end
        if C_LFGList and C_LFGList.ApplyToGroup and hooksecurefunc then
            hooksecurefunc(C_LFGList, "ApplyToGroup", function(resultID)
                markNativeApplication(resultID)
            end)
            owner.mkaFindGroupNativeApplyHooked = true
        end
    end

    local function hookApplicationDialog()
        hookApplyToGroup()

        local dialog = _G.LFGListApplicationDialog
        if dialog and not dialog.mkaFindGroupNativeDialogHooked then
            dialog:HookScript("OnHide", function()
                local pendingResultID = tonumber(owner.mkaFindGroupNativeApplicationResultID)
                if pendingResultID then
                    ScheduleLater(0, function()
                        if tonumber(owner.mkaFindGroupNativeApplicationResultID) == pendingResultID then
                            owner.mkaFindGroupNativeApplicationResultID = nil
                        end
                    end)
                end
            end)
            dialog.mkaFindGroupNativeDialogHooked = true
        end

        if _G.LFGListApplicationDialogSignUpButton_OnClick and hooksecurefunc and not owner.mkaFindGroupNativeSignUpHooked then
            hooksecurefunc("LFGListApplicationDialogSignUpButton_OnClick", function(button)
                local dialogFrame = button and button.GetParent and button:GetParent() or _G.LFGListApplicationDialog
                markNativeApplication(dialogFrame and dialogFrame.resultID)
            end)
            owner.mkaFindGroupNativeSignUpHooked = true
        end
    end

    hookApplicationDialog()
    if EventUtil and EventUtil.ContinueOnAddOnLoaded and not owner.mkaFindGroupNativeLoadHooked then
        owner.mkaFindGroupNativeLoadHooked = true
        EventUtil.ContinueOnAddOnLoaded("Blizzard_GroupFinder", hookApplicationDialog)
    end
end

function FindGroupSearch.OpenApplicationDialog(owner, resultID)
    resultID = tonumber(resultID)
    if not (owner and resultID) then
        return false, TRANSLATE("FIND_GROUP.ERROR.APPLY_UNAVAILABLE")
    end
    if not CanApplyToFindGroupResult() then
        return false, TRANSLATE("FIND_GROUP.ERROR.APPLY_LEADER_REQUIRED")
    end
    if not EnsureGroupFinderLoaded() then
        return false, TRANSLATE("FIND_GROUP.ERROR.APPLY_UNAVAILABLE")
    end

    FindGroupSearch.EnsureNativeApplicationHooks(owner)
    owner.mkaFindGroupNativeApplicationResultID = resultID
    local ok, reason = pcall(_G.LFGListApplicationDialog_Show, _G.LFGListApplicationDialog, resultID)
    if ok then
        return true
    end

    owner.mkaFindGroupNativeApplicationResultID = nil
    return false, tostring(reason or TRANSLATE("FIND_GROUP.ERROR.APPLY_FAILED"))
end

function FindGroupSearch.CanApplyToResult()
    return CanApplyToFindGroupResult()
end

function FindGroupSearch.EnsureEventFrame(owner)
    if owner.mkaFindGroupEventFrame then
        FindGroupSearch.EnsureAutoJoinHooks(owner)
        FindGroupSearch.EnsureNativeApplicationHooks(owner)
        return
    end

    local frame = CreateFrame("Frame")
    frame.owner = owner
    frame:RegisterEvent("LFG_LIST_SEARCH_RESULTS_RECEIVED")
    frame:RegisterEvent("LFG_LIST_SEARCH_RESULT_UPDATED")
    frame:RegisterEvent("LFG_LIST_SEARCH_FAILED")
    frame:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
    frame:RegisterEvent("GROUP_ROSTER_UPDATE")
    frame:SetScript("OnEvent", function(eventFrame, event, ...)
        local eventOwner = eventFrame.owner
        if event == "LFG_LIST_SEARCH_RESULTS_RECEIVED" then
            local state = GetState(eventOwner)
            if state.searching == true and IsFindGroupWindowActive(eventOwner) then
                FindGroupSearch.RefreshResults(eventOwner, event)
            end
        elseif event == "LFG_LIST_SEARCH_RESULT_UPDATED" then
            local resultID = ...
            local state = GetState(eventOwner)
            if state.searching ~= true and state.processing ~= true then
                RefreshUpdatedResult(eventOwner, resultID, event)
            end
        elseif event == "LFG_LIST_SEARCH_FAILED" then
            local state = GetState(eventOwner)
            if state.searching == true and IsFindGroupWindowActive(eventOwner) then
                CancelSearchProcessing(state)
                ClearDetailState(state)
                state.searching = false
                state.processing = false
                state.lastError = TRANSLATE("FIND_GROUP.ERROR.SEARCH_FAILED")
                BumpStateVersion(eventOwner)
                RefreshFindGroupWindow(eventOwner, "FindGroupSearchFailed")
            end
        elseif event == "LFG_LIST_APPLICATION_STATUS_UPDATED" then
            local appID, status = ...
            UpdateApplicationStatus(eventOwner, appID, status)
            if status == "invited" or status == "inviteaccepted" then
                TryClickLFGInviteDialog(eventOwner)
            end
            if status == "inviteaccepted" then
                ClearApplicationsIfJoinedGroup(eventOwner)
            end
        elseif event == "GROUP_ROSTER_UPDATE" then
            ClearApplicationsIfJoinedGroup(eventOwner)
        end
    end)
    owner.mkaFindGroupEventFrame = frame
    FindGroupSearch.EnsureAutoJoinHooks(owner)
    FindGroupSearch.EnsureNativeApplicationHooks(owner)
end

function FindGroupSearch.BuildSnapshot(owner)
    local settings = GetProfile(owner) or {}
    local state = GetState(owner)
    local selected = {}
    for activityID, enabled in pairs(settings.selectedActivityIDs or {}) do
        if enabled then
            selected[#selected + 1] = tostring(activityID)
        end
    end
    table.sort(selected)

    local rows = {}
    for _, result in ipairs(state.results or {}) do
        rows[#rows + 1] = table.concat({
            tostring(result.resultID or ""),
            tostring(result.applicationStatus or ""),
            tostring(result.pendingApplicationStatus or ""),
            tostring(FindGroupSearch.GetResultType(result) or ""),
        }, ":")
    end

    return table.concat({
        tostring(state.version or 0),
        table.concat(selected, ","),
        tostring(settings.haveHealer),
        tostring(settings.haveTank),
        tostring(settings.noHealer),
        tostring(settings.noTank),
        tostring(settings.noDps),
        tostring(settings.minAvailableSlots),
        tostring(settings.minScore),
        tostring(settings.searchNewbie),
        tostring(settings.quickJoin),
        tostring(settings.autoJoin),
        tostring(settings.sortKey or ""),
        tostring(settings.sortDirection or ""),
        tostring(state.searching),
        tostring(state.processing),
        tostring(state.newbieSearching),
        table.concat(rows, ","),
    }, "|")
end
