using Akka.Actor;
using Akka.Cluster.Tools.PublishSubscribe;
using Akka.Event;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;

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

/// <summary>
/// Wraps a single virtual-tag expression. Receives dependency-tag updates, recomputes the
/// expression via an injected <see cref="IVirtualTagEvaluator"/>, and emits an
/// <see cref="EvaluationResult"/> to the parent (the publish actor) whenever the value
/// actually changes. Script failures publish a <see cref="ScriptLogEntry"/> on the
/// <c>script-logs</c> DPS topic so operators see the diagnostic in the live tail.
///
/// <para>
///     02/S13 — a persistent script failure/timeout also degrades the node's OPC UA quality to
///     <see cref="OpcUaQuality.Bad"/> (carrying the last-known value), published <b>once per
///     transition</b> (Good→Bad) and gated on every declared dependency having arrived at least once
///     — so a broken script no longer serves its last Good value forever, and a fresh/respawned
///     multi-dep script does not flash Bad during deploy warm-up. Recovery to a Good value clears the
///     Bad state and is force-published even when the value equals the pre-failure value.
/// </para>
/// </summary>
public sealed class VirtualTagActor : ReceiveActor
{
    public const string ScriptLogsTopic = "script-logs";

    /// <summary>A dependency's value changed. <paramref name="Quality"/> (#478) carries the source
    /// driver's OPC UA quality so the scripted-alarm host can derive a condition's worst-of-input quality;
    /// it defaults to <see cref="OpcUaQuality.Good"/>, and the virtual-tag engine ignores it.</summary>
    public sealed record DependencyValueChanged(
        string TagId, object? Value, DateTime TimestampUtc, OpcUaQuality Quality = OpcUaQuality.Good);

    /// <summary>Re-assert the last emitted value/quality to the parent bridge, bypassing the value dedup.
    /// Sent by <see cref="VirtualTagHostActor"/> on every apply (deploy) to a surviving child: a deploy
    /// re-materialises the child's published UNS node to <c>BadWaitingForInitialData</c>, but an
    /// unchanged-plan child keeps its dedup state, so a static-dependency recompute is suppressed and the
    /// node would stay Bad forever. Re-asserting the last state recovers it. Singleton — carries no data.</summary>
    public sealed class ReassertValue { public static readonly ReassertValue Instance = new(); private ReassertValue() { } }

    /// <summary>Result emitted to the parent (the host bridge). <paramref name="Quality"/> carries the
    /// OPC UA quality the node should read: <see cref="OpcUaQuality.Good"/> for a fresh computed value,
    /// <see cref="OpcUaQuality.Bad"/> when the script failed/timed out (02/S13) — in which case
    /// <paramref name="Value"/> is the last-known value (or null if none). Defaulting to Good keeps
    /// every existing construction site source-compatible.</summary>
    /// <param name="IsReassert"><c>true</c> when this result is a deploy-time re-assertion of the last
    /// value/quality (see <see cref="ReassertValue"/>) rather than a fresh evaluation. The host still
    /// publishes it (to repair the reset OPC UA node) but must NOT re-record it to the historian — it is a
    /// stale value stamped with a fresh deploy-time timestamp, never a real evaluation sample. Defaults to
    /// <c>false</c> so every existing construction site stays source-compatible.</param>
    public sealed record EvaluationResult(string VirtualTagId, object? Value, DateTime TimestampUtc, CorrelationId Correlation, OpcUaQuality Quality = OpcUaQuality.Good, bool IsReassert = false);

    private readonly string _virtualTagId;
    private readonly string _scriptId;
    private readonly string _expression;
    private readonly IVirtualTagEvaluator _evaluator;
    private readonly Func<DPSPublisher>? _publisherFactory;
    private readonly IReadOnlyList<string> _dependencyRefs;
    private readonly IActorRef? _mux;
    private readonly ITelemetryLocalHub? _telemetryHub;
    private readonly ILoggingAdapter _log = Context.GetLogger();
    private readonly Dictionary<string, object?> _dependencies = new(StringComparer.Ordinal);

    private bool _hasLastValue;
    private object? _lastValue;
    // 02/S13: true once a Bad EvaluationResult has been published and not yet cleared by a Good
    // recovery. Gates Bad publishes to once-per-transition and force-publishes the recovery value.
    private bool _lastPublishedBad;

