GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.SessionManager = addon.SessionManager or {}
addon.SessionManager.activeSessions = addon.SessionManager.activeSessions or {}
addon.SessionManager.sessionSequence = addon.SessionManager.sessionSequence or 0

local SESSION_TIMEOUT_SECONDS = 180
local ROSTER_SESSION_TIMEOUT_SECONDS = 600
local INITIAL_PEER_REPLY_TIMEOUT_SECONDS = 30
local SESSION_RETENTION_SECONDS = 60
local ROSTER_SUMMARY_MERGE_WINDOW_SECONDS = 300

local function createSessionStats()
    return {
        outboundChunksByOp = {},
        inboundChunksByOp = {},
        outboundBytesByOp = {},
        inboundBytesByOp = {},
        recordsRequestedFromPeer = 0,
        recordsRequestedByPeer = 0,
    }
end

local function bumpMetric(bucket, key, amount)
    bucket[key] = (bucket[key] or 0) + (amount or 0)
end

local function getSessionTimeoutSeconds(session)
    if session and session.tableName == "roster" then
        return ROSTER_SESSION_TIMEOUT_SECONDS
    end

    return SESSION_TIMEOUT_SECONDS
end

local function isWaitingForPeerReply(session)
    local metadata = session and type(session.metadata) == "table" and session.metadata or nil

    return session
        and session.status == "active"
        and metadata
        and metadata.direction == "outgoing"
        and not session.lastSeenAt
end

local function getWaitingForPeerReplyLabel(session)
    local resolution = session and session.metadata and session.metadata.resolution or nil

    if resolution == "snapshot-pull" then
        return "waiting for peer snapshot reply"
    end

    if resolution == "roster-journal-pull" then
        return "waiting for peer roster journal reply"
    end

    if resolution == "push-delta" then
        return "waiting for peer record request"
    end

    if resolution == "accept-remote" or resolution == "accept-remote-bulk" then
        return "waiting for peer records"
    end

    return "waiting for peer manifest reply"
end

local function buildEmptyRosterSummary(session, now)
    return {
        peer = session and session.peer or nil,
        tableName = session and session.tableName or "roster",
        startedAt = now,
        completedAt = 0,
        updatedAt = now,
        pullSessions = 0,
        pushSessions = 0,
        cancelledSessions = 0,
        failedSessions = 0,
        completedSessions = 0,
        manifestChunksSent = 0,
        manifestChunksReceived = 0,
        manifestBytesSent = 0,
        manifestBytesReceived = 0,
        recordRequestChunksSent = 0,
        recordRequestChunksReceived = 0,
        recordRequestBytesSent = 0,
        recordRequestBytesReceived = 0,
        recordResponseChunksSent = 0,
        recordResponseChunksReceived = 0,
        recordResponseBytesSent = 0,
        recordResponseBytesReceived = 0,
        snapshotChunksSent = 0,
        snapshotChunksReceived = 0,
        snapshotBytesSent = 0,
        snapshotBytesReceived = 0,
        recordsRequestedFromPeer = 0,
        recordsRequestedByPeer = 0,
        lastError = nil,
        lastStatus = "unknown",
        notes = {},
    }
end

local function getLastRosterSyncSummary(session)
    local sync = addon.Store:GetSync()
    local now = time()
    local summary = type(sync.lastRosterSync) == "table" and sync.lastRosterSync or nil

    if not summary
        or summary.peer ~= session.peer
        or (now - (summary.updatedAt or 0)) > ROSTER_SUMMARY_MERGE_WINDOW_SECONDS then
        summary = buildEmptyRosterSummary(session, now)
        sync.lastRosterSync = summary
    end

    return summary
end

