using System.Collections.Concurrent;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;

/// <summary>
/// Tracks which sessions currently have at least one live <see cref="EventsHub"/>
/// subscriber, so <see cref="DashboardEventBroadcaster"/> can skip the redaction
/// clone and the group send for sessions nobody is watching.
/// </summary>
/// <remarks>
/// SignalR does not expose group membership, so the hub mirrors its own
/// <c>AddToGroup</c>/<c>RemoveFromGroup</c> calls here. In the steady state no
/// browser is on a session-details page, yet every session's dashboard-mirror
/// subscriber still called <c>Publish</c> for every event — a deep protobuf
/// clone (values are redacted by default) plus a send to an empty group, per
/// event, thrown away. This registry is the cheap gate in front of that work.
/// <para>
/// Per-connection subscriptions are tracked as well, because a browser tab that
/// simply goes away never calls <c>UnsubscribeSession</c>; the hub's
/// <c>OnDisconnectedAsync</c> releases everything the connection held.
/// </para>
/// </remarks>
public sealed class EventsHubViewerRegistry
{
    private readonly ConcurrentDictionary<string, int> _viewersBySession = new(StringComparer.Ordinal);

    private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> _sessionsByConnection =
        new(StringComparer.Ordinal);

    /// <summary>
    /// Records that <paramref name="connectionId"/> is watching
    /// <paramref name="sessionId"/>. Repeat calls for the same pair are
    /// idempotent, so one <see cref="RemoveViewer"/> always clears them.
    /// </summary>
    /// <param name="connectionId">SignalR connection id of the subscriber.</param>
    /// <param name="sessionId">Session id being watched.</param>
    public void AddViewer(string connectionId, string sessionId)
    {
        if (string.IsNullOrWhiteSpace(connectionId) || string.IsNullOrWhiteSpace(sessionId))
        {
            return;
        }

        ConcurrentDictionary<string, byte> sessions = _sessionsByConnection.GetOrAdd(
            connectionId,
            static _ => new ConcurrentDictionary<string, byte>(StringComparer.Ordinal));

        // The per-connection set is the source of truth for the count: only a
        // subscription that was genuinely new increments the session's viewers.
        if (!sessions.TryAdd(sessionId, 0))
        {
            return;
        }

        _viewersBySession.AddOrUpdate(sessionId, 1, static (_, count) => count + 1);
    }

    /// <summary>
    /// Records that <paramref name="connectionId"/> stopped watching
    /// <paramref name="sessionId"/>. A removal with no matching
    /// <see cref="AddViewer"/> is a no-op, so the count cannot go negative.
    /// </summary>
    /// <param name="connectionId">SignalR connection id of the subscriber.</param>
    /// <param name="sessionId">Session id no longer being watched.</param>
    public void RemoveViewer(string connectionId, string sessionId)
    {
        if (string.IsNullOrWhiteSpace(connectionId) || string.IsNullOrWhiteSpace(sessionId))
        {
            return;
        }

        if (!_sessionsByConnection.TryGetValue(connectionId, out ConcurrentDictionary<string, byte>? sessions)
            || !sessions.TryRemove(sessionId, out _))
        {
            return;
        }

        ReleaseSession(sessionId);
    }

    /// <summary>
    /// Releases every subscription held by <paramref name="connectionId"/>.
    /// Called from the hub's disconnect callback, which is the only reliable
    /// signal for a browser tab that closed without unsubscribing.
    /// </summary>
    /// <param name="connectionId">SignalR connection id that dropped.</param>
    public void ReleaseConnection(string connectionId)
    {
        if (string.IsNullOrWhiteSpace(connectionId))
        {
            return;
        }

        // Detaching the set is safe against a SubscribeSession that arrives after the disconnect
        // only because SignalR dispatches a connection's hub invocations sequentially by default
        // (MaximumParallelInvocationsPerClient = 1): OnDisconnectedAsync cannot overlap an
        // AddViewer for the same connection, so no late add can re-create the entry and leak a
        // count that nothing will ever release. Raising that option would break this.
        if (!_sessionsByConnection.TryRemove(connectionId, out ConcurrentDictionary<string, byte>? sessions))
        {
            return;
        }

        foreach (string sessionId in sessions.Keys)
        {
            // TryRemove, not a bare enumeration: a concurrent RemoveViewer on the
            // same detached set must not let the session be decremented twice.
            if (sessions.TryRemove(sessionId, out _))
            {
                ReleaseSession(sessionId);
            }
        }
    }

    /// <summary>Gets a value indicating whether any hub connection is watching the session.</summary>
    /// <param name="sessionId">Session id to test.</param>
    /// <returns><see langword="true"/> when at least one connection is subscribed.</returns>
    public bool HasViewers(string sessionId) =>
        !string.IsNullOrEmpty(sessionId)
        && _viewersBySession.TryGetValue(sessionId, out int count)
        && count > 0;

    /// <summary>
    /// Decrements the session's viewer count, dropping the entry entirely at
    /// zero so the dictionary does not grow one key per session ever viewed.
    /// The compare-and-swap loop keeps the decrement correct against a
    /// concurrent <see cref="AddViewer"/> on the same session.
    /// </summary>
    /// <param name="sessionId">Session id whose count is released.</param>
    private void ReleaseSession(string sessionId)
    {
        while (true)
        {
            if (!_viewersBySession.TryGetValue(sessionId, out int count))
            {
                return;
            }

            if (count <= 1)
            {
                if (_viewersBySession.TryRemove(new KeyValuePair<string, int>(sessionId, count)))
                {
                    return;
                }
            }
            else if (_viewersBySession.TryUpdate(sessionId, count - 1, count))
            {
                return;
            }
        }
    }
}
