using System.Threading.Channels;
using Akka;
using Akka.Actor;
using Akka.Streams;
using Akka.Streams.Dsl;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;

/// <summary>
/// Site-Wide Akka Stream — manages a broadcast stream for attribute value
/// and alarm state changes. Instance Actors publish events via fire-and-forget Tell.
/// A BroadcastHub fans events out to per-subscriber graphs, each filtered by
/// instance name and bounded by a drop-oldest buffer.
///
/// Filterable by instance name for debug view.
/// Implements ISiteStreamSubscriber so the gRPC server can subscribe actors
/// to instance events without referencing SiteRuntime directly.
/// </summary>
/// <remarks>
/// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): attribute
/// and alarm events used to share ONE upstream <c>Source.ActorRef</c> + BroadcastHub
/// pair, so a burst of attribute changes could fill that shared buffer and evict a
/// pending alarm transition before any subscriber ever saw it. Attribute and alarm
/// events now travel through entirely SEPARATE publish sources/hubs
/// (<see cref="_attributeSourceActor"/>/<see cref="_attributeHubSource"/> vs.
/// <see cref="_alarmSourceActor"/>/<see cref="_alarmHubSource"/>), so an attribute storm
/// can only ever evict other attribute events. The alarm path additionally stages
/// through a bounded <see cref="Channel{T}"/> (<see cref="_alarmPublishQueue"/>) so drops
/// are counted (<see cref="AlarmPublishDroppedCount"/>) — <c>Source.ActorRef</c>'s
/// built-in DropHead overflow has no drop callback to instrument. Publishing is also
/// skipped entirely when nobody is subscribed to that source (see
/// <see cref="_attributeSubscriberCount"/>/<see cref="_alarmSubscriberCount"/>).
/// </remarks>
public class SiteStreamManager : ISiteStreamSubscriber
{
    /// <summary>Sentinel instance name recorded for site-wide (non-instance-scoped) subscriptions.</summary>
    private const string SiteWideInstanceName = "*";

    private ActorSystem? _system;
    private IMaterializer? _materializer;
    private readonly int _bufferSize;
    private readonly int _alarmPublishQueueCapacity;
    private readonly ILogger<SiteStreamManager> _logger;
    private readonly object _lock = new();

    // Attribute-only publish source/hub. By far the higher-volume of the two —
    // deliberately kept on Source.ActorRef's built-in bounded-mailbox + DropHead
    // behavior (unchanged from before WP2.6d); losing an occasional attribute update
    // under a storm is the accepted, pre-existing trade-off.
    private IActorRef? _attributeSourceActor;
    private Source<ISiteStreamEvent, NotUsed>? _attributeHubSource;

    // Alarm-only publish source/hub — isolated from the attribute path above so an
    // attribute storm can never evict a pending alarm transition.
    private IActorRef? _alarmSourceActor;
    private Source<ISiteStreamEvent, NotUsed>? _alarmHubSource;

    // Bounded hand-off queue feeding the alarm source actor. Staging alarm publishes
    // through this (rather than Tell-ing _alarmSourceActor directly) is what makes
    // AlarmPublishDroppedCount possible — Source.ActorRef's own DropHead overflow has
    // no drop callback to observe.
    private Channel<AlarmStateChanged>? _alarmPublishQueue;
    private Task? _alarmPump;
    private long _alarmPublishDroppedCount;

    /// <summary>
    /// Cumulative count of alarm state changes dropped because
    /// <see cref="_alarmPublishQueue"/> was at capacity (WP2.6d) — i.e. the site-wide
    /// alarm hand-off queue itself fell behind, not the downstream per-subscriber
    /// buffers. Surfaced on the site health report.
    /// </summary>
    public long AlarmPublishDroppedCount => Interlocked.Read(ref _alarmPublishDroppedCount);

    // Per-source subscriber counts (WP2.6d "skip publish at zero subscribers").
    // A per-instance Subscribe touches BOTH sources (it needs both attribute and alarm
    // events for that instance); SubscribeSiteAlarms touches only the alarm source.
    private int _attributeSubscriberCount;
    private int _alarmSubscriberCount;

