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.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 Warning <see cref="ScriptLogEntry"/> on the
/// <c>script-logs</c> DPS topic so operators see the diagnostic in the live tail.
/// </summary>
public sealed class VirtualTagActor : ReceiveActor
{
    public const string ScriptLogsTopic = "script-logs";

    public sealed record DependencyValueChanged(string TagId, object? Value, DateTime TimestampUtc);
    public sealed record EvaluationResult(string VirtualTagId, object? Value, DateTime TimestampUtc, CorrelationId Correlation);

    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;

    /// <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>
    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);
            OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "fail"));
            PublishLog("Error", $"evaluator threw: {ex.Message}");
            return;
        }

        if (!result.Success)
        {
            OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "fail"));
            PublishLog("Warning", result.Reason ?? "evaluator failure");
            return;
        }

        // Skip no-change results. Real evaluator returns Ok(value); Null returns NoChange — both
        // safe because Null never produces a fresh value.
        if (ReferenceEquals(result, VirtualTagEvalResult.NoChange))
        {
            OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "skip"));
            return;
        }

        if (_hasLastValue && Equals(_lastValue, result.Value))
        {
            OtOpcUaTelemetry.VirtualTagEval.Add(1, new KeyValuePair<string, object?>("outcome", "skip"));
            return;
        }

        _hasLastValue = true;
        _lastValue = result.Value;
        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);
    }

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