namespace Mbproxy.Admin;

/// <summary>
/// Tracks which SignalR connections are subscribed to which PLC detail pages, so the
/// admin layer knows (a) when to arm / disarm a PLC's tag-value capture — capture is
/// armed only while at least one detail page is open — and (b) which PLC groups the
/// <see cref="StatusBroadcaster"/> needs to push to.
///
/// <para>Registered as a DI singleton; <see cref="StatusHub"/> instances (transient,
/// one per hub call) share this single tracker. All methods are thread-safe under a
/// single lock — subscription churn is low-frequency (one event per detail-page
/// open/close), so lock contention is a non-issue.</para>
/// </summary>
internal sealed class PlcSubscriptionTracker
{
    private readonly object _gate = new();

    // PLC name → number of connections currently subscribed to its detail page.
    private readonly Dictionary<string, int> _plcCounts = new(StringComparer.Ordinal);

    // Connection id → the set of PLC names that connection is subscribed to.
    private readonly Dictionary<string, HashSet<string>> _byConnection = new(StringComparer.Ordinal);

    /// <summary>
    /// Records that <paramref name="connectionId"/> subscribed to <paramref name="plcName"/>.
    /// Returns <c>true</c> when this is the PLC's first subscriber (count 0 → 1), the
    /// signal to arm its capture. Returns <c>false</c> for a redundant re-subscribe.
    /// </summary>
    public bool Add(string connectionId, string plcName)
    {
        lock (_gate)
        {
            if (!_byConnection.TryGetValue(connectionId, out var set))
                _byConnection[connectionId] = set = new HashSet<string>(StringComparer.Ordinal);

            if (!set.Add(plcName))
                return false; // this connection was already subscribed to this PLC

            int count = _plcCounts.GetValueOrDefault(plcName);
            _plcCounts[plcName] = count + 1;
            return count == 0;
        }
    }

    /// <summary>
    /// Drops every subscription held by <paramref name="connectionId"/> (called from
    /// <see cref="StatusHub.OnDisconnectedAsync"/>). Returns the PLC names whose
    /// subscriber count fell to 0 — the signal to disarm their captures.
    /// </summary>
    public IReadOnlyList<string> RemoveConnection(string connectionId)
    {
        lock (_gate)
        {
            if (!_byConnection.Remove(connectionId, out var set))
                return Array.Empty<string>();

            var dropped = new List<string>();
            foreach (var plcName in set)
            {
                int count = _plcCounts.GetValueOrDefault(plcName);
                if (count <= 1)
                {
                    _plcCounts.Remove(plcName);
                    dropped.Add(plcName);
                }
                else
                {
                    _plcCounts[plcName] = count - 1;
                }
            }
            return dropped;
        }
    }

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