GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.GratsUService = addon.GratsUService or {}

local MAX_MESSAGES = 50
local PEER_MAX_AGE_SECONDS = 180
local PRESENCE_INTERVAL_SECONDS = 60
local ROSTER_PROCESS_DELAY_SECONDS = 1
local ROSTER_PROCESS_MIN_INTERVAL_SECONDS = 10
local STARTUP_GRACE_SECONDS = 5
local SAMPLE_PREVIEW_NAME = "Guildmate"
local WELCOME_FALLBACK_DELAY_SECONDS = 3
local SEND_ANNOUNCEMENT_SEPARATOR = "\31"

local DEFAULT_ACHIEVEMENT_MESSAGES = {
    "Grats! Did you just break the game?",
    "Achievement unlocked: Making us all look bad!",
    "Congrats! Now go touch some grass!",
    "You did it! The guild is proud (and slightly jealous).",
    "Grats! Is that your final form?",
    "Achievement unlocked: Professional button presser!",
    "Grats! Now teach the rest of us!",
    "You win! The rest of us are still loading...",
    "Grats! The RNG gods smile upon you.",
    "Achievement unlocked: Guild MVP!",
    "Grats! Screenshot it before it wears off!",
    "Nice! That tooltip never stood a chance.",
    "Grats! You have unlocked +1 swagger.",
    "A wild achievement appears! You used Grats. It is super effective!",
    "Grats! Your carry application has been approved.",
    "Congrats! Loot council is now watching closely.",
    "Achievement unlocked: Keyboard acrobatics master.",
    "Grats! Your mount is jealous of your speed.",
    "Certified epic moment. Well played!",
    "Grats! Tell us your secrets. Asking for a guildmate.",
    "Your DPS and your achievements are both cracked. Grats!",
    "Grats! Did the boss even get to read the mechanics?",
    "Achievement unlocked: Button mashing with precision.",
    "Big grats! Now do it again but blindfolded.",
    "Grats! The repair bill was worth it.",
    "Achievement unlocked: Professional line-of-sight enjoyer.",
    "Grats! The meter is quaking.",
    "Congrats! That is a sparkle-worthy moment.",
    "Grats! You have been promoted to Designated Show-Off.",
    "Achievement unlocked: Making the boss reconsider life choices.",
    "Grats! That is what we call peak gaming.",
    "Congrats! Now teach my alt to do that.",
    "Grats! The tooltip gasped a little.",
    "Achievement unlocked: Pulling without dying, mostly.",
    "Grats! You just gained +10 guild street cred.",
    "Congrats! The loot pinata has noticed you.",
    "Grats! You are now legally required to flex in guild chat.",
    "Achievement unlocked: Mechanic dodger extraordinaire.",
    "Grats! That is a certified shiny moment.",
    "Congrats! The instance bosses filed a complaint.",
    "Grats! Your keyboard approves this message.",
    "Achievement unlocked: Panic healed at 1 percent.",
    "Grats! You have ascended to Gamer with receipts.",
    "Congrats! RNG finally paid their debts.",
    "Grats! The raid llamas nod in approval.",
    "Achievement unlocked: Knower of which button to press.",
    "Grats! Add it to the montage.",
    "Congrats! You have unlocked Hard Stuck No More.",
    "Grats! That is a chef's kiss moment.",
}

