local addonName, TOAST = ...

-- The one recency window for "seen recently": strictly under it counts as
-- present, at or over it is offline. Exported so the leaderboard dot, its
-- tooltip age text and the dashboard crew count can never drift apart.
local RECENT_SECONDS = 30 * 60
TOAST.PRESENCE_RECENT_SECONDS = RECENT_SECONDS

local PRESENCE_SCHEMA_VERSION = 1

-- Live session state, private to this module: which roster characters are
-- online right now, and which character each crew is online on. Both are keyed
-- the same way for every writer -- see MarkSessionOnline below.
local onlineByIdentity = nil
local onlineCharByDiscordId = nil

-- crew_state.lua owns roster identity (normalisation, the member indexes, and
-- the name-only fallback for callbacks that expose no realm). This module used
-- to carry a second copy of all of it, which meant two caches of one roster.
local function IdentityKey(name, realm)
    return TOAST.CrewState.IdentityKey(name, realm)
end

local function ResolveKnownMember(name, realm)
    return TOAST.CrewState.ResolveMember(name, realm)
end

local function EnsurePresenceDB()
    TOAST_DB = TOAST_DB or {}
    local presence = TOAST_DB.presence
    if type(presence) ~= "table" then
        presence = {}
        TOAST_DB.presence = presence
    end
    presence.schemaVersion = PRESENCE_SCHEMA_VERSION
    if type(presence.characters) ~= "table" then
        presence.characters = {}
    end
    return presence
end

local function StoreObservation(member, seenAt)
    if not member then return end
    seenAt = tonumber(seenAt)
    if not seenAt or seenAt <= 0 then return end

    local identity = IdentityKey(member.name, member.realm)
    if not identity then return end

    local presence = EnsurePresenceDB()
    local current = presence.characters[identity]
    local currentSeen = type(current) == "table" and tonumber(current.lastSeen) or nil
    if not currentSeen or seenAt > currentSeen then
        presence.characters[identity] = {
            name = member.name,
            realm = member.realm,
            lastSeen = seenAt,
        }
    end
end

-- Both writers of the session online state (this one, fed by addon messages,
-- and RefreshOnlineStatus below, fed by C_Club) must agree on the record shape,
-- or an observation would mean something different depending on its source.
local function MarkSessionOnline(member)
    if not member then return end

    local identity = IdentityKey(member.name, member.realm)
    if not identity then return end

    onlineByIdentity = onlineByIdentity or {}
    onlineByIdentity[identity] = true

    onlineCharByDiscordId = onlineCharByDiscordId or {}
    if member.discord_id then
        onlineCharByDiscordId[member.discord_id] =
            { name = member.name, realm = member.realm }
    end
end

-- The one read path into the session state for other modules: which character
-- a crew is currently online on, realm included, or nil.
function TOAST.GetOnlineCrewCharacter(discordId)
    if not discordId or not onlineCharByDiscordId then return nil end
    return onlineCharByDiscordId[discordId]
end

function TOAST.MarkPresenceSeen(name, realm)
    local member = ResolveKnownMember(name, realm)
    if not member then return false end

    StoreObservation(member, GetServerTime())
    MarkSessionOnline(member)
    return true
end

function TOAST.MarkPresenceSenderSeen(sender)
    if type(sender) ~= "string" then return false end
    local name, realm = sender:match("^([^%-]+)%-(.+)$")
    if not name then
        name = sender
    end
    return TOAST.MarkPresenceSeen(name, realm)
end

function TOAST.IsCharacterCurrentlyOnline(name, realm)
    local member = ResolveKnownMember(name, realm)
    local identity = member and IdentityKey(member.name, member.realm)
    return identity ~= nil
        and onlineByIdentity ~= nil
        and onlineByIdentity[identity] == true
end

local function CharacterLastSeen(member)
    if not member then return nil end
    local presence = EnsurePresenceDB()
    local identity = IdentityKey(member.name, member.realm)
    if not identity then return nil end
    local record = presence.characters[identity]
    return type(record) == "table" and tonumber(record.lastSeen) or nil
end

function TOAST.GetPresenceStatus(name, realm, discordId)
    local member = ResolveKnownMember(name, realm)
    discordId = discordId or (member and member.discord_id)

    -- The queried character wins over its crew: when both a main and its alt
    -- are online, callers asking about the main must be told about the main.
    -- Only if it is offline does the crew answer for it, which is what lights
    -- up a leaderboard row whose player is playing an alt.
    if member and TOAST.IsCharacterCurrentlyOnline(member.name, member.realm) then
        return "online", member.name
    end
    local onlineRecord = TOAST.GetOnlineCrewCharacter(discordId)
    if onlineRecord then
        return "online", onlineRecord.name
    end

    local newestSeen
    local crew = discordId and TOAST.CrewState.GetCrewMembers(discordId) or nil
    if crew and #crew > 0 then
        for _, crewMember in ipairs(crew) do
            local seen = CharacterLastSeen(crewMember)
            if seen and (not newestSeen or seen > newestSeen) then
                newestSeen = seen
            end
        end
    else
        newestSeen = CharacterLastSeen(member)
    end

    if not newestSeen then return nil end
    if GetServerTime() - newestSeen < RECENT_SECONDS then
        return "online", nil, newestSeen
    end
    return "known", nil, newestSeen
end

-- Refreshes live C_Club state only when the source was actually readable.
-- A nil result (combat, systems not ready, or no readable club) keeps the
-- previous session state instead of falsely declaring everyone offline.
function TOAST.RefreshOnlineStatus()
    local online = TOAST.GetOnlineMembers and TOAST.GetOnlineMembers() or nil
    if online == nil then return false end

    local canonicalOnline = {}
    local byId = {}
    for _, character in pairs(online) do
        local member = ResolveKnownMember(character.name, character.realm)
        local identity = member and IdentityKey(member.name, member.realm)
        if identity then
            StoreObservation(member, GetServerTime())
            canonicalOnline[identity] = true
            if member.discord_id then
                byId[member.discord_id] = { name = member.name, realm = member.realm }
            end
        end
    end

    onlineByIdentity = canonicalOnline
    onlineCharByDiscordId = byId
    return true
end

local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_LOGIN")
events:SetScript("OnEvent", function()
    -- UnitName's second return is nil on your own realm, so the realm comes
    -- from GetRealmName -- same pairing as every other self-identity read in
    -- the addon. Without it the login observation falls back to matching on
    -- character name alone.
    TOAST.MarkPresenceSeen(UnitName("player"), GetRealmName())
end)