    private readonly Dictionary<string, SubscriptionInfo> _subscriptions = new();

    /// <summary>Initializes the stream manager with configuration and logger; the Akka stream is not started until <see cref="Initialize"/> is called.</summary>
    /// <param name="options">Site runtime options providing the stream buffer size.</param>
    /// <param name="logger">Logger instance.</param>
    public SiteStreamManager(
        SiteRuntimeOptions options,
        ILogger<SiteStreamManager> logger)
    {
        _bufferSize = options.StreamBufferSize;
        _alarmPublishQueueCapacity = options.AlarmPublishQueueCapacity;
        _logger = logger;
    }

    /// <summary>
    /// Initializes the broadcast streams (one for attribute events, one for alarm
    /// events — see the type-level remarks) and starts the alarm hand-off pump. Must be
    /// called after ActorSystem is ready. The ActorSystem is passed here rather than via
    /// the constructor so that SiteStreamManager can be created by DI before the actor
    /// system exists.
    /// </summary>
    /// <param name="system">The running Akka <see cref="ActorSystem"/> used to materialize the broadcast streams.</param>
    public void Initialize(ActorSystem system)
    {
        _system = system;
        _materializer = _system.Materializer();

        var (attributeSourceActor, attributeHubSource) = Source.ActorRef<ISiteStreamEvent>(
                _bufferSize,
                OverflowStrategy.DropHead)
            .ToMaterialized(
                BroadcastHub.Sink<ISiteStreamEvent>(bufferSize: 256),
                Keep.Both)
            .Run(_materializer);
        _attributeSourceActor = attributeSourceActor;
        _attributeHubSource = attributeHubSource;

        var (alarmSourceActor, alarmHubSource) = Source.ActorRef<ISiteStreamEvent>(
                _bufferSize,
                OverflowStrategy.DropHead)
            .ToMaterialized(
                BroadcastHub.Sink<ISiteStreamEvent>(bufferSize: 256),
                Keep.Both)
            .Run(_materializer);
        _alarmSourceActor = alarmSourceActor;
        _alarmHubSource = alarmHubSource;

        _alarmPublishQueue = CreateAlarmPublishQueue(_alarmPublishQueueCapacity, () =>
        {
            var total = Interlocked.Increment(ref _alarmPublishDroppedCount);
            _logger.LogWarning(
                "Alarm publish queue exceeded its bounded capacity ({Capacity}); " +
                "oldest pending alarm transition dropped (total dropped: {Dropped})",
                _alarmPublishQueueCapacity, total);
        });

        var alarmQueue = _alarmPublishQueue;
        var alarmActor = _alarmSourceActor;
        _alarmPump = Task.Run(async () =>
        {
            await foreach (var changed in alarmQueue.Reader.ReadAllAsync())
            {
                alarmActor.Tell(changed);
            }
        });

        _logger.LogInformation(
            "SiteStreamManager initialized with publish buffer size {BufferSize} " +
            "and alarm publish queue capacity {AlarmQueueCapacity}",
            _bufferSize, _alarmPublishQueueCapacity);
    }

    /// <summary>
    /// Builds the bounded, single-reader alarm hand-off queue with DropOldest overflow,
    /// invoking <paramref name="onDropped"/> on every eviction. Extracted as a pure,
    /// testable helper (internal — see <c>InternalsVisibleTo</c>) so the drop-counting
    /// wiring can be verified deterministically, without racing the async pump/actor
    /// system against a burst of publishes.
    /// </summary>
    internal static Channel<AlarmStateChanged> CreateAlarmPublishQueue(int capacity, Action onDropped) =>
        Channel.CreateBounded<AlarmStateChanged>(
            new BoundedChannelOptions(Math.Max(1, capacity))
            {
                SingleReader = true,
                SingleWriter = false,
                FullMode = BoundedChannelFullMode.DropOldest,
            },
            itemDropped: _ => onDropped());