local DEFAULT_WELCOME_MESSAGES = {
    "Welcome back! The party can finally start!",
    "Hey, look who logged in!",
    "Welcome! Did you bring snacks?",
    "The guild just got 10 percent cooler!",
    "Welcome! Time to carry us!",
    "Welcome! The fun has arrived!",
    "Welcome! Ready to wipe with us?",
    "Welcome! Hope you brought your lucky socks!",
    "Welcome! The adventure resumes!",
    "Welcome! The guild missed you!",
    "Welcome back! We kept a chair warm. It is a little on fire.",
    "Yo! Log in detected. Hype levels rising.",
    "Welcome! May your loot be shiny and your pulls be kind.",
    "You are here! Queue the victory music.",
    "Welcome! We totally did not pull without you, probably.",
    "Greetings! The mobs have been asking for you by name.",
    "Welcome! Your key just became +2 easier.",
    "They are back! Everyone act natural.",
    "Welcome! Hope your repair bill is ready for today.",
    "Welcome! Your transmog looks immaculate as always.",
    "Welcome back! We saved you a lucky roll.",
    "Welcome! The raid needs your sparkle.",
    "Hail, traveler! Got snacks and summons?",
    "Welcome! Your login buff just procced morale +10.",
    "Hey! The party bar just turned green.",
    "Welcome! We believe in your cooldowns.",
    "Welcome back! The bosses are nervous again.",
    "Sup! Ready to gaming?",
    "Welcome! Pulling in 3, 2, just kidding. Unless?",
    "Welcome! Do not feed the adds.",
    "Welcome back! The AFK detector is watching.",
    "Welcome! We have been practicing our wipes without you.",
    "Oh hey, it is you! Time to carry the carry.",
    "Welcome! Your alt army salutes you.",
    "Welcome! We promise not to ninja your coffee.",
    "Welcome back! Your lucky socks better be clean.",
    "Welcome! May your queues be short and your loot be fat.",
    "Welcome back! The guild chat chaos resumes.",
    "Welcome! The mount you want just went on break.",
    "Welcome! The one-pull clear is totally happening today.",
    "Welcome back! We have missed your combat log.",
    "Welcome! Someone say key, raid, or dance party?",
    "Welcome! You are just in time for morally questionable pulls.",
    "Welcome back! Your presence increases drop rates by 0.001 percent.",
    "Welcome! The world is saved. Again. Probably.",
    "Welcome! Time to make the meters do weird things.",
    "Welcome back! We brought extra bandages.",
    "Welcome! We have got portals, puns, and problems.",
}

local function trimString(value)
    return tostring(value or ""):match("^%s*(.-)%s*$") or ""
end

local function copyList(source)
    local copy = {}

    for index, value in ipairs(source or {}) do
        copy[index] = value
    end

    return copy
end

local function normalizeKind(kind)
    return string.lower(trimString(kind)) == "welcome" and "welcome" or "achievement"
end

local function clampNumber(value, fallback)
    local numeric = tonumber(value)

    if not numeric or numeric < 0 then
        return fallback
    end

    return math.floor(numeric + 0.5)
end

local function sendAddonMessage(prefix, payload, channel)
    if C_ChatInfo and C_ChatInfo.SendAddonMessage then
        local ok, result = pcall(C_ChatInfo.SendAddonMessage, prefix, payload, channel)

        if ok then
            return result
        end

        local parameterSets = {
            { prefix = prefix, message = payload, chatType = channel },
            { prefix = prefix, text = payload, chatType = channel },
            { prefix = prefix, message = payload, distribution = channel },
            { prefix = prefix, text = payload, distribution = channel },
        }

        for _, params in ipairs(parameterSets) do
            local tableOk, tableResult = pcall(C_ChatInfo.SendAddonMessage, params)

            if tableOk then
                return tableResult
            end
        end

        return nil
    end

    if SendAddonMessage then
        return SendAddonMessage(prefix, payload, channel)
    end

    return nil
end

local function requestGuildRoster()
    if C_GuildInfo and C_GuildInfo.GuildRoster then
        return C_GuildInfo.GuildRoster()
    end

    if GuildRoster then
        return GuildRoster()
    end

    return nil
end

local function canEditOfficerNote()
    if C_GuildInfo and C_GuildInfo.CanEditOfficerNote then
        return C_GuildInfo.CanEditOfficerNote()
    end

    if CanEditOfficerNote then
        return CanEditOfficerNote()
    end

    return false
end

local function sendGuild(text)
    if addon.GratsUService:IsPaused() or not text or text == "" or not IsInGuild or not IsInGuild() then
        return false
    end

    SendChatMessage(text, "GUILD")
    return true
end

local function baseName(fullName)
    if not fullName then
        return nil
    end

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

local function normalizePeerName(fullName)
    local normalized = addon.Store and addon.Store.NormalizeFullName and addon.Store:NormalizeFullName(fullName) or trimString(fullName)

    if normalized == "" then
        return nil
    end

    return normalized
