using Akka.Actor;
using Akka.Event;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;

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

/// <summary>
///     Supervisor that gives Equipment-namespace VirtualTags live values. For each
///     <see cref="EquipmentVirtualTagPlan"/> in the desired set it spawns one child
///     <see cref="VirtualTagActor"/> (which self-registers with the dependency mux and evaluates its
///     expression on dependency changes) and remembers the plan's <b>folder-scoped NodeId</b>. When a
///     child reports a fresh <see cref="VirtualTagActor.EvaluationResult"/>, the host bridges it onto
///     an <see cref="OpcUaPublishActor.AttributeValueUpdate"/> targeting that NodeId so the
///     already-materialised Variable node (currently BadWaitingForInitialData) reflects the value.
///
///     <para>
///         The published NodeId is computed with the <b>identical</b> formula
///         <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c> uses to materialise the variable —
///         <c>{parent}/{Name}</c> where <c>parent = IsNullOrWhiteSpace(FolderPath) ? EquipmentId :
///         {EquipmentId}/{FolderPath}</c> — or the value would land on a NodeId that does not exist.
///     </para>
/// </summary>
public sealed class VirtualTagHostActor : ReceiveActor
{
    /// <summary>Reconciles the live VirtualTag children to exactly the supplied desired set:
    /// stops children whose vtagId is gone, spawns children for new vtagIds, and rebuilds the
    /// vtagId→NodeId map so renames are reflected.</summary>
    /// <param name="Plans">The desired Equipment-namespace VirtualTag plans.</param>
    public sealed record ApplyVirtualTags(IReadOnlyList<EquipmentVirtualTagPlan> Plans);

    private readonly IActorRef _publishActor;
    private readonly IActorRef? _mux;
    private readonly IVirtualTagEvaluator _evaluator;
    private readonly ILoggingAdapter _log = Context.GetLogger();

    // vtagId -> spawned child VirtualTagActor.
    private readonly Dictionary<string, IActorRef> _children = new(StringComparer.Ordinal);
    // vtagId -> folder-scoped OPC UA NodeId the materialiser placed the variable at.
    private readonly Dictionary<string, string> _nodeIdByVtag = new(StringComparer.Ordinal);

    /// <summary>Factory method to create Props for a VirtualTagHostActor.</summary>
    /// <param name="publishActor">The OPC UA publish actor that consumes
    /// <see cref="OpcUaPublishActor.AttributeValueUpdate"/> bridged from child results.</param>
    /// <param name="mux">Optional dependency multiplexer; passed to each spawned child so it can
    /// register interest in its dependency refs. Null on the dev/Mac path (no live values).</param>
    /// <param name="evaluator">The evaluator each child uses to compute its expression.</param>
    public static Props Props(IActorRef publishActor, IActorRef? mux, IVirtualTagEvaluator evaluator) =>
        Akka.Actor.Props.Create(() => new VirtualTagHostActor(publishActor, mux, evaluator));

    /// <summary>Initializes a new instance of the <see cref="VirtualTagHostActor"/> class.</summary>
    /// <param name="publishActor">The OPC UA publish actor results are bridged to.</param>
    /// <param name="mux">Optional dependency multiplexer passed to each spawned child.</param>
    /// <param name="evaluator">The evaluator each child uses to compute its expression.</param>
    public VirtualTagHostActor(IActorRef publishActor, IActorRef? mux, IVirtualTagEvaluator evaluator)
    {
        ArgumentNullException.ThrowIfNull(publishActor);
        ArgumentNullException.ThrowIfNull(evaluator);
        _publishActor = publishActor;
        _mux = mux;
        _evaluator = evaluator;

        Receive<ApplyVirtualTags>(OnApply);
        Receive<VirtualTagActor.EvaluationResult>(OnResult);
        Receive<Terminated>(OnChildTerminated);
    }

