local _, addonTable = ...
---@type MK
local MK = addonTable.MK
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end
local MeetingStoneConstants = core.MeetingStoneConstants or {}
local AccountType = core.AccountType 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 = 8
local FIND_GROUP_DETAIL_RESULT_BATCH_SIZE = 2
local FIND_GROUP_RESULT_BATCH_DELAY = 0.02
local FIND_GROUP_RESULT_REFRESH_INTERVAL = 0.2
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 = {},
    dungeonSelectionInitialized = false,
    haveHealer = false,
    haveTank = false,
    noHealer = false,
    noTank = false,
    noDps = false,
    minAvailableSlots = 1,
    minScore = 0,
    searchText = "",
    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 GetDungeonOptions
local HasSelectedActivities
local ScheduleApplicationCountdown
local ConfirmPendingGroupReport
local ClearNativeSearchFields

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

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

local function IsFindGroupSecretValue(value)
    if not issecretvalue then
        return false
    end
    local ok, secret = pcall(issecretvalue, value)
    return ok and secret == true
end

local mkFindGroupTextReader

local function ReadFindGroupDisplayText(value)
    if not (UIParent and CreateFrame) then
        return "", false
    end
    if not mkFindGroupTextReader then
        local ok, reader = pcall(function()
            local frame = CreateFrame("Frame")
            frame:Hide()
            local fontString = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
            fontString:SetPoint("TOPLEFT", frame, "TOPLEFT")
            return fontString
        end)
        if not ok or not reader then
            return "", false
        end
        mkFindGroupTextReader = reader
    end

    local okSet = pcall(mkFindGroupTextReader.SetText, mkFindGroupTextReader, value)
    if not okSet then
        return "", false
    end
    local okGet, text = pcall(mkFindGroupTextReader.GetText, mkFindGroupTextReader)
    if not okGet or text == nil or IsFindGroupSecretValue(text) then
        return "", false
    end
    local okText, plainText = pcall(tostring, text)
    if not okText or plainText == nil or IsFindGroupSecretValue(plainText) then
        return "", false
    end
    return plainText, true
end

local function ReadFindGroupText(value)
    if value == nil then
        return "", true
    end
    if IsFindGroupSecretValue(value) then
        return ReadFindGroupDisplayText(value)
    end
    local ok, text = pcall(tostring, value)
    if not ok or text == nil or IsFindGroupSecretValue(text) then
        return ReadFindGroupDisplayText(value)
    end
    return text, true
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)
    if target.searchText == nil and target.titleKeyword ~= nil then
        target.searchText = target.titleKeyword
    end
    target.searchText = NormalizeFindGroupSearchText(target.searchText)
    target.titleKeyword = nil
    return target
end

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

local function NormalizeReportCacheText(value)
    local readable
    value, readable = ReadFindGroupText(value)
    if not readable then
        return ""
    end
    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.mkFindGroupState = owner.mkFindGroupState or {
        results = {},
        resultByID = {},
        blockedTitles = {},
        blockedTitleRecords = {},
        version = 0,
    }
    return owner.mkFindGroupState
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.accountQuerySet = nil
    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 CompareRelationshipPriority(left, right)
    local leftRelationshipPriority = GetRelationshipPriority(left)
    local rightRelationshipPriority = GetRelationshipPriority(right)
    if leftRelationshipPriority ~= rightRelationshipPriority then
        return leftRelationshipPriority < rightRelationshipPriority
    end
    return nil
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 relationshipComparison = CompareRelationshipPriority(left, right)
    if relationshipComparison ~= nil then
        return relationshipComparison
    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
        local relationshipComparison = CompareRelationshipPriority(left, right)
        if relationshipComparison ~= nil then
            return relationshipComparison
        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

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

local function EnsureDefaultDungeonSelection(owner, settings)
    if type(settings) ~= "table" then
        return
    end
    if type(settings.selectedActivityIDs) ~= "table" then
        settings.selectedActivityIDs = {}
    end
    if settings.dungeonSelectionInitialized == true then
        return
    end
    if HasSelectedActivities(settings) then
        settings.dungeonSelectionInitialized = true
        return
    end

    local options = GetDungeonOptions(owner)
    if #options <= 0 then
        return
    end
    for _, option in ipairs(options) do
        local activityID = tonumber(option.activityID)
        if activityID then
            settings.selectedActivityIDs[activityID] = true
        end
    end
    settings.dungeonSelectionInitialized = true
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)
    EnsureDefaultDungeonSelection(owner, profile.findGroup)
    return profile.findGroup
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

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