end

local function sanitizeMessageText(text)
    local sanitized = trimString(text)

    if sanitized == "" then
        return nil
    end

    return sanitized:sub(1, 240)
end

local function buildEventKey(prefix, name)
    return prefix .. ":" .. trimString(name or "?")
end

local function buildSendAnnouncement(kind, key)
    return table.concat({
        "SENT",
        normalizeKind(kind),
        trimString(key or "?"),
    }, SEND_ANNOUNCEMENT_SEPARATOR)
end

local function parseSendAnnouncement(message)
    local op, kind, key = strsplit(SEND_ANNOUNCEMENT_SEPARATOR, tostring(message or ""), 3)

    if op ~= "SENT" then
        return nil, nil
    end

    local memberKey = baseName(key)

    if not memberKey or memberKey == "" then
        return nil, nil
    end

    return normalizeKind(kind), memberKey
end

local function announceSend(kind, key)
    sendAddonMessage(addon.GratsUService:GetPrefix(), buildSendAnnouncement(kind, key), "GUILD")
end

local function sendWelcomeMessage(key)
    local message = addon.GratsUService:GetRandomMessage("welcome")

    if message and sendGuild(addon.GratsUService:FormatGuildMessage(key, message)) then
        local runtime = addon.GratsUService:GetRuntime()
        local sentAt = GetTime()

        runtime.lastWelcome[key] = sentAt
        addon.GratsUService:RecordSend("welcome")
        announceSend("welcome", key)
        return true
    end

    return false
end

function addon.GratsUService:GetPrefix()
    return "GH3GRATS"
end

function addon.GratsUService:GetDefaultMessages(kind)
    if normalizeKind(kind) == "welcome" then
        return copyList(DEFAULT_WELCOME_MESSAGES)
    end

    return copyList(DEFAULT_ACHIEVEMENT_MESSAGES)
end

function addon.GratsUService:GetDefaultSettings()
    return {
        antiSpamSeconds = 300,
        ignoreSelf = true,
        enableWelcome = true,
        enableAchievement = true,
        minOfflineSeconds = 120,
        achievementCooldownSeconds = 120,
        welcomeDelaySeconds = 20,
    }
end

function addon.GratsUService:GetState()
    return addon.Store:GetProfile().gratsU
end

function addon.GratsUService:GetRuntime()
    self.runtime = self.runtime or {
        addonPeersTime = {},
        haveSeenOffline = {},
        lastAchieveCongrats = {},
        lastRosterProcessedAt = 0,
        lastWelcome = {},
        offlineSince = {},
        onlineMembers = {},
        pendingWelcomeTimers = {},
        presenceTicker = nil,
        rosterInitialized = false,
        rosterProcessGeneration = 0,
        rosterProcessPending = false,
        snapshotWindowUntil = 0,
        startupBlockUntil = 0,
        startupOnline = {},
        startupSeen = {},
    }

    return self.runtime
end

local function getRosterProcessingTime()
    if type(GetTime) == "function" then
        return tonumber(GetTime()) or 0
    end

    return tonumber(time()) or 0
end

local function shouldTrackGuildRoster(service, runtime)
    local settings = service:GetSettings()

    if type(runtime) == "table" and not runtime.rosterInitialized then
        return true
    end

    return settings.enableWelcome ~= false
end

local function getRosterProcessingDelay(runtime)
    local delaySeconds = ROSTER_PROCESS_DELAY_SECONDS
    local now = getRosterProcessingTime()
    local lastProcessedAt = tonumber(type(runtime) == "table" and runtime.lastRosterProcessedAt) or 0

    if lastProcessedAt > 0 then
        delaySeconds = math.max(delaySeconds, (lastProcessedAt + ROSTER_PROCESS_MIN_INTERVAL_SECONDS) - now)
    end

    return math.max(0, delaySeconds)
end