    /// <summary>
    /// Publishes an attribute value change to the attribute broadcast hub.
    /// Fire-and-forget — never blocks the calling actor. Skipped entirely when no
    /// subscriber is currently interested in attribute events (WP2.6d).
    /// </summary>
    /// <param name="changed">The attribute value change event to publish.</param>
    public void PublishAttributeValueChanged(AttributeValueChanged changed)
    {
        if (Volatile.Read(ref _attributeSubscriberCount) == 0)
            return;

        _attributeSourceActor?.Tell(changed);
    }

    /// <summary>
    /// Publishes an alarm state change to the DEDICATED alarm broadcast hub — isolated
    /// from the (far higher-volume) attribute path, so an attribute storm can never
    /// evict a pending alarm transition. Fire-and-forget — never blocks the calling
    /// actor. Skipped entirely when no subscriber is currently interested in alarm
    /// events (WP2.6d).
    /// </summary>
    /// <param name="changed">The alarm state change event to publish.</param>
    public void PublishAlarmStateChanged(AlarmStateChanged changed)
    {
        if (Volatile.Read(ref _alarmSubscriberCount) == 0)
            return;

        _alarmPublishQueue?.Writer.TryWrite(changed);
    }

    /// <inheritdoc />
    public string Subscribe(string instanceName, IActorRef subscriber)
    {
        if (_attributeHubSource is null || _alarmHubSource is null || _materializer is null)
            throw new InvalidOperationException("SiteStreamManager.Initialize must be called before Subscribe");

        var subscriptionId = Guid.NewGuid().ToString();
        var capturedInstance = instanceName;
        var capturedSubscriber = subscriber;

        // Two independent graphs — one per source — both forwarding to the same
        // subscriber actor. The actor's own mailbox serializes delivery; this instance
        // subscription (used by Debug View) does not need cross-source ordering
        // guarantees, only that alarm events for it are never lost to an attribute
        // storm on a DIFFERENT instance sharing the same (now attribute-only) hub.
        var attributeKillSwitch = _attributeHubSource
            .Where(ev => ev.InstanceUniqueName == capturedInstance)
            .Buffer(_bufferSize, OverflowStrategy.DropHead)
            .ViaMaterialized(KillSwitches.Single<ISiteStreamEvent>(), Keep.Right)
            .To(Sink.ForEach<ISiteStreamEvent>(ev => capturedSubscriber.Tell(ev)))
            .Run(_materializer);

        var alarmKillSwitch = _alarmHubSource
            .Where(ev => ev.InstanceUniqueName == capturedInstance)
            .Buffer(_bufferSize, OverflowStrategy.DropHead)
            .ViaMaterialized(KillSwitches.Single<ISiteStreamEvent>(), Keep.Right)
            .To(Sink.ForEach<ISiteStreamEvent>(ev => capturedSubscriber.Tell(ev)))
            .Run(_materializer);

        lock (_lock)
        {
            _subscriptions[subscriptionId] = new SubscriptionInfo(
                instanceName, subscriber,
                new[] { attributeKillSwitch, alarmKillSwitch },
                TouchesAttributes: true, TouchesAlarms: true,
                DateTimeOffset.UtcNow);
        }

        Interlocked.Increment(ref _attributeSubscriberCount);
        Interlocked.Increment(ref _alarmSubscriberCount);

        _logger.LogDebug(
            "Subscriber {SubscriptionId} registered for instance {Instance}",
            subscriptionId, instanceName);

        return subscriptionId;
    }

