namespace Mbproxy.Admin;

/// <summary>
/// Tracks which browser tabs currently have a PLC connection-detail page open, so the
/// admin layer knows which PLC tag-value captures should be armed and which PLC groups
/// the <see cref="StatusBroadcaster"/> needs to push to.
///
/// <para><b>Why tabs, not SignalR connections.</b> A SignalR connection is assigned a
/// fresh <c>ConnectionId</c> on every transport reconnect (a WebSocket drop, a
/// long-polling cycle, a network blip). Counting <em>connections</em> therefore leaks a
/// subscriber on every reconnect — the old connection's <c>OnDisconnectedAsync</c> is
/// not ordered against the new connection's re-subscribe, so the count never returns to
/// 0 and the capture stays armed forever with no viewer. Instead each detail page sends
/// a stable per-page-load <c>tabId</c>: a tab "views" a PLC for as long as it has at
/// least one live connection. A reconnect is just the same tab acquiring a new
/// connection, so it cannot leak; a tab is released only when its <em>last</em>
/// connection is gone (the page closed, or SignalR's keepalive timeout elapsed for an
/// abruptly-killed tab).</para>
///
/// <para>Registered as a DI singleton; the transient <see cref="StatusHub"/> instances
/// share this one tracker. All methods are thread-safe under a single lock —
/// subscription churn is low-frequency (one event per detail-page open / close /
/// reconnect), so lock contention is a non-issue. The tracker never arms or disarms a
/// capture itself — <see cref="StatusBroadcaster"/> reconciles arm state each push
/// cycle from <see cref="ActivePlcs"/>, which keeps arming single-threaded.</para>
/// </summary>
internal sealed class PlcSubscriptionTracker
{
    /// <summary>Live state for one browser tab: its connections and the PLCs it views.</summary>
    private sealed class TabState
    {
        public readonly HashSet<string> Connections = new(StringComparer.Ordinal);
        public readonly HashSet<string> Plcs = new(StringComparer.Ordinal);
    }

    private readonly object _gate = new();

    // tabId → tab state (live connections + the PLC detail pages it has open).
    private readonly Dictionary<string, TabState> _tabs = new(StringComparer.Ordinal);

    // connectionId → owning tabId, so a disconnect can find (and decrement) its tab.
    private readonly Dictionary<string, string> _connToTab = new(StringComparer.Ordinal);

    // PLC name → number of distinct tabs currently viewing its detail page.
    private readonly Dictionary<string, int> _plcViewerTabs = new(StringComparer.Ordinal);

    /// <summary>
    /// Records that connection <paramref name="connectionId"/>, belonging to browser tab
    /// <paramref name="tabId"/>, has the detail page for <paramref name="plcName"/> open.
    /// Idempotent: a reconnect (same tab, new connection) or a repeated call for an
    /// already-subscribed tag does not double-count the tab.
    /// </summary>
    public void SubscribePlc(string connectionId, string tabId, string plcName)
    {
        lock (_gate)
        {
            if (!_tabs.TryGetValue(tabId, out var tab))
                _tabs[tabId] = tab = new TabState();

            tab.Connections.Add(connectionId);
            _connToTab[connectionId] = tabId;

            if (tab.Plcs.Add(plcName))
                _plcViewerTabs[plcName] = _plcViewerTabs.GetValueOrDefault(plcName) + 1;
        }
    }

    /// <summary>
    /// Drops connection <paramref name="connectionId"/>. If it was the last live
    /// connection of its tab, the tab is released and its PLC subscriptions decremented.
    /// A still-live sibling connection (reconnect overlap) keeps the tab — and its
    /// captures — alive. Safe to call for an unknown / fleet-only connection (no-op).
    /// </summary>
    public void RemoveConnection(string connectionId)
    {
        lock (_gate)
        {
            if (!_connToTab.Remove(connectionId, out var tabId))
                return;
            if (!_tabs.TryGetValue(tabId, out var tab))
                return;

            tab.Connections.Remove(connectionId);
            if (tab.Connections.Count > 0)
                return; // the tab is still alive on another connection

            _tabs.Remove(tabId);
            foreach (var plcName in tab.Plcs)
            {
                int count = _plcViewerTabs.GetValueOrDefault(plcName);
                if (count <= 1)
                    _plcViewerTabs.Remove(plcName);
                else
                    _plcViewerTabs[plcName] = count - 1;
            }
        }
    }

    /// <summary>PLC names that currently have at least one detail-page tab open.</summary>
    public IReadOnlyList<string> ActivePlcs()
    {
        lock (_gate)
        {
            return _plcViewerTabs.Count == 0
                ? Array.Empty<string>()
                : _plcViewerTabs.Keys.ToArray();
        }
    }
}