local function ensurePresenceTicker(runtime)
    if type(runtime) ~= "table" or runtime.presenceTicker or not (C_Timer and C_Timer.NewTicker) then
        return false
    end

    runtime.presenceTicker = C_Timer.NewTicker(PRESENCE_INTERVAL_SECONDS, function()
        addon.GratsUService:BroadcastPresence("PING")
    end)

    return true
end

function addon.GratsUService:EnsureDefaults()
    local state = self:GetState()
    local defaults = self:GetDefaultSettings()

    state.settings = state.settings or {}

    for key, value in pairs(defaults) do
        if state.settings[key] == nil then
            state.settings[key] = value
        end
    end

    state.stats = state.stats or {
        achievementSent = 0,
        lastSentAt = 0,
        welcomeSent = 0,
    }

    if type(state.achievementMessages) ~= "table" or #state.achievementMessages == 0 then
        state.achievementMessages = self:GetDefaultMessages("achievement")
    end

    if type(state.welcomeMessages) ~= "table" or #state.welcomeMessages == 0 then
        state.welcomeMessages = self:GetDefaultMessages("welcome")
    end
end

function addon.GratsUService:Initialize()
    self:EnsureDefaults()
    self:GetRuntime()
end

function addon.GratsUService:IsPaused()
    return (addon.IsSuspended and addon:IsSuspended())
        or (addon.IsInstanceFeatureSuspended and addon:IsInstanceFeatureSuspended())
        or false
end

function addon.GratsUService:DiscardPendingWelcomes()
    local runtime = self:GetRuntime()

    wipe(runtime.pendingWelcomeTimers)
    runtime.rosterProcessPending = false
    runtime.rosterProcessGeneration = (runtime.rosterProcessGeneration or 0) + 1
end

function addon.GratsUService:Pause(options)
    local resolvedOptions = type(options) == "table" and options or {}
    local runtime = self:GetRuntime()

    if runtime.presenceTicker then
        runtime.presenceTicker:Cancel()
        runtime.presenceTicker = nil
    end

    if resolvedOptions.instanceSuspend then
        self:DiscardPendingWelcomes()
        runtime.rosterInitialized = false
    end
end

function addon.GratsUService:Resume(options)
    local resolvedOptions = type(options) == "table" and options or {}

    if self:IsPaused() then
        return
    end

    self:EnsureDefaults()

    if not self:CanAccess() then
        return
    end

    local runtime = self:GetRuntime()
    ensurePresenceTicker(runtime)

    if resolvedOptions.instanceResume then
        self:DiscardPendingWelcomes()
        runtime.rosterInitialized = false
        runtime.lastRosterProcessedAt = 0
        self:BroadcastPresence("PING")
        requestGuildRoster()
        self:HandleGuildRosterUpdate()
    elseif resolvedOptions.lightResume then
        runtime.lastRosterProcessedAt = 0
        self:BroadcastPresence("PING")
        requestGuildRoster()
        self:HandleGuildRosterUpdate()
    end
end

function addon.GratsUService:IsGuildMaster()
    local _, _, rankIndex = GetGuildInfo("player")
    return tonumber(rankIndex) == 0
end

function addon.GratsUService:CanAccess()
    if not IsInGuild or not IsInGuild() then
        return false
    end

    return self:IsGuildMaster() or canEditOfficerNote()
end

function addon.GratsUService:GetMessages(kind)
    local state = self:GetState()

    if normalizeKind(kind) == "welcome" then
        return state.welcomeMessages
    end

    return state.achievementMessages
end

function addon.GratsUService:GetSettings()
    return self:GetState().settings
end