    /// <summary>Factory method to create Props for a VirtualTagActor.</summary>
    /// <param name="virtualTagId">Unique identifier for the virtual tag.</param>
    /// <param name="expression">The expression to evaluate.</param>
    /// <param name="evaluator">Optional evaluator; defaults to a null-object instance.</param>
    /// <param name="scriptId">Optional script identifier; defaults to <paramref name="virtualTagId"/>.</param>
    /// <param name="publisherFactory">Optional factory for creating DPS publishers.</param>
    /// <param name="dependencyRefs">Optional list of dependency tag references; defaults to empty.</param>
    /// <param name="mux">Optional reference to a dependency multiplexer actor.</param>
    /// <param name="telemetryHub">Optional node-local live-telemetry hub each script-log entry is also
    /// emitted into (Phase 5); null (tests) simply skips the emit.</param>
    /// <returns>A configured <see cref="Akka.Actor.Props"/> for creating the actor.</returns>
    public static Props Props(
        string virtualTagId,
        string expression,
        IVirtualTagEvaluator? evaluator = null,
        string? scriptId = null,
        Func<DPSPublisher>? publisherFactory = null,
        IReadOnlyList<string>? dependencyRefs = null,
        IActorRef? mux = null,
        ITelemetryLocalHub? telemetryHub = null) =>
        Akka.Actor.Props.Create(() => new VirtualTagActor(
            virtualTagId, expression,
            evaluator ?? NullVirtualTagEvaluator.Instance,
            scriptId ?? virtualTagId,
            publisherFactory,
            dependencyRefs ?? Array.Empty<string>(),
            mux,
            telemetryHub));

    /// <summary>Initializes a virtual tag actor with the given configuration and dependencies.</summary>
    /// <param name="virtualTagId">Unique identifier for the virtual tag.</param>
    /// <param name="expression">The expression to evaluate.</param>
    /// <param name="evaluator">The evaluator responsible for computing the expression result.</param>
    /// <param name="scriptId">The script identifier associated with this tag.</param>
    /// <param name="publisherFactory">Optional factory for creating DPS publishers.</param>
    /// <param name="dependencyRefs">List of dependency tag references that this tag depends on.</param>
    /// <param name="mux">Optional reference to a dependency multiplexer actor.</param>
    /// <param name="telemetryHub">Optional node-local live-telemetry hub each script-log entry is also
    /// emitted into (Phase 5); null (tests) simply skips the emit.</param>
    public VirtualTagActor(
        string virtualTagId,
        string expression,
        IVirtualTagEvaluator evaluator,
        string scriptId,
        Func<DPSPublisher>? publisherFactory,
        IReadOnlyList<string> dependencyRefs,
        IActorRef? mux,
        ITelemetryLocalHub? telemetryHub = null)
    {
        _virtualTagId = virtualTagId;
        _scriptId = scriptId;
        _expression = expression;
        _evaluator = evaluator;
        _publisherFactory = publisherFactory;
        _dependencyRefs = dependencyRefs;
        _mux = mux;
        _telemetryHub = telemetryHub;

        Receive<DependencyValueChanged>(OnDependencyChanged);
        Receive<ReassertValue>(_ => OnReassertValue());
    }

    /// <inheritdoc />
    protected override void PreStart()
    {
        if (_mux is not null && _dependencyRefs.Count > 0)
        {
            _mux.Tell(new DependencyMuxActor.RegisterInterest(_dependencyRefs, Self));
        }
    }

    /// <inheritdoc />
    protected override void PostStop()
    {
        _mux?.Tell(new DependencyMuxActor.UnregisterInterest(Self));
    }

    /// <summary>Re-emit the last emitted value/quality to the parent bridge, bypassing the value dedup, so a
    /// deploy that re-materialised our published UNS node to <c>BadWaitingForInitialData</c> recovers even when
    /// the recomputed value is unchanged (permanent Bad otherwise, with a static dependency). No-op until the
    /// child has emitted at least once — the first dependency arrival publishes the initial value normally.
    /// <para>
    ///     The result is flagged <see cref="EvaluationResult.IsReassert"/> so the host publishes it (repairing
    ///     the reset node) but does NOT re-record it to the historian — it is a stale value stamped with a
    ///     fresh deploy-time timestamp, not a real evaluation sample (M1).
    /// </para>
    /// <para>
    ///     Ordering (why the re-publish lands on the freshly-materialised node, not the pre-reset one): the
    ///     reset and this re-publish converge on the SAME single-threaded <c>OpcUaPublishActor</c>. On a deploy
    ///     <c>DriverHostActor</c> enqueues <c>RebuildAddressSpace</c> (the reset) to that actor FIRST, then sends
    ///     <c>ApplyVirtualTags</c> to the VirtualTag HOST; the host tells this child <c>ReassertValue</c>, and
    ///     only then does this method Tell the host an <c>EvaluationResult</c> which the host bridges onward to
    ///     the publish actor. That multi-hop chain guarantees the re-publish arrives AFTER <c>RebuildAddressSpace</c>
    ///     in the publish actor's FIFO mailbox, so the materialise runs before the write.
    /// </para></summary>
    private void OnReassertValue()
    {
        if (!_hasLastValue && !_lastPublishedBad) return;
        var quality = _lastPublishedBad ? OpcUaQuality.Bad : OpcUaQuality.Good;
        Context.Parent.Tell(new EvaluationResult(
            _virtualTagId, _lastValue, DateTime.UtcNow, CorrelationId.NewId(), quality, IsReassert: true));
    }

