using System.Collections.Concurrent;
using Mbproxy.Bcd;

namespace Mbproxy.Proxy;

/// <summary>
/// Process-wide registry of per-PLC <see cref="TagValueCapture"/> instances — the
/// shared seam between the proxy hot path (which records tag values) and the admin
/// layer (which arms captures on detail-page open and reads their snapshots).
///
/// <para>Registered as a DI singleton. <see cref="ProxyWorker"/> and
/// <see cref="Configuration.ConfigReconciler"/> call <see cref="GetOrCreate"/> as they
/// build each <see cref="PerPlcContext"/>; <see cref="Configuration.ConfigReconciler"/>
/// calls <see cref="Remove"/> for hot-reload-removed PLCs. <c>StatusBroadcaster</c>
/// calls <see cref="ReconcileArmed"/> every push cycle (the single arm/disarm authority)
/// and <see cref="DisarmAll"/> on shutdown; <c>StatusSnapshotBuilder</c> calls
/// <see cref="TryGet"/>.</para>
/// </summary>
internal sealed class TagCaptureRegistry
{
    private readonly ConcurrentDictionary<string, TagValueCapture> _captures =
        new(StringComparer.Ordinal);

    /// <summary>
    /// Returns the capture for <paramref name="plcName"/>, creating it on first call.
    /// A subsequent call (hot-reload reseat/restart, where the tag set may have changed)
    /// rebuilds the capture for <paramref name="map"/>'s current tags. The rebuilt
    /// capture is disarmed; <see cref="StatusBroadcaster"/> re-arms it on its next push
    /// cycle (within one <c>AdminPushIntervalMs</c>) if the PLC still has a viewer — so
    /// arm state is never carried across the rebuild, which removes any race between
    /// arming and the rebuild.
    /// </summary>
    public TagValueCapture GetOrCreate(string plcName, BcdTagMap map)
        => _captures.AddOrUpdate(
            plcName,
            _ => new TagValueCapture(map.All),
            (_, _) => new TagValueCapture(map.All));

    /// <summary>Drops the capture for a hot-reload-removed PLC.</summary>
    public void Remove(string plcName) => _captures.TryRemove(plcName, out _);

    /// <summary>Looks up a PLC's capture; false when the PLC is unknown.</summary>
    public bool TryGet(string plcName, out TagValueCapture capture)
        => _captures.TryGetValue(plcName, out capture!);

    /// <summary>
    /// Reconciles every capture's armed state against <paramref name="activePlcs"/> —
    /// the set of PLCs that currently have a detail-page viewer. Captures for active
    /// PLCs are armed, all others disarmed. Called once per push cycle by
    /// <see cref="StatusBroadcaster"/>, so it is the <b>single</b> arm/disarm authority:
    /// no hub thread ever arms a capture, which both removes the race against a
    /// hot-reload <see cref="GetOrCreate"/> and makes a leaked subscriber impossible to
    /// reach here (the tracker is reconnect-safe).
    /// </summary>
    public void ReconcileArmed(IReadOnlyCollection<string> activePlcs)
    {
        var active = activePlcs as IReadOnlySet<string>
                     ?? activePlcs.ToHashSet(StringComparer.Ordinal);

        foreach (var (name, capture) in _captures)
        {
            bool shouldArm = active.Contains(name);
            if (shouldArm && !capture.IsArmed)
                capture.Arm();
            else if (!shouldArm && capture.IsArmed)
                capture.Disarm();
        }
    }

    /// <summary>
    /// Disarms every capture. Called when the admin endpoint stops (e.g. AdminPort
    /// hot-reload tears down the SignalR host) so no capture is left armed with no viewer.
    /// </summary>
    public void DisarmAll()
    {
        foreach (var c in _captures.Values)
            c.Disarm();
    }
}