function addon.GratsUService:UpdateSettings(payload)
    local settings = self:GetSettings()
    local defaults = self:GetDefaultSettings()
    payload = payload or {}

    if payload.enableWelcome ~= nil then
        settings.enableWelcome = payload.enableWelcome and true or false
    end

    if payload.enableAchievement ~= nil then
        settings.enableAchievement = payload.enableAchievement and true or false
    end

    if payload.ignoreSelf ~= nil then
        settings.ignoreSelf = payload.ignoreSelf and true or false
    end

    if payload.antiSpamSeconds ~= nil then
        settings.antiSpamSeconds = clampNumber(payload.antiSpamSeconds, defaults.antiSpamSeconds)
    end

    if payload.minOfflineSeconds ~= nil then
        settings.minOfflineSeconds = clampNumber(payload.minOfflineSeconds, defaults.minOfflineSeconds)
    end

    if payload.achievementCooldownSeconds ~= nil then
        settings.achievementCooldownSeconds = clampNumber(payload.achievementCooldownSeconds, defaults.achievementCooldownSeconds)
    end

    if payload.welcomeDelaySeconds ~= nil then
        settings.welcomeDelaySeconds = clampNumber(payload.welcomeDelaySeconds, defaults.welcomeDelaySeconds)
    end

    return settings
end

function addon.GratsUService:ResetSettings()
    local state = self:GetState()
    state.settings = self:GetDefaultSettings()
    return state.settings
end

