using Akka.Actor;
using Akka.Event;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

namespace ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;

/// <summary>
///     Per-node fan-out router from <see cref="DriverInstanceActor.AttributeValuePublished"/>
///     to interested <see cref="VirtualTagActor"/> instances. VirtualTagActor sends
///     <see cref="RegisterInterest"/> on start-up listing the tag refs it depends on; the mux
///     keeps a map of <c>tagRef → Set&lt;IActorRef&gt;</c> and on every AttributeValuePublished
///     forwards a <see cref="VirtualTagActor.DependencyValueChanged"/> to each interested
///     subscriber.
///
///     DriverHostActor forwards every <c>AttributeValuePublished</c> it receives from its
///     DriverInstanceActor children to this mux (one mux per driver-role node). The mux is
///     deliberately not a DPS subscriber — virtual-tag evaluation is local to each node and
///     would over-emit if it spanned the cluster.
/// </summary>
public sealed class DependencyMuxActor : ReceiveActor
{
    public const string ActorName = "dependency-mux";

    /// <summary>Register a subscriber's interest in a set of tag refs. Idempotent on re-register —
    /// the second call replaces the prior interest set for that subscriber.</summary>
    public sealed record RegisterInterest(IReadOnlyList<string> TagRefs, IActorRef Subscriber);

    /// <summary>Unregister every interest held by <see cref="Subscriber"/>. Sent on PostStop by
    /// the subscriber, or by Terminated handling when the mux watches.</summary>
    public sealed record UnregisterInterest(IActorRef Subscriber);

    private readonly Dictionary<string, HashSet<IActorRef>> _byRef = new(StringComparer.Ordinal);
    private readonly Dictionary<IActorRef, HashSet<string>> _bySubscriber = new();
    private readonly ILoggingAdapter _log = Context.GetLogger();

    /// <summary>Creates props for the DependencyMuxActor.</summary>
    /// <returns>The props for creating this actor.</returns>
    public static Props Props() => Akka.Actor.Props.Create<DependencyMuxActor>();

    /// <summary>Initializes a new instance of the <see cref="DependencyMuxActor"/> class.</summary>
    public DependencyMuxActor()
    {
        Receive<RegisterInterest>(OnRegister);
        Receive<UnregisterInterest>(msg => RemoveSubscriber(msg.Subscriber));
        Receive<DriverInstanceActor.AttributeValuePublished>(OnAttributeValuePublished);
        Receive<Terminated>(msg => RemoveSubscriber(msg.ActorRef));
    }

    private void OnRegister(RegisterInterest msg)
    {
        // Replace any prior interest set so re-registers on actor restart don't leak old refs.
        if (_bySubscriber.TryGetValue(msg.Subscriber, out var priorRefs))
        {
            foreach (var r in priorRefs)
            {
                if (_byRef.TryGetValue(r, out var set))
                {
                    set.Remove(msg.Subscriber);
                    if (set.Count == 0) _byRef.Remove(r);
                }
            }
        }

        var refs = new HashSet<string>(msg.TagRefs, StringComparer.Ordinal);
        _bySubscriber[msg.Subscriber] = refs;
        foreach (var r in refs)
        {
            if (!_byRef.TryGetValue(r, out var set))
            {
                set = new HashSet<IActorRef>();
                _byRef[r] = set;
            }
            set.Add(msg.Subscriber);
        }
        Context.Watch(msg.Subscriber);
        _log.Debug("DependencyMux: subscriber {Sub} registered for {Count} refs", msg.Subscriber, refs.Count);
    }

    private void RemoveSubscriber(IActorRef subscriber)
    {
        if (!_bySubscriber.TryGetValue(subscriber, out var refs)) return;
        foreach (var r in refs)
        {
            if (_byRef.TryGetValue(r, out var set))
            {
                set.Remove(subscriber);
                if (set.Count == 0) _byRef.Remove(r);
            }
        }
        _bySubscriber.Remove(subscriber);
        Context.Unwatch(subscriber);
        _log.Debug("DependencyMux: subscriber {Sub} removed", subscriber);
    }

    private void OnAttributeValuePublished(DriverInstanceActor.AttributeValuePublished msg)
    {
        if (!_byRef.TryGetValue(msg.FullReference, out var subscribers) || subscribers.Count == 0)
        {
            // No virtual tag cares about this ref — drop. Common in normal operation; the address
            // space carries thousands of tags and only a fraction feed virtual-tag expressions.
            return;
        }
        var dep = new VirtualTagActor.DependencyValueChanged(msg.FullReference, msg.Value, msg.TimestampUtc, msg.Quality);
        foreach (var sub in subscribers)
        {
            sub.Tell(dep);
        }
    }

}
