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;

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";

    public sealed record DependencyValueChanged(string TagId, object? Value, DateTime TimestampUtc);

    /// <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>
    public sealed record EvaluationResult(string VirtualTagId, object? Value, DateTime TimestampUtc, CorrelationId Correlation, OpcUaQuality Quality = OpcUaQuality.Good);

    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 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>
    /// <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) =>
        Akka.Actor.Props.Create(() => new VirtualTagActor(
            virtualTagId, expression,
            evaluator ?? NullVirtualTagEvaluator.Instance,
            scriptId ?? virtualTagId,
            publisherFactory,
            dependencyRefs ?? Array.Empty<string>(),
            mux));

    /// <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>
    public VirtualTagActor(
        string virtualTagId,
        string expression,
        IVirtualTagEvaluator evaluator,
        string scriptId,
        Func<DPSPublisher>? publisherFactory,
        IReadOnlyList<string> dependencyRefs,
        IActorRef? mux)
    {
        _virtualTagId = virtualTagId;
        _scriptId = scriptId;
        _expression = expression;
        _evaluator = evaluator;
        _publisherFactory = publisherFactory;
        _dependencyRefs = dependencyRefs;
        _mux = mux;

        Receive<DependencyValueChanged>(OnDependencyChanged);
    }

    /// <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));
    }

    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);

        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);