local function HasSelectedDungeonOptions(owner, settings)
    settings = settings or {}
    for _, option in ipairs(GetDungeonOptions(owner)) do
        local activityID = tonumber(option.activityID)
        if activityID and settings.selectedActivityIDs and settings.selectedActivityIDs[activityID] 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.mkFindGroupApplicationStates = type(owner.mkFindGroupApplicationStates) == "table"
        and owner.mkFindGroupApplicationStates
        or {}
    return owner.mkFindGroupApplicationStates
end

local ApplyApplicationStateToResult

local function ClearApplicationStates(owner)
    if not owner then
        return
    end
    owner.mkFindGroupApplicationStates = nil
    owner.mkFindGroupAppliedResultIDs = nil
    owner.mkFindGroupApplicationGroupMemberCount = nil
    owner.mkFindGroupCancelledRemovalTokens = 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.mkFindGroupApplicationStates
    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,
        isLeader = member.isLeader == true,
    }
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 FillEmptyRoleEntries(entries, maxMembers)
    local total = math.min(FIND_GROUP_MAX_MEMBERS, tonumber(maxMembers) or FIND_GROUP_MAX_MEMBERS)
    while #entries < total do
        entries[#entries + 1] = { roleKey = "EMPTY" }
    end
    return entries
end

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

local function BuildMembers(owner, resultID, info)
    local members = {}
    local hasLeaver = false
    local leaderClassFile = nil
    local leaderName, leaderNameReadable = ReadFindGroupText(info and info.leaderName)
    if not leaderNameReadable or leaderName == "" then
        leaderName = nil
    end
    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 owner and owner.UpdateCharacterLFGLeaver then
                owner:UpdateCharacterLFGLeaver(member.name, member.realm, member.isLeaver, "BlizzardLFG:SearchResult")
            end
            local leaderShortName, leaderRealm = SplitFindGroupMemberName(leaderName)
            if member.isLeader or (leaderName and member.name == leaderName) or (leaderShortName and member.name == leaderShortName) then
                member.isLeader = true
                if (type(member.realm) ~= "string" or member.realm == "") and type(leaderRealm) == "string" and leaderRealm ~= "" then
                    member.realm = leaderRealm
                end
                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 GetFindGroupTitleBlockKey(title)
    local text, readable = ReadFindGroupText(title)
    if not readable or text == "" then
        return nil
    end
    return text
end

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

local function GetMemberAccountTypeState(member)
    if not (member and member.name and AccountType.GetAccountState) then
        return nil, nil
    end
    local name, realm = GetMemberAccountTypeQuery(member)
    if not (name and realm) then
        return nil, nil
    end
    local accountState = AccountType.GetAccountState(name, realm, {
        requireRealm = true,
        returnUnavailable = false,
        includeVeteran = false,
    })
    local status = accountState and accountState.status or nil
    if status == "pending" or status == "newbie" or status == "leader" or status == "star" then
        return status, accountState
    end
    return nil, accountState
end

local function GetMemberNewbieState(member)
    local accountTypeState = GetMemberAccountTypeState(member)
    if accountTypeState == "newbie" then
        return true
    end
    if accountTypeState == "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
    local hasAccountTypeLeader = false
    local hasAccountTypeStar = false
    for _, member in ipairs(result.members or {}) do
        local memberState, accountState = GetMemberAccountTypeState(member)
        local _, _, accountKey = GetMemberAccountTypeQuery(member)
        local accountResolved = accountState
            and accountState.status
            and accountState.status ~= "unknown"
            and accountState.status ~= "pending"
        if accountResolved and accountKey and searchState and searchState.accountQuerySet then
            searchState.accountQuerySet[accountKey] = nil
        end

        member.isNewbie = memberState == "newbie"
        member.isAccountTypeLeader = memberState == "leader"
        member.isAccountTypeStar = memberState == "star"
        member.hasPendingNewbie = false
        if memberState == "newbie" then
            hasNewbie = true
        elseif memberState == "leader" then
            hasAccountTypeLeader = true
        elseif memberState == "star" then
            hasAccountTypeStar = true
        elseif memberState == "pending" then
            member.hasPendingNewbie = true
            hasPendingNewbie = true
        else
            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
    result.hasAccountTypeLeader = hasAccountTypeLeader
    result.hasAccountTypeStar = hasAccountTypeStar
    if result.hasLeaver then
        result.resultType = "leaver"
    elseif hasNewbie then
        result.resultType = "newbie"
    elseif hasAccountTypeLeader then
        result.resultType = "leader"
    elseif hasAccountTypeStar then
        result.resultType = "star"
    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.hasAccountTypeLeader = cache.hasAccountTypeLeader == true
    result.hasAccountTypeStar = cache.hasAccountTypeStar == 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,
        hasAccountTypeLeader = result.hasAccountTypeLeader == true,
        hasAccountTypeStar = result.hasAccountTypeStar == 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 leaderName, leaderNameReadable = ReadFindGroupText(info and info.leaderName)
    if not leaderNameReadable or leaderName == "" then
        leaderName = nil
    end
    if owner and owner.IsPlayerBlacklisted and leaderName and owner:IsPlayerBlacklisted(leaderName) then
        return nil
    end

    activityInfo = activityInfo or GetActivityInfo(activityID)
    local dungeonName = option.activityName or activityInfo and (activityInfo.fullName or activityInfo.shortName) or ""
    local score = GetLeaderScore(info)
    local title = info.name or ""
    local titleText, titleReadable = ReadFindGroupText(title)
    local resultTitle = title
    if titleReadable and titleText == "" then
        resultTitle = dungeonName
    end
    if IsTitleBlocked(owner, title) then
        return nil
    end
    local comment = info.comment or ""
    local reportCacheKey = BuildReportCacheKey(title, 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(owner, resultID, info)
        roleEntries = FillEmptyRoleEntries(GetOrderedRoleEntries(members), maxMembers)
    else
        roleEntries = GetRoleEntriesFromCounts(counts, maxMembers)
    end

    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 = resultTitle,
        dungeonName = dungeonName,
        comment = comment,
        leaderName = leaderName,
        leaderDisplayName = GetDisplayName(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,
        hasAccountTypeLeader = false,
        hasAccountTypeStar = 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 function QueueAccountTypesForResult(owner, state, token, result)
    if not (state and AccountType.QueueQueries) then
        return false
    end

    state.accountQuerySet = state.accountQuerySet or {}
    local queryEntries = {}
    local entryKeys = {}
    local awaitingQueuedName = false
    for _, member in ipairs(result and result.members or {}) do
        local name, realm, accountKey = GetMemberAccountTypeQuery(member)
        if name and realm and accountKey and not member.isLeaver then
            if state.accountQuerySet[accountKey] ~= true then
                queryEntries[#queryEntries + 1] = {
                    name = name,
                    realm = realm,
                }
                entryKeys[#entryKeys + 1] = accountKey
            else
                awaitingQueuedName = true
            end
        end
    end

    local queuedCount, queuedKeys, _, waitingKeys = AccountType.QueueQueries(queryEntries, {
        requireRealm = true,
        includeVeteran = false,
    })
    awaitingQueuedName = awaitingQueuedName or queuedCount > 0
    for _, accountKey in ipairs(entryKeys) do
        if (queuedKeys and queuedKeys[accountKey] == true) or (waitingKeys and waitingKeys[accountKey] == true) then
            state.accountQuerySet[accountKey] = true
            awaitingQueuedName = true
        else
            state.accountQuerySet[accountKey] = nil
        end
    end

    return 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(owner, resultID, {
        leaderName = result.leaderName,
        numMembers = result.totalMembers,
    })
    result.members = members
    result.roleEntries = FillEmptyRoleEntries(GetOrderedRoleEntries(members), result.maxMembers)
    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(FIND_GROUP_RESULT_BATCH_DELAY, 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, HasSelectedDungeonOptions(owner, 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("未选择副本")
    end
    if optionCount > 0 and selectedCount >= optionCount then
        return TRANSLATE("全部钥石副本")
    end
    if selectedCount == 1 then
        return selectedName or TRANSLATE("已选 1 个副本")
    end
    return string.format(TRANSLATE("已选 %d 个副本"), selectedCount)
end

function FindGroupSearch.HasSelectedDungeons(owner)
    return HasSelectedDungeonOptions(owner, 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.dungeonSelectionInitialized = true
    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
    settings.dungeonSelectionInitialized = true
    wipe(settings.selectedActivityIDs)
    BumpStateVersion(owner)
end

function FindGroupSearch.SelectAllDungeons(owner)
    local settings = GetProfile(owner)
    if not settings then
        return
    end
    settings.dungeonSelectionInitialized = true
    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.SetSearchTextFilter(owner, value)
    local settings = GetProfile(owner)
    if not settings then
        return false
    end

    local searchText = NormalizeFindGroupSearchText(value)
    if settings.searchText == searchText then
        return false
    end

    settings.searchText = searchText
    BumpStateVersion(owner)
    return true
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({
        dungeonSelectionInitialized = true,
        quickJoin = previous.quickJoin == true,
        autoJoin = previous.autoJoin == true,
        reportBlockedResults = type(previous.reportBlockedResults) == "table" and previous.reportBlockedResults or {},
    })
    if ClearNativeSearchFields then
        ClearNativeSearchFields()
    end
    local state = GetState(owner)
    CancelSearchProcessing(state)
    ClearDetailState(state)
    ClearApplicationStates(owner)
    state.results = {}
    state.resultByID = {}
    state.activeSearchText = nil
    state.blockedTitles = {}
    state.blockedTitleRecords = {}
    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, result)
    local titleKey = GetFindGroupTitleBlockKey(title)
    if not titleKey then
        return
    end
    local state = GetState(owner)
    state.blockedTitles[titleKey] = true
    state.blockedTitleRecords = type(state.blockedTitleRecords) == "table" and state.blockedTitleRecords or {}
    state.blockedTitleRecords[titleKey] = {
        title = title,
        leaderName = type(result) == "table" and result.leaderName or nil,
        blockedAt = GetNow(),
    }
    if owner and owner.AddPlayerToBlacklist and type(result) == "table" and result.leaderName then
        owner:AddPlayerToBlacklist(
            result.leaderName,
            nil,
            owner.GetBlacklistNoteSameTitleAdvertisement and owner:GetBlacklistNoteSameTitleAdvertisement() or "同标题广告传染",
            "findgroup_block_title",
            true
        )
    end
    for index = #(state.results or {}), 1, -1 do
        local result = state.results[index]
        if result and GetFindGroupTitleBlockKey(result.title) == titleKey then
            ClearDetailCacheForResult(state, result.resultID)
            state.resultByID[result.resultID] = nil
            table.remove(state.results, index)
        end
    end
    if FindGroupSearch.RefreshBlacklistFilter then
        FindGroupSearch.RefreshBlacklistFilter(owner)
    end
    BumpStateVersion(owner)
end

local function AddFindGroupNameMatchKey(keys, value)
    local normalized = NormalizeReportCacheText(value)
    if normalized == "" then
        return
    end

    keys[normalized] = true
    local shortName = normalized:match("^([^-]+)%-")
    if shortName and shortName ~= "" then
        keys[shortName] = true
    end
end

local function BuildBlacklistRecordNameKeys(record)
    local keys = {}
    if type(record) ~= "table" then
        return keys
    end

    AddFindGroupNameMatchKey(keys, record.displayName)
    AddFindGroupNameMatchKey(keys, record.name)
    if record.name and record.realm then
        AddFindGroupNameMatchKey(keys, string.format("%s-%s", record.name, record.realm))
    end

    return keys
end

local function FindGroupNameMatchesBlacklistRecord(value, nameKeys)
    if type(nameKeys) ~= "table" then
        return false
    end

    local normalized = NormalizeReportCacheText(value)
    if normalized == "" then
        return false
    end
    if nameKeys[normalized] then
        return true
    end

    local shortName = normalized:match("^([^-]+)%-")
    return shortName and nameKeys[shortName] == true
end

local function ClearReportBlockedResultsByBlacklistRecord(owner, nameKeys)
    local blockedResults = GetReportBlockedResults(owner)
    if type(blockedResults) ~= "table" then
        return false
    end

    local changed = false
    for cacheKey, record in pairs(blockedResults) do
        if type(record) == "table" and FindGroupNameMatchesBlacklistRecord(record.leaderName, nameKeys) then
            blockedResults[cacheKey] = nil
            changed = true
        end
    end
    return changed
end

local function ClearBlockedTitlesByBlacklistRecord(owner, nameKeys)
    local state = GetState(owner)
    local blockedTitles = type(state.blockedTitles) == "table" and state.blockedTitles or nil
    local blockedTitleRecords = type(state.blockedTitleRecords) == "table" and state.blockedTitleRecords or nil
    if not (blockedTitles and blockedTitleRecords) then
        return false
    end

    local changed = false
    for titleKey, record in pairs(blockedTitleRecords) do
        if blockedTitles[titleKey] == true and FindGroupNameMatchesBlacklistRecord(record and record.leaderName, nameKeys) then
            blockedTitles[titleKey] = nil
            blockedTitleRecords[titleKey] = nil
            changed = true
        end
    end
    return changed
end

function FindGroupSearch.ClearBlacklistLinkedFilters(owner, record)
    if type(record) ~= "table" then
        return false
    end

    local source = record.source
    local clearReportCache = source == "findgroup_report_ad" or source == "report_ad"
    local clearTitleBlocks = source == "findgroup_block_title" or source == "block_title"
    if not clearReportCache and not clearTitleBlocks then
        return false
    end

    local nameKeys = BuildBlacklistRecordNameKeys(record)
    if not next(nameKeys) then
        return false
    end

    local changed = false
    if clearReportCache then
        changed = ClearReportBlockedResultsByBlacklistRecord(owner, nameKeys) or changed
    end
    if clearTitleBlocks then
        changed = ClearBlockedTitlesByBlacklistRecord(owner, nameKeys) or changed
    end

    if changed then
        BumpStateVersion(owner)
        RefreshFindGroupWindow(owner, "FindGroupBlacklistLinkedFiltersCleared", record.displayName or record.name or "")
    end
    return changed
end

function FindGroupSearch.RefreshBlacklistFilter(owner)
    local state = GetState(owner)
    if not (owner and owner.IsPlayerBlacklisted and state and state.results) then
        return false
    end

    local changed = false
    for index = #(state.results or {}), 1, -1 do
        local result = state.results[index]
        if result and result.leaderName and owner:IsPlayerBlacklisted(result.leaderName) then
            ClearDetailCacheForResult(state, result.resultID)
            if state.resultByID and result.resultID then
                state.resultByID[result.resultID] = nil
            end
            table.remove(state.results, index)
            changed = true
        end
    end

    if changed then
        BumpStateVersion(owner)
    end
    return changed
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.mkFindGroupPendingGroupReport
    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.mkFindGroupPendingGroupReport = nil
        return false
    end

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

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

local function EnsureReportHooks(owner)
    if not owner or owner.mkFindGroupReportHooksInstalled then
        return
    end
    owner.mkFindGroupReportHooksInstalled = 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.mkFindGroupPendingGroupReport
        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.mkFindGroupReportHideHooked then
                frame.mkFindGroupReportHideHooked = true
                frame:HookScript("OnHide", function()
                    local current = owner.mkFindGroupPendingGroupReport
                    local token = current and current.pendingToken
                    if not token then
                        return
                    end
                    ScheduleLater(0.1, function()
                        local latest = owner.mkFindGroupPendingGroupReport
                        if latest and latest.pendingToken == token then
                            owner.mkFindGroupPendingGroupReport = 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 owner and owner.AddPlayerToBlacklist and result and result.leaderName then
        owner:AddPlayerToBlacklist(
            result.leaderName,
            nil,
            owner.GetBlacklistNoteAdvertisement and owner:GetBlacklistNoteAdvertisement() or "广告",
            "findgroup_report_ad",
            true
        )
        if FindGroupSearch.RefreshBlacklistFilter then
            FindGroupSearch.RefreshBlacklistFilter(owner)
        end
    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.mkFindGroupPendingReportToken) or 0) + 1
    owner.mkFindGroupPendingReportToken = record.pendingToken
    owner.mkFindGroupPendingGroupReport = record
    local ok = pcall(LFGList_ReportListing, result.resultID, result.leaderName)
    if not ok then
        owner.mkFindGroupPendingGroupReport = nil
        return false
    end
    HookPendingReportDialog(owner)
    return true
end

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

function FindGroupSearch.RefreshAccountTypeStates(owner)
    local state = GetState(owner)
    local settings = GetProfile(owner) or {}
    local changed = false

    local function refreshResult(result)
        if not (result and result.detailLoaded == true) then
            return
        end

        local previousType = result.resultType
        local previousHasNewbie = result.hasNewbie == true
        local previousPendingNewbie = result.hasPendingNewbie == true
        local previousHasAccountTypeLeader = result.hasAccountTypeLeader == true
        local previousHasAccountTypeStar = result.hasAccountTypeStar == true
        RefreshResultType(result, state)
        StoreResultDetails(state, result)
        if previousType ~= result.resultType
            or previousHasNewbie ~= (result.hasNewbie == true)
            or previousPendingNewbie ~= (result.hasPendingNewbie == true)
            or previousHasAccountTypeLeader ~= (result.hasAccountTypeLeader == true)
            or previousHasAccountTypeStar ~= (result.hasAccountTypeStar == true)
        then
            changed = true
        end
    end

    for _, result in ipairs(state.results or {}) do
        refreshResult(result)
    end
    for _, result in ipairs(state.newbiePendingResults or {}) do
        refreshResult(result)
    end

    if settings.searchNewbie == true then
        local filteredResults = {}
        local filteredByID = {}
        for _, result in ipairs(state.results or {}) do
            if result and result.hasNewbie == true then
                filteredResults[#filteredResults + 1] = result
                if result.resultID then
                    filteredByID[result.resultID] = result
                end
            else
                changed = true
            end
        end
        state.results = filteredResults
        state.resultByID = filteredByID
    end

    if changed then
        SortResults(owner)
        BumpStateVersion(owner)
    end

    return changed
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)
    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("队伍查找器接口不可用")
        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("队伍搜索失败。")
        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 = HasSelectedDungeonOptions(owner, 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 GetAdvancedSearchFilter()
    if C_LFGList and C_LFGList.GetAdvancedFilter then
        local ok, advancedFilter = pcall(C_LFGList.GetAdvancedFilter)
        if ok then
            return advancedFilter
        end
    end
    return nil
end

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

local function RunNativeSearch(owner)
    local baseFilter = Enum and Enum.LFGListFilter and Enum.LFGListFilter.PvE or 4
    local state = GetState(owner)
    local useNativeSearchText = state and state.nativeSearchTextActive == true
    if not useNativeSearchText then
        ClearNativeSearchFields()
    end
    local languages = GetLanguageSearchFilter()
    local ok, reason
    if useNativeSearchText and 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
    if useNativeSearchText then
        ok, reason = pcall(C_LFGList.Search, FIND_GROUP_CATEGORY_ID, FIND_GROUP_MYTHIC_PLUS_FILTER, baseFilter)
        if ok then
            return true
        end
    end
    if languages ~= nil then
        local advancedFilter = GetAdvancedSearchFilter()
        ok, reason = pcall(C_LFGList.Search, FIND_GROUP_CATEGORY_ID, FIND_GROUP_MYTHIC_PLUS_FILTER, baseFilter, languages, nil, advancedFilter)
        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)
    local settings = GetProfile(owner) or {}
    if not FindGroupSearch.HasSelectedDungeons(owner) then
        state.lastError = TRANSLATE("请先选择副本")
        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.activeSearchText = NormalizeFindGroupSearchText(settings.searchText)
    state.nativeSearchTextActive = state.activeSearchText ~= ""
        and owner
        and owner.widgets
        and owner.widgets.findGroupSearchTextBox
        and owner.widgets.findGroupSearchTextBox.mkFindGroupUsesNativeSearchBox == true
    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("队伍查找器接口不可用")
        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(owner)
        if not ok then
            currentState.searching = false
            currentState.initialDisplayPending = false
            currentState.initialDisplayReady = false
            currentState.lastError = tostring(reason or TRANSLATE("队伍搜索失败。"))
            BumpStateVersion(owner)
            RefreshFindGroupWindow(owner, "FindGroupSearchFailed")
            return
        end

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

local function GetApplyRoles()
    if GetLFGRoles then
        local ok, _, tank, healer, damager = pcall(GetLFGRoles)
        if ok then
            return tank == true, healer == true, damager == true
        end
    end
    return false, false, false
end

local function HasApplyRolesSelected()
    local tank, healer, damager = GetApplyRoles()
    return tank or healer or 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.mkFindGroupApplicationStates
    if type(states) ~= "table" or next(states) == nil then
        return false
    end

    local baseline = tonumber(owner.mkFindGroupApplicationGroupMemberCount) 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.mkFindGroupAppliedResultIDs then
        owner.mkFindGroupAppliedResultIDs[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.mkFindGroupCancelledRemovalTokens and owner.mkFindGroupCancelledRemovalTokens[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.mkFindGroupCancelledRemovalTokens[resultID] = nil
    if owner.mkFindGroupApplicationStates then
        owner.mkFindGroupApplicationStates[resultID] = nil
    end
    if owner.mkFindGroupAppliedResultIDs then
        owner.mkFindGroupAppliedResultIDs[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.mkFindGroupCancelledRemovalTokens = owner.mkFindGroupCancelledRemovalTokens or {}
    local token = (tonumber(owner.mkFindGroupCancelledRemovalTokens[resultID]) or 0) + 1
    owner.mkFindGroupCancelledRemovalTokens[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("取消申请接口不可用。")
    end

    local ok, reason = pcall(cancelApplication, resultID)
    if ok then
        MarkApplicationCancelled(owner, resultID)
        return true
    end
    return false, tostring(reason or TRANSLATE("取消申请失败。"))
end

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

    local records = owner and owner.mkFindGroupApplicationStates
    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.mkFindGroupAppliedResultIDs = owner.mkFindGroupAppliedResultIDs or {}
    owner.mkFindGroupAppliedResultIDs[resultID] = GetNow()
    owner.mkFindGroupApplicationGroupMemberCount = 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("申请接口不可用。")
    end
    if not CanApplyToFindGroupResult() then
        return false, TRANSLATE("只有队长可以申请队伍。")
    end
    local tank, healer, damager = GetApplyRoles()
    if not (tank or healer or damager) then
        return false, TRANSLATE("请先在队伍查找器中选择申请职责。")
    end
    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("申请加入失败。"))
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.mkFindGroupAppliedResultIDs then
        owner.mkFindGroupAppliedResultIDs[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.mkFindGroupAppliedResultIDs
    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 IsLFGListRoleCheckActive()
    if not (C_LFGList and C_LFGList.GetRoleCheckInfo) then
        return false
    end
    local ok, isLFGList = pcall(C_LFGList.GetRoleCheckInfo)
    return ok and isLFGList == true
end

local function ShouldAutoHandleLFGApplication(owner, resultID)
    return FindGroupSearch.IsTrackedApplication(owner, resultID) == true
end

local function SetRoleCheckButtonChecked(button, checked)
    if not button then
        return
    end
    if LFGRole_SetChecked then
        pcall(LFGRole_SetChecked, button, checked == true)
        return
    end
    local checkButton = button.checkButton or button.CheckButton
    if checkButton and checkButton.SetChecked then
        pcall(checkButton.SetChecked, checkButton, checked == true)
    end
end

local function ApplySelectedRolesToRoleCheckPopup()
    local tank, healer, damager = GetApplyRoles()
    if not (tank or healer or damager) then
        return false
    end

    if GetLFGRoles and SetLFGRoles then
        local ok, leader = pcall(GetLFGRoles)
        if ok then
            pcall(SetLFGRoles, leader == true, tank == true, healer == true, damager == true)
        end
    end

    SetRoleCheckButtonChecked(_G.LFDRoleCheckPopupRoleButtonTank, tank)
    SetRoleCheckButtonChecked(_G.LFDRoleCheckPopupRoleButtonHealer, healer)
    SetRoleCheckButtonChecked(_G.LFDRoleCheckPopupRoleButtonDPS, damager)
    if _G.LFDRoleCheckPopup_UpdateAcceptButton then
        pcall(_G.LFDRoleCheckPopup_UpdateAcceptButton)
    end
    return true
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 ShouldAutoHandleLFGApplication(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 ShouldAutoHandleLFGApplication(owner, resultID) then
                pcall(dialog.AcceptButton.Click, dialog.AcceptButton)
                owner.mkFindGroupAutoAcceptedAt = 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 ShouldAutoHandleLFGApplication(owner, resultID) then
                pcall(dialog.AcknowledgeButton.Click, dialog.AcknowledgeButton)
                owner.mkFindGroupAutoAcceptedAt = 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.mkFindGroupAutoAcceptedAt)
    local shouldAcceptRecentInvite = acceptedAt and GetNow() - acceptedAt <= FIND_GROUP_AUTO_ROLE_CHECK_SECONDS
    if not shouldAcceptRecentInvite or not IsLFGListRoleCheckActive() then
        return
    end
    if not ApplySelectedRolesToRoleCheckPopup() 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

local function ScheduleAutoAcceptRoleCheck(owner)
    if not owner then
        return
    end
    owner.mkFindGroupAutoRoleCheckTicket = (tonumber(owner.mkFindGroupAutoRoleCheckTicket) or 0) + 1
    local ticket = owner.mkFindGroupAutoRoleCheckTicket
    ScheduleLater(0, function()
        if owner.mkFindGroupAutoRoleCheckTicket == ticket then
            TryAcceptRecentRoleCheck(owner)
        end
    end)
end

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

    local function hookDialog()
        if _G.LFGListInviteDialog and not _G.LFGListInviteDialog.mkFindGroupHooked then
            _G.LFGListInviteDialog:HookScript("OnShow", function(dialog)
                TryClickLFGInviteDialog(owner, dialog)
            end)
            _G.LFGListInviteDialog.mkFindGroupHooked = true
        end
        if _G.LFDRoleCheckPopup and not _G.LFDRoleCheckPopup.mkFindGroupHooked then
            _G.LFDRoleCheckPopup:HookScript("OnShow", function()
                TryAcceptRecentRoleCheck(owner)
            end)
            _G.LFDRoleCheckPopup.mkFindGroupHooked = 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.mkFindGroupNativeApplicationResultID)
        if not (resultID and pendingResultID and resultID == pendingResultID) then
            return
        end

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

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

    local function hookApplicationDialog()
        hookApplyToGroup()

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

        if _G.LFGListApplicationDialogSignUpButton_OnClick and hooksecurefunc and not owner.mkFindGroupNativeSignUpHooked 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.mkFindGroupNativeSignUpHooked = true
        end
    end

    hookApplicationDialog()
    if EventUtil and EventUtil.ContinueOnAddOnLoaded and not owner.mkFindGroupNativeLoadHooked then
        owner.mkFindGroupNativeLoadHooked = 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("申请接口不可用。")
    end
    if not CanApplyToFindGroupResult() then
        return false, TRANSLATE("只有队长可以申请队伍。")
    end
    if not EnsureGroupFinderLoaded() then
        return false, TRANSLATE("申请接口不可用。")
    end

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

    owner.mkFindGroupNativeApplicationResultID = nil
    return false, tostring(reason or TRANSLATE("申请加入失败。"))
end

function FindGroupSearch.CanApplyToResult()
    return CanApplyToFindGroupResult()
end

function FindGroupSearch.EnsureEventFrame(owner)
    if owner.mkFindGroupEventFrame 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("LFG_ROLE_CHECK_SHOW")
    frame:RegisterEvent("LFG_ROLE_CHECK_UPDATE")
    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("队伍搜索失败。")
                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 == "LFG_ROLE_CHECK_SHOW" then
            ScheduleAutoAcceptRoleCheck(eventOwner)
        elseif event == "LFG_ROLE_CHECK_UPDATE" then
            local acceptedAt = tonumber(eventOwner and eventOwner.mkFindGroupAutoAcceptedAt)
            if acceptedAt and GetNow() - acceptedAt <= FIND_GROUP_AUTO_ROLE_CHECK_SECONDS and IsLFGListRoleCheckActive() and HasApplyRolesSelected() then
                ApplySelectedRolesToRoleCheckPopup()
            end
        elseif event == "GROUP_ROSTER_UPDATE" then
            ClearApplicationsIfJoinedGroup(eventOwner)
        end
    end)
    owner.mkFindGroupEventFrame = frame
    FindGroupSearch.EnsureAutoJoinHooks(owner)
    FindGroupSearch.EnsureNativeApplicationHooks(owner)
end

function FindGroupSearch.BuildSnapshot(owner)
    local settings = GetProfile(owner) or {}
    local state = GetState(owner)
    local memberDisplayMode = owner and owner.GetFindGroupMemberDisplayMode and owner:GetFindGroupMemberDisplayMode() or "role"
    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),
        tostring(memberDisplayMode),
        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(settings.searchText or ""),
        tostring(state.searching),
        tostring(state.processing),
        tostring(state.newbieSearching),
        table.concat(rows, ","),
    }, "|")
end