    private void OnDependencyChanged(DependencyValueChanged msg)
    {
        _dependencies[msg.TagId] = msg.Value;

        VirtualTagEvalResult result;
        try
        {
            result = _evaluator.Evaluate(_virtualTagId, _expression, _dependencies);
        }
        catch (Exception ex)
        {
            _log.Warning(ex, "VirtualTag {Id}: evaluator threw", _virtualTagId);
            OnEvaluationFailed($"evaluator threw: {ex.Message}", "Error", msg.TimestampUtc);
            return;
        }

        if (!result.Success)
        {
            OnEvaluationFailed(result.Reason ?? "evaluator failure", "Warning", msg.TimestampUtc);
            return;
        }

        // Skip no-change results. Real evaluator returns Ok(value); Null returns NoChange — both
        // safe because Null never produces a fresh value. NoChange is "no fresh value", NOT a recovery,
        // so it must stay above the dedup and must NOT clear _lastPublishedBad.
        if (ReferenceEquals(result, VirtualTagEvalResult.NoChange))
        {
            OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "skip"));
            return;
        }

        // Value dedup — but bypass it while recovering from Bad, so a recovery whose value equals the
        // pre-failure value still republishes Good (otherwise the node would stay Bad forever).
        if (!_lastPublishedBad && _hasLastValue && Equals(_lastValue, result.Value))
        {
            OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "skip"));
            return;
        }

        _hasLastValue = true;
        _lastValue = result.Value;
        _lastPublishedBad = false;
        OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "ok"));
        var evalResult = new EvaluationResult(_virtualTagId, result.Value, msg.TimestampUtc, CorrelationId.NewId());
        Context.Parent.Tell(evalResult);
    }

    /// <summary>02/S13 failure tail shared by the evaluator-threw catch and the <c>!Success</c> branch.
    /// Always meters + publishes the per-failure script-log entry (the pre-existing operator
    /// diagnostic); additionally publishes a Bad <see cref="EvaluationResult"/> — carrying the
    /// last-known value if any — once per Good→Bad transition, and only after every declared dependency
    /// has arrived (the inputs-ready gate that suppresses the cold-start/respawn warm-up flash).</summary>
    /// <param name="reason">The failure reason for the script-log entry.</param>
    /// <param name="level">The script-log level ("Warning" for a Failure result, "Error" for a throw).</param>
    /// <param name="timestampUtc">The triggering dependency change's timestamp, stamped on the Bad result.</param>
    private void OnEvaluationFailed(string reason, string level, DateTime timestampUtc)
    {
        OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "fail"));
        PublishLog(level, reason);

        if (_lastPublishedBad || !AllDependenciesArrived())
        {
            return;
        }

        _lastPublishedBad = true;
        Context.Parent.Tell(new EvaluationResult(
            _virtualTagId, _hasLastValue ? _lastValue : null, timestampUtc, CorrelationId.NewId(), OpcUaQuality.Bad));
    }

    /// <summary>Inputs-ready gate (mirrors the dormant <c>VirtualTagEngine.AreInputsReady</c>): true once
    /// every declared dependency ref has been seen at least once. Called once per failure — the ref list
    /// is small, so no caching. A vacuously-true empty ref list means a no-dependency script degrades on
    /// its first failure.</summary>
    private bool AllDependenciesArrived() => _dependencyRefs.All(_dependencies.ContainsKey);

    private void PublishLog(string level, string message)
    {
        var entry = new ScriptLogEntry(
            ScriptId: _scriptId,
            Level: level,
            Message: message,
            TimestampUtc: DateTime.UtcNow,
            VirtualTagId: _virtualTagId,
            AlarmId: null,
            EquipmentId: null);

        // Phase 5: fan the same entry into the node-local live-telemetry hub (no-op until a gRPC client
        // subscribes). Emitted BEFORE the publish branch so BOTH the production DPS path and the test-seam
        // publisherFactory path (which returns early below) feed the hub. Both taps are fire-and-forget, so
        // emit-then-publish ordering is immaterial; the DPS/factory publish itself is unchanged.
        _telemetryHub?.Emit(new TelemetryItem.Script(entry));

        if (_publisherFactory is not null)
        {
            _publisherFactory().Publish(ScriptLogsTopic, entry);
            return;
        }

        DistributedPubSub.Get(Context.System).Mediator.Tell(new Publish(ScriptLogsTopic, entry));
    }
}

/// <summary>
///     Thin seam for tests to capture DPS publishes without standing up a real cluster.
///     Production never instantiates this directly — the actor falls through to
///     <see cref="DistributedPubSub"/> when the factory is null.
/// </summary>
public sealed record DPSPublisher(Action<string, object> Publish);
