local addonName, TOAST = ...

local RECENT_SECONDS = 3600
local PRESENCE_SCHEMA_VERSION = 1

local memberByIdentity
local membersByName
local membersByDiscordId

local function NormalizeName(name)
    if type(name) ~= "string" then return nil end
    name = name:match("^%s*(.-)%s*$")
    if name == "" then return nil end
    return name:lower()
end

local function NormalizeRealm(realm)
    if type(realm) ~= "string" then return nil end
    realm = realm:gsub("%s+", ""):match("^%s*(.-)%s*$")
    if realm == "" then return nil end
    return realm:lower()
end

local function IdentityKey(name, realm)
    local normalizedName = NormalizeName(name)
    local normalizedRealm = NormalizeRealm(realm)
    if not normalizedName or not normalizedRealm then return nil end
    return normalizedName .. "-" .. normalizedRealm
end

local function EnsureMemberIndexes()
    if memberByIdentity then return end

    memberByIdentity = {}
    membersByName = {}
    membersByDiscordId = {}

    for _, member in ipairs(TOAST.members or {}) do
        local identity = IdentityKey(member.name, member.realm)
        local normalizedName = NormalizeName(member.name)
        if identity and normalizedName then
            memberByIdentity[identity] = member

            local sameName = membersByName[normalizedName]
            if not sameName then
                sameName = {}
                membersByName[normalizedName] = sameName
            end
            sameName[#sameName + 1] = member

            if member.discord_id then
                local crew = membersByDiscordId[member.discord_id]
                if not crew then
                    crew = {}
                    membersByDiscordId[member.discord_id] = crew
                end
                crew[#crew + 1] = member
            end
        end
    end
end

local function ResolveKnownMember(name, realm)
    EnsureMemberIndexes()

    local identity = IdentityKey(name, realm)
    if identity and memberByIdentity[identity] then
        return memberByIdentity[identity]
    end
    if NormalizeRealm(realm) then
        return nil
    end

    -- Some WoW callbacks expose only the character name. Accept that only
    -- when it maps unambiguously to one roster character.
    local candidates = membersByName[NormalizeName(name)]
    if candidates and #candidates == 1 then
        return candidates[1]
    end
    return nil
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

local function MarkSessionOnline(member)
    if not member then return end

    TOAST._onlineMembers = TOAST._onlineMembers or {}
    TOAST._onlineMembers[member.name] = true

    TOAST._onlineCharByDiscordId = TOAST._onlineCharByDiscordId or {}
    if member.discord_id then
        TOAST._onlineCharByDiscordId[member.discord_id] = member.name
    end
end

local function MigrateKeyPresence()
    local presence = EnsurePresenceDB()
    if presence.migratedKeyLastSeen then return end

    for name, keyData in pairs(TOAST_DB.keys or {}) do
        local member = ResolveKnownMember(name)
        local lastSeen = type(keyData) == "table" and tonumber(keyData.lastSeen) or nil
        if member and lastSeen and lastSeen > 0 then
            StoreObservation(member, lastSeen)
        end
    end

    presence.migratedKeyLastSeen = true
end
TOAST.MigrateKeyPresence = MigrateKeyPresence

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)
    return member ~= nil
        and TOAST._onlineMembers ~= nil
        and TOAST._onlineMembers[member.name] == 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)
    EnsureMemberIndexes()

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

    if discordId and TOAST._onlineCharByDiscordId
       and TOAST._onlineCharByDiscordId[discordId] then
        return "online", TOAST._onlineCharByDiscordId[discordId]
    end
    if member and TOAST.IsCharacterCurrentlyOnline(member.name, member.realm) then
        return "online", member.name
    end

    local newestSeen
    if discordId and membersByDiscordId[discordId] then
        for _, crewMember in ipairs(membersByDiscordId[discordId]) 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 characterName in pairs(online) do
        local member = ResolveKnownMember(characterName)
        if member then
            StoreObservation(member, GetServerTime())
            canonicalOnline[member.name] = true
            if member.discord_id then
                byId[member.discord_id] = member.name
            end
        end
    end

    TOAST._onlineMembers = canonicalOnline
    TOAST._onlineCharByDiscordId = byId
    return true
end

local events = CreateFrame("Frame")
events:RegisterEvent("ADDON_LOADED")
events:RegisterEvent("PLAYER_LOGIN")
events:SetScript("OnEvent", function(_, event, arg1)
    if event == "ADDON_LOADED" then
        if arg1 ~= addonName then return end
        MigrateKeyPresence()
        return
    end

    local playerName, playerRealm = UnitName("player")
    TOAST.MarkPresenceSeen(playerName, playerRealm)
end)