    /// <summary>
    /// Subscribe to ALARM events for ALL instances on the site (no per-instance
    /// filter). The dedicated alarm hub carries only <see cref="AlarmStateChanged"/>
    /// events (the <c>Where</c> below is a defensive no-op, not a load-bearing filter —
    /// see the type-level remarks); the aggregated Alarm Summary never sees attribute
    /// events. Same broadcast-hub wiring as <see cref="Subscribe"/>, and the returned
    /// subscription id is torn down via <see cref="Unsubscribe"/> exactly like the
    /// per-instance variant.
    /// </summary>
    /// <param name="subscriber">The actor that receives forwarded <see cref="AlarmStateChanged"/> events.</param>
    /// <returns>A subscription id to pass to <see cref="Unsubscribe"/>.</returns>
    public string SubscribeSiteAlarms(IActorRef subscriber)
    {
        if (_alarmHubSource is null || _materializer is null)
            throw new InvalidOperationException("SiteStreamManager.Initialize must be called before SubscribeSiteAlarms");

        var subscriptionId = Guid.NewGuid().ToString();
        var capturedSubscriber = subscriber;

        var killSwitch = _alarmHubSource
            .Where(ev => ev is AlarmStateChanged)
            .Buffer(_bufferSize, OverflowStrategy.DropHead)
            .ViaMaterialized(KillSwitches.Single<ISiteStreamEvent>(), Keep.Right)
            .To(Sink.ForEach<ISiteStreamEvent>(ev => capturedSubscriber.Tell(ev)))
            .Run(_materializer);

        lock (_lock)
        {
            _subscriptions[subscriptionId] = new SubscriptionInfo(
                SiteWideInstanceName, subscriber,
                new[] { killSwitch },
                TouchesAttributes: false, TouchesAlarms: true,
                DateTimeOffset.UtcNow);
        }

        Interlocked.Increment(ref _alarmSubscriberCount);

        _logger.LogDebug(
            "Subscriber {SubscriptionId} registered for site-wide alarm events", subscriptionId);

        return subscriptionId;
    }

    /// <summary>
    /// Unsubscribe from instance events. Shuts down the per-subscriber
    /// stream graph(s) via their KillSwitch(es).
    /// </summary>
    /// <param name="subscriptionId">The subscription ID returned by <see cref="Subscribe"/>.</param>
    /// <returns><c>true</c> if the subscription was found and removed; <c>false</c> if it was already gone.</returns>
    public bool Unsubscribe(string subscriptionId)
    {
        SubscriptionInfo? info;
        lock (_lock)
        {
            if (!_subscriptions.Remove(subscriptionId, out info))
                return false;
        }

        TearDown(info);
        _logger.LogDebug("Subscriber {SubscriptionId} removed", subscriptionId);
        return true;
    }

    /// <inheritdoc />
    public void RemoveSubscriber(IActorRef subscriber)
    {
        List<SubscriptionInfo> toShutdown;
        lock (_lock)
        {
            var matched = _subscriptions
                .Where(kvp => kvp.Value.Subscriber.Equals(subscriber))
                .ToList();
            foreach (var kvp in matched)
                _subscriptions.Remove(kvp.Key);
            toShutdown = matched.Select(kvp => kvp.Value).ToList();
        }

        foreach (var info in toShutdown)
            TearDown(info);

        if (toShutdown.Count > 0)
        {
            _logger.LogDebug(
                "Removed {Count} subscriptions for disconnected subscriber", toShutdown.Count);
        }
    }

    /// <summary>Shuts down every kill switch for a subscription and decrements the matching per-source counters.</summary>
    private void TearDown(SubscriptionInfo info)
    {
        foreach (var killSwitch in info.KillSwitches)
            killSwitch.Shutdown();

        if (info.TouchesAttributes)
            Interlocked.Decrement(ref _attributeSubscriberCount);
        if (info.TouchesAlarms)
            Interlocked.Decrement(ref _alarmSubscriberCount);
    }

    /// <summary>
    /// Returns the count of active subscriptions (for diagnostics/testing).
    /// </summary>
    public int SubscriptionCount
    {
        get { lock (_lock) { return _subscriptions.Count; } }
    }

    private record SubscriptionInfo(
        string InstanceName,
        IActorRef Subscriber,
        IReadOnlyList<IKillSwitch> KillSwitches,
        bool TouchesAttributes,
        bool TouchesAlarms,
        DateTimeOffset SubscribedAt);
}