local function mergeRosterSessionSummary(session)
    if not session or session.tableName ~= "roster" then
        return
    end

    local summary = getLastRosterSyncSummary(session)
    local stats = session.stats or createSessionStats()
    local resolution = session.metadata and session.metadata.resolution or "delta-pull"

    summary.peer = session.peer
    summary.startedAt = session.startedAt or summary.startedAt or time()
    summary.updatedAt = time()
    summary.completedAt = session.completedAt or summary.completedAt or 0
    summary.lastError = session.lastError or nil
    summary.lastStatus = session.status or summary.lastStatus or "unknown"

    if resolution == "push-delta" then
        summary.pushSessions = (summary.pushSessions or 0) + 1
    else
        summary.pullSessions = (summary.pullSessions or 0) + 1
    end

    if session.status == "complete" then
        summary.completedSessions = (summary.completedSessions or 0) + 1
    elseif session.status == "failed" then
        summary.failedSessions = (summary.failedSessions or 0) + 1
    elseif session.status == "cancelled" then
        summary.cancelledSessions = (summary.cancelledSessions or 0) + 1
    end

    summary.manifestChunksSent = (summary.manifestChunksSent or 0) + (stats.outboundChunksByOp.MANIFEST_RES or 0)
    summary.manifestChunksReceived = (summary.manifestChunksReceived or 0) + (stats.inboundChunksByOp.MANIFEST_RES or 0)
    summary.manifestBytesSent = (summary.manifestBytesSent or 0) + (stats.outboundBytesByOp.MANIFEST_RES or 0)
    summary.manifestBytesReceived = (summary.manifestBytesReceived or 0) + (stats.inboundBytesByOp.MANIFEST_RES or 0)
    summary.recordRequestChunksSent = (summary.recordRequestChunksSent or 0) + (stats.outboundChunksByOp.RECORD_REQ or 0)
    summary.recordRequestChunksReceived = (summary.recordRequestChunksReceived or 0) + (stats.inboundChunksByOp.RECORD_REQ or 0)
    summary.recordRequestBytesSent = (summary.recordRequestBytesSent or 0) + (stats.outboundBytesByOp.RECORD_REQ or 0)
    summary.recordRequestBytesReceived = (summary.recordRequestBytesReceived or 0) + (stats.inboundBytesByOp.RECORD_REQ or 0)
    summary.recordResponseChunksSent = (summary.recordResponseChunksSent or 0) + (stats.outboundChunksByOp.RECORD_RES or 0)
    summary.recordResponseChunksReceived = (summary.recordResponseChunksReceived or 0) + (stats.inboundChunksByOp.RECORD_RES or 0)
    summary.recordResponseBytesSent = (summary.recordResponseBytesSent or 0) + (stats.outboundBytesByOp.RECORD_RES or 0)
    summary.recordResponseBytesReceived = (summary.recordResponseBytesReceived or 0) + (stats.inboundBytesByOp.RECORD_RES or 0)
    summary.snapshotChunksSent = (summary.snapshotChunksSent or 0) + (stats.outboundChunksByOp.SNAPSHOT_RES or 0) + (stats.outboundChunksByOp.SNAPSHOT_PUSH or 0)
    summary.snapshotChunksReceived = (summary.snapshotChunksReceived or 0) + (stats.inboundChunksByOp.SNAPSHOT_RES or 0) + (stats.inboundChunksByOp.SNAPSHOT_PUSH or 0)
    summary.snapshotBytesSent = (summary.snapshotBytesSent or 0) + (stats.outboundBytesByOp.SNAPSHOT_RES or 0) + (stats.outboundBytesByOp.SNAPSHOT_PUSH or 0)
    summary.snapshotBytesReceived = (summary.snapshotBytesReceived or 0) + (stats.inboundBytesByOp.SNAPSHOT_RES or 0) + (stats.inboundBytesByOp.SNAPSHOT_PUSH or 0)
    summary.recordsRequestedFromPeer = (summary.recordsRequestedFromPeer or 0) + (stats.recordsRequestedFromPeer or 0)
    summary.recordsRequestedByPeer = (summary.recordsRequestedByPeer or 0) + (stats.recordsRequestedByPeer or 0)

    local note = string.format("%s %s with %s (%s)", date("%H:%M:%S", session.completedAt or time()), resolution, session.peer or "unknown", session.status or "unknown")

    if session.lastError and session.lastError ~= "" then
        note = string.format("%s: %s", note, session.lastError)
    end

    summary.notes[#summary.notes + 1] = note

    while #summary.notes > 6 do
        table.remove(summary.notes, 1)
    end
end

function addon.SessionManager:StartSession(peer, tableName, total, metadata)
    self.sessionSequence = (self.sessionSequence or 0) + 1

    local sessionId = string.format("%s:%s:%d:%d", peer or "unknown", tableName or "na", time(), self.sessionSequence)

    self.activeSessions[sessionId] = {
        sessionId = sessionId,
        peer = peer,
        tableName = tableName,
        total = total or 0,
        seq = 0,
        status = "active",
        retries = 0,
        startedAt = time(),
        metadata = metadata,
        stats = createSessionStats(),
    }

    return sessionId
end

function addon.SessionManager:RegisterSession(sessionId, peer, tableName, total, metadata)
    if not sessionId or sessionId == "" then
        return nil
    end

    local session = self.activeSessions[sessionId]

    if not session then
        session = {
            sessionId = sessionId,
            peer = peer,
            tableName = tableName,
            total = total or 0,
            seq = 0,
            status = "active",
            retries = 0,
            startedAt = time(),
            metadata = metadata,
            stats = createSessionStats(),
        }
        self.activeSessions[sessionId] = session
        return session
    end

    if peer and peer ~= "" then
        session.peer = peer
    end

    if tableName and tableName ~= "" then
        session.tableName = tableName
    end

    if total and total > 0 then
        session.total = total
    end

    if metadata then
        session.metadata = session.metadata or {}

        for key, value in pairs(metadata) do
            session.metadata[key] = value
        end
    end

    if session.status ~= "cancelled" and session.status ~= "failed" and session.status ~= "complete" then
        session.status = "active"
    end

    session.lastSeenAt = time()
    return session
end

function addon.SessionManager:IsSessionActive(session)
    return session and session.status == "active"
end

function addon.SessionManager:GetSession(sessionId)
    return self.activeSessions[sessionId]
end

function addon.SessionManager:IsWaitingForPeerReply(session)
    return isWaitingForPeerReply(session)
end

function addon.SessionManager:GetWaitingForPeerReplyLabel(session)
    if not isWaitingForPeerReply(session) then
        return nil
    end

    return getWaitingForPeerReplyLabel(session)
end

function addon.SessionManager:FindActiveSession(peer, tableName)
    for _, session in pairs(self.activeSessions) do
        if self:IsSessionActive(session) and session.peer == peer and session.tableName == tableName then
            return session
        end
    end

    return nil
end

function addon.SessionManager:FindActivePeerSession(peer)
    for _, session in pairs(self.activeSessions) do
        if self:IsSessionActive(session) and session.peer == peer then
            return session
        end
    end

    return nil
end

function addon.SessionManager:BuildDiagnostics()
    local diagnostics = {
        total = 0,
        active = 0,
        complete = 0,
        failed = 0,
        cancelled = 0,
        newestAge = 0,
        oldestActiveAge = 0,
    }

    local now = time()

    for _, session in pairs(self.activeSessions) do
        diagnostics.total = diagnostics.total + 1

        if session.status == "active" then
            diagnostics.active = diagnostics.active + 1
            diagnostics.oldestActiveAge = math.max(diagnostics.oldestActiveAge, now - (session.startedAt or now))
        elseif session.status == "complete" then
            diagnostics.complete = diagnostics.complete + 1
        elseif session.status == "failed" then
            diagnostics.failed = diagnostics.failed + 1
        elseif session.status == "cancelled" then
            diagnostics.cancelled = diagnostics.cancelled + 1
        end

        diagnostics.newestAge = math.max(diagnostics.newestAge, now - (session.startedAt or now))
    end

    return diagnostics
end

function addon.SessionManager:CancelSession(sessionId, message)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.status = "cancelled"
    session.lastError = message or "sync cancelled"
    session.completedAt = time()
    mergeRosterSessionSummary(session)
    return session
end

function addon.SessionManager:CancelAllActive(message)
    local cancelled = 0

    for sessionId, session in pairs(self.activeSessions) do
        if self:IsSessionActive(session) then
            self:CancelSession(sessionId, message)
            cancelled = cancelled + 1
        end
    end

    return cancelled
end

function addon.SessionManager:Advance(sessionId)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.seq = session.seq + 1
    return session
end

function addon.SessionManager:RecordOutboundEnvelope(sessionId, op, messageBytes)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.stats = session.stats or createSessionStats()
    bumpMetric(session.stats.outboundChunksByOp, op or "UNKNOWN", 1)
    bumpMetric(session.stats.outboundBytesByOp, op or "UNKNOWN", messageBytes or 0)
    return session
end

function addon.SessionManager:RecordInboundEnvelope(sessionId, op, messageBytes)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.stats = session.stats or createSessionStats()
    bumpMetric(session.stats.inboundChunksByOp, op or "UNKNOWN", 1)
    bumpMetric(session.stats.inboundBytesByOp, op or "UNKNOWN", messageBytes or 0)
    return session
end

function addon.SessionManager:AddRequestedFromPeer(sessionId, count)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.stats = session.stats or createSessionStats()
    session.stats.recordsRequestedFromPeer = (session.stats.recordsRequestedFromPeer or 0) + (count or 0)
    return session
end

function addon.SessionManager:AddRequestedByPeer(sessionId, count)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.stats = session.stats or createSessionStats()
    session.stats.recordsRequestedByPeer = (session.stats.recordsRequestedByPeer or 0) + (count or 0)
    return session
end

function addon.SessionManager:SetTotal(sessionId, total)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.total = total or session.total
    return session
end

function addon.SessionManager:Touch(sessionId)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.lastSeenAt = time()
    return session
end

function addon.SessionManager:CompleteSession(sessionId)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.status = "complete"
    session.completedAt = time()
    mergeRosterSessionSummary(session)
    return session
end

function addon.SessionManager:FailSession(sessionId, message)
    local session = self:GetSession(sessionId)

    if not session then
        return nil
    end

    session.status = "failed"
    session.lastError = message
    session.completedAt = time()
    mergeRosterSessionSummary(session)
    return session
end

function addon.SessionManager:CleanupExpired(now)
    local currentTime = now or time()
    local cleared = 0

    for sessionId, session in pairs(self.activeSessions) do
        local referenceTime = session.lastSeenAt or session.startedAt or currentTime
        local timeoutSeconds = getSessionTimeoutSeconds(session)
        local timeoutMessage = "sync session timed out"

        if isWaitingForPeerReply(session) then
            timeoutSeconds = math.min(timeoutSeconds, INITIAL_PEER_REPLY_TIMEOUT_SECONDS)
            timeoutMessage = (getWaitingForPeerReplyLabel(session) or "waiting for peer reply"):gsub("^waiting", "timed out waiting", 1)
        end

        if self:IsSessionActive(session) and (currentTime - referenceTime) > timeoutSeconds then
            session.status = "failed"
            session.lastError = timeoutMessage
            session.completedAt = currentTime
            mergeRosterSessionSummary(session)
            cleared = cleared + 1
        elseif not self:IsSessionActive(session) and (currentTime - (session.completedAt or referenceTime)) > SESSION_RETENTION_SECONDS then
            self.activeSessions[sessionId] = nil
            cleared = cleared + 1
        end
    end

    return cleared
end

function addon.SessionManager:ListActive()
    local sessions = {}

    for _, session in pairs(self.activeSessions) do
        if self:IsSessionActive(session) then
            table.insert(sessions, session)
        end
    end

    table.sort(sessions, function(left, right)
        return (left.startedAt or 0) > (right.startedAt or 0)
    end)

    return sessions
end