function addon.GratsUService:AddMessage(kind, text)
    local list = self:GetMessages(kind)
    local sanitized = sanitizeMessageText(text)

    if not sanitized then
        return false, "message cannot be empty"
    end

    if #list >= MAX_MESSAGES then
        return false, "maximum messages reached"
    end

    list[#list + 1] = sanitized
    return true
end

function addon.GratsUService:UpdateMessage(kind, index, text)
    local list = self:GetMessages(kind)
    local sanitized = sanitizeMessageText(text)
    local resolvedIndex = tonumber(index)

    if not sanitized then
        return false, "message cannot be empty"
    end

    if not resolvedIndex or resolvedIndex < 1 or resolvedIndex > #list then
        return false, "message selection is invalid"
    end

    list[resolvedIndex] = sanitized
    return true
end

function addon.GratsUService:DeleteMessage(kind, index)
    local list = self:GetMessages(kind)
    local resolvedIndex = tonumber(index)

    if not resolvedIndex or resolvedIndex < 1 or resolvedIndex > #list then
        return false, "message selection is invalid"
    end

    table.remove(list, resolvedIndex)

    if #list == 0 then
        if normalizeKind(kind) == "welcome" then
            self:GetState().welcomeMessages = self:GetDefaultMessages("welcome")
        else
            self:GetState().achievementMessages = self:GetDefaultMessages("achievement")
        end
    end

    return true
end

function addon.GratsUService:ResetMessages(kind)
    if normalizeKind(kind) == "welcome" then
        self:GetState().welcomeMessages = self:GetDefaultMessages("welcome")
        return self:GetState().welcomeMessages
    end

    self:GetState().achievementMessages = self:GetDefaultMessages("achievement")
    return self:GetState().achievementMessages
end

function addon.GratsUService:ResetAllDefaults()
    self:ResetMessages("achievement")
    self:ResetMessages("welcome")
    self:ResetSettings()
end

function addon.GratsUService:GetRandomMessage(kind)
    local list = self:GetMessages(kind)

    if #list == 0 then
        return nil
    end

    return list[math.random(1, #list)]
end

function addon.GratsUService:FormatGuildMessage(name, text)
    return string.format("%s - %s", baseName(name) or SAMPLE_PREVIEW_NAME, trimString(text or ""))
end

function addon.GratsUService:IsSelf(name)
    return baseName(name) == baseName(addon.Store:GetPlayerName())
end

function addon.GratsUService:GetActivePeers(maxAge)
    local runtime = self:GetRuntime()
    local age = maxAge or PEER_MAX_AGE_SECONDS
    local now = GetTime()
    local peers = {}
    local activePeerTimes = {}
    local localPlayerName = normalizePeerName(addon.Store:GetPlayerName())

    if self:CanAccess() and localPlayerName then
        runtime.addonPeersTime[localPlayerName] = now
    end

    for name, lastSeenAt in pairs(runtime.addonPeersTime) do
        local normalizedName = normalizePeerName(name)

        if normalizedName then
            local resolvedSeenAt = tonumber(lastSeenAt) or 0
            local previousSeenAt = tonumber(activePeerTimes[normalizedName]) or 0

            if resolvedSeenAt > previousSeenAt then
                activePeerTimes[normalizedName] = resolvedSeenAt
            end
        end
    end

    for name, lastSeenAt in pairs(activePeerTimes) do
        if (now - lastSeenAt) <= age then
            peers[#peers + 1] = name
        end
    end

    table.sort(peers)
    return peers
end

function addon.GratsUService:IsDesignatedSender(eventKey)
    local peers = self:GetActivePeers(PEER_MAX_AGE_SECONDS)
    local localPlayerName = normalizePeerName(addon.Store:GetPlayerName())

    if #peers == 0 then
        return true
    end

    local checksum = 0

    for index = 1, #eventKey do
        checksum = (checksum + string.byte(eventKey, index)) % 2147483647
    end

    return peers[(checksum % #peers) + 1] == localPlayerName
end

function addon.GratsUService:GetWelcomeSendDelay(eventKey)
    local peers = self:GetActivePeers(PEER_MAX_AGE_SECONDS)
    local localPlayerName = normalizePeerName(addon.Store:GetPlayerName())

    if not localPlayerName then
        return nil
    end

    if #peers <= 1 then
        return 0
    end

    local checksum = 0

    for index = 1, #eventKey do
        checksum = (checksum + string.byte(eventKey, index)) % 2147483647
    end

    local designatedIndex = (checksum % #peers) + 1
    local localIndex = nil

    for index, peerName in ipairs(peers) do
        if peerName == localPlayerName then
            localIndex = index
            break
        end
    end

    if not localIndex then
        return nil
    end

    local senderOffset = localIndex - designatedIndex

    if senderOffset < 0 then
        senderOffset = senderOffset + #peers
    end

    return senderOffset * WELCOME_FALLBACK_DELAY_SECONDS
end

function addon.GratsUService:RecordSend(kind)
    local stats = self:GetState().stats

    if normalizeKind(kind) == "welcome" then
        stats.welcomeSent = (stats.welcomeSent or 0) + 1
    else
        stats.achievementSent = (stats.achievementSent or 0) + 1
    end

    stats.lastSentAt = time()
end

function addon.GratsUService:BroadcastPresence(message)
    if self:IsPaused() or not self:CanAccess() then
        return false
    end

    local runtime = self:GetRuntime()
    local localPlayerName = normalizePeerName(addon.Store:GetPlayerName())

    if localPlayerName then
        runtime.addonPeersTime[localPlayerName] = GetTime()
    end

    sendAddonMessage(self:GetPrefix(), message or "PING", "GUILD")
    return true
end

function addon.GratsUService:HandlePlayerLogin()
    if self:IsPaused() or not self:CanAccess() then
        return
    end

    local runtime = self:GetRuntime()
    local now = GetTime()

    runtime.startupBlockUntil = now + STARTUP_GRACE_SECONDS
    runtime.snapshotWindowUntil = now + STARTUP_GRACE_SECONDS
    requestGuildRoster()
    self:BroadcastPresence("HELLO")
    ensurePresenceTicker(runtime)
end

function addon.GratsUService:HandleIncoming(prefix, message, _, sender)
    local normalizedSender = normalizePeerName(sender)
    local localPlayerName = normalizePeerName(addon.Store:GetPlayerName())

    if self:IsPaused() or prefix ~= self:GetPrefix() or not normalizedSender or normalizedSender == localPlayerName then
        return
    end

    local runtime = self:GetRuntime()
    runtime.addonPeersTime[normalizedSender] = GetTime()

    local announcementKind, announcementKey = parseSendAnnouncement(message)

    if announcementKind == "welcome" and announcementKey then
        runtime.lastWelcome[announcementKey] = GetTime()
        runtime.pendingWelcomeTimers[announcementKey] = nil
        return true
    end

    if message == "HELLO" and self:CanAccess() then
        self:BroadcastPresence("PING")
        return true
    end

    if message == "PING" then
        return true
    end
end

function addon.GratsUService:HandleGuildAchievement(...)
    if self:IsPaused() or not self:CanAccess() then
        return
    end

    local settings = self:GetSettings()
    local _, playerName = ...

    if settings.enableAchievement == false then
        return
    end

    if settings.ignoreSelf and self:IsSelf(playerName) then
        return
    end

    local memberKey = baseName(playerName)

    if not memberKey then
        return
    end

    local runtime = self:GetRuntime()
    local now = GetTime()
    local cooldown = settings.achievementCooldownSeconds or 120
    local lastSentAt = runtime.lastAchieveCongrats[memberKey]

    if lastSentAt and (now - lastSentAt) <= cooldown then
        return
    end

    if not self:IsDesignatedSender(buildEventKey("ACH", playerName or memberKey)) then
        return
    end

    local message = self:GetRandomMessage("achievement")

    if message and sendGuild(self:FormatGuildMessage(playerName or memberKey, message)) then
        runtime.lastAchieveCongrats[memberKey] = now
        self:RecordSend("achievement")
    end
end

function addon.GratsUService:ProcessGuildRoster()
    local perfScopeName = "gratsu:process-guild-roster"
    local perfStartedAt = addon.BeginPerfScope and addon:BeginPerfScope(perfScopeName) or nil
    local function finishPerf()
        if perfStartedAt ~= nil and addon.EndPerfScope then
            addon:EndPerfScope(perfScopeName, perfStartedAt)
        end
    end

    if self:IsPaused() or not self:CanAccess() then
        finishPerf()
        return
    end

    local runtime = self:GetRuntime()
    local settings = self:GetSettings()
    local totalMembers = GetNumGuildMembers() or 0

    if totalMembers == 0 then
        finishPerf()
        return
    end

    if not runtime.rosterInitialized then
        wipe(runtime.onlineMembers)
        wipe(runtime.haveSeenOffline)
        wipe(runtime.offlineSince)
        wipe(runtime.startupOnline)
        wipe(runtime.startupSeen)
        local initializedAt = GetTime()
        local offlineBaseline = initializedAt - (settings.minOfflineSeconds or 120)

        for index = 1, totalMembers do
            local name, _, _, _, _, _, _, _, online = GetGuildRosterInfo(index)
            local key = baseName(name)

            if key then
                runtime.onlineMembers[key] = online and true or nil
                if online then
                    runtime.startupOnline[key] = true
                else
                    runtime.haveSeenOffline[key] = true
                    runtime.offlineSince[key] = offlineBaseline
                end
                runtime.startupSeen[key] = true
            end
        end

        runtime.rosterInitialized = true
        runtime.lastRosterProcessedAt = getRosterProcessingTime()
        finishPerf()
        return
    end

    if settings.enableWelcome == false then
        runtime.lastRosterProcessedAt = getRosterProcessingTime()
        finishPerf()
        return
    end

    local now = GetTime()
    runtime.lastRosterProcessedAt = tonumber(now) or getRosterProcessingTime()

    for index = 1, totalMembers do
        local name, _, _, _, _, _, _, _, online, status, _, _, _, isMobile = GetGuildRosterInfo(index)
        local key = baseName(name)

        if key then
            if now < (runtime.snapshotWindowUntil or 0) then
                runtime.startupSeen[key] = true
            end

            local rawOnline = online and true or nil

            if settings.ignoreSelf and self:IsSelf(key) then
                runtime.onlineMembers[key] = rawOnline
            else
                local wasOnline = runtime.onlineMembers[key]
                local minOfflineSeconds = settings.minOfflineSeconds or 120

                if rawOnline
                    and not wasOnline
                    and runtime.haveSeenOffline[key] ~= true
                    and runtime.offlineSince[key] == nil
                    and runtime.startupSeen[key] ~= true then
                    runtime.haveSeenOffline[key] = true
                    runtime.offlineSince[key] = now - minOfflineSeconds
                end

                local shouldWelcome = rawOnline and not isMobile
                local offlineSince = runtime.offlineSince[key]
                local wasOfflineLongEnough = offlineSince ~= nil and (now - offlineSince) >= minOfflineSeconds
                local allowWelcome = settings.enableWelcome ~= false
                    and now > (runtime.startupBlockUntil or 0)
                    and shouldWelcome
                    and not wasOnline
                    and runtime.haveSeenOffline[key] == true
                    and wasOfflineLongEnough

                if allowWelcome then
                    local lastWelcomeAt = runtime.lastWelcome[key]
                    local cooldown = settings.antiSpamSeconds or 300

                    if not lastWelcomeAt or (now - lastWelcomeAt) > cooldown then
                        local eventKey = buildEventKey("WEL", key)
                        local baseDelay = settings.welcomeDelaySeconds or 0
                        local senderDelay = self:GetWelcomeSendDelay(eventKey)

                        if senderDelay ~= nil then
                            local delay = (baseDelay > 0 and baseDelay or 0) + senderDelay

                            if delay > 0 then
                                if not runtime.pendingWelcomeTimers[key] then
                                    local scheduledFor = now + delay
                                    runtime.pendingWelcomeTimers[key] = scheduledFor
                                    C_Timer.After(delay, function()
                                        local currentRuntime = addon.GratsUService:GetRuntime()
                                        local currentSettings = addon.GratsUService:GetSettings()
                                        local checkNow = GetTime()
                                        local antiSpamSeconds = currentSettings.antiSpamSeconds or 300

                                        if currentRuntime.pendingWelcomeTimers[key] ~= scheduledFor then
                                            return
                                        end

                                        currentRuntime.pendingWelcomeTimers[key] = nil

                                        if not addon.GratsUService:IsPaused()
                                            and currentRuntime.onlineMembers[key]
                                            and currentSettings.enableWelcome ~= false
                                            and (not currentRuntime.lastWelcome[key] or (checkNow - currentRuntime.lastWelcome[key]) > antiSpamSeconds) then
                                            sendWelcomeMessage(key)
                                        end
                                    end)
                                end
                            else
                                sendWelcomeMessage(key)
                            end
                        end
                    end
                end

                runtime.onlineMembers[key] = rawOnline

                if rawOnline then
                    runtime.offlineSince[key] = nil
                else
                    runtime.haveSeenOffline[key] = true
                    runtime.offlineSince[key] = runtime.offlineSince[key] or now
                end
            end
        end
    end

    finishPerf()
end

function addon.GratsUService:HandleGuildRosterUpdate()
    if self:IsPaused() or not self:CanAccess() then
        return
    end

    local runtime = self:GetRuntime()

    if not shouldTrackGuildRoster(self, runtime) then
        return
    end

    if runtime.rosterProcessPending or not (C_Timer and C_Timer.After) then
        return
    end

    runtime.rosterProcessPending = true
    runtime.rosterProcessGeneration = runtime.rosterProcessGeneration or 0
    local expectedGeneration = runtime.rosterProcessGeneration

    C_Timer.After(getRosterProcessingDelay(runtime), function()
        local currentRuntime = addon.GratsUService:GetRuntime()
        currentRuntime.rosterProcessPending = false

        if expectedGeneration ~= (currentRuntime.rosterProcessGeneration or 0) then
            return
        end

        if addon.GratsUService:IsPaused() then
            return
        end

        if not shouldTrackGuildRoster(addon.GratsUService, currentRuntime) then
            return
        end

        addon.GratsUService:ProcessGuildRoster()
    end)
end

function addon.GratsUService:GetSummary()
    local state = self:GetState()
    local peers = self:GetActivePeers(PEER_MAX_AGE_SECONDS)
    local roleLabel = "Officer"

    if self:IsGuildMaster() then
        roleLabel = "Guild Master"
    end

    return {
        access = self:CanAccess() and roleLabel or "Restricted",
        achievementCount = #(state.achievementMessages or {}),
        activePeerCount = #peers,
        activePeerNames = peers,
        lastSentAt = state.stats and state.stats.lastSentAt or 0,
        stats = state.stats or {},
        welcomeCount = #(state.welcomeMessages or {}),
    }
end

function addon.GratsUService:GetPreview(kind, text)
    local sourceText = sanitizeMessageText(text)

    if not sourceText then
        sourceText = self:GetRandomMessage(kind) or "Type a message to preview it here."
    end

    return self:FormatGuildMessage(SAMPLE_PREVIEW_NAME, sourceText)
end