    private void OnApply(ApplyVirtualTags msg)
    {
        var desired = new HashSet<string>(msg.Plans.Select(p => p.VirtualTagId), StringComparer.Ordinal);

        // Stop + forget children whose vtagId is no longer desired. Stopping the child triggers its
        // PostStop, which unregisters its interest from the mux.
        foreach (var vtagId in _children.Keys.Where(id => !desired.Contains(id)).ToList())
        {
            Context.Stop(_children[vtagId]);
            _children.Remove(vtagId);
        }

        // Rebuild the NodeId map every apply so renames (Name/FolderPath/EquipmentId changes) are
        // picked up. The map only contains currently-desired vtags, so a result for a removed vtag
        // finds no entry and is dropped.
        _nodeIdByVtag.Clear();
        foreach (var p in msg.Plans)
        {
            _nodeIdByVtag[p.VirtualTagId] = NodeIdFor(p);
        }

        // Spawn children for new vtagIds only — existing children keep their mux subscriptions and
        // last-value dedup state. Expression/dependency changes on an existing vtag are NOT
        // re-applied here; the loader's vtags are stable, and a future enhancement can stop+respawn
        // a child whose plan changed (the diff already identifies ChangedEquipmentVirtualTags).
        foreach (var p in msg.Plans)
        {
            // TODO(equipment-virtualtags): when a plan's Expression/DependencyRefs change in place
            // (ChangedEquipmentVirtualTags), stop+respawn the child here; today only spawn-new/stop-removed
            // is handled (loader vtags are stable).
            if (_children.ContainsKey(p.VirtualTagId)) continue;

            // Auto-name the child: vtagIds can contain characters illegal in actor names, so let Akka
            // assign a safe unique name. The child self-registers with the mux in PreStart.
            var child = Context.ActorOf(VirtualTagActor.Props(
                virtualTagId: p.VirtualTagId,
                expression: p.Expression,
                evaluator: _evaluator,
                scriptId: p.VirtualTagId,
                publisherFactory: null,
                dependencyRefs: p.DependencyRefs,
                mux: _mux));
            Context.Watch(child);
            _children[p.VirtualTagId] = child;
            _log.Debug("VirtualTagHost: spawned child for vtag {VirtualTagId}", p.VirtualTagId);
        }

        _log.Debug("VirtualTagHost: applied (desired={Desired}, children={Children})",
            desired.Count, _children.Count);
    }

    private void OnResult(VirtualTagActor.EvaluationResult result)
    {
        // A result may arrive for a vtag that was just removed from the desired set (the child's
        // last in-flight message). With no NodeId mapping we have nowhere to land it — drop silently.
        if (!_nodeIdByVtag.TryGetValue(result.VirtualTagId, out var nodeId))
        {
            return;
        }

        _publishActor.Tell(new OpcUaPublishActor.AttributeValueUpdate(
            nodeId, result.Value, OpcUaQuality.Good, result.TimestampUtc));
    }

    private void OnChildTerminated(Terminated msg)
    {
        var stale = _children.Where(kv => kv.Value.Equals(msg.ActorRef)).Select(kv => kv.Key).ToList();
        foreach (var id in stale)
        {
            _children.Remove(id);
            // NodeId map is rebuilt on the next ApplyVirtualTags; leaving the mapping is harmless
            // (no child will publish for it until respawned). A dead child is respawned on next apply.
            _log.Warning("VirtualTagHost: child for vtag {VirtualTagId} terminated; will respawn on next apply", id);
        }
    }

    /// <summary>Folder-scoped NodeId for a VirtualTag plan — MUST match
    /// <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c> exactly, or the published value lands on a
    /// NodeId that was never materialised.</summary>
    private static string NodeIdFor(EquipmentVirtualTagPlan p)
    {
        var parent = string.IsNullOrWhiteSpace(p.FolderPath)
            ? p.EquipmentId
            : $"{p.EquipmentId}/{p.FolderPath}";
        return $"{parent}/{p.Name}";
    }
}
