using Serilog;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;

namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;

/// <summary>
///     <see cref="ScriptContext"/> subclass for alarm predicate evaluation. Reads from
///     the engine's shared tag cache (driver + virtual tags), writes are rejected —
///     predicates must be side-effect free so their output doesn't depend on evaluation
///     order or drive cascade behavior.
/// </summary>
/// <remarks>
///     Per the scripting design (see
///     <c>docs/v2/implementation/phase-7-scripting-and-alarming.md</c>, decision row 4),
///     alarm scripts are one-script-per-alarm returning <c>bool</c>. They read any tag
///     they want but should not write anything (the owning alarm's state is tracked by the
///     engine, not the script).
/// </remarks>
public sealed class AlarmPredicateContext : ScriptContext
{
    private readonly IReadOnlyDictionary<string, DataValueSnapshot> _readCache;
    private readonly Func<DateTime> _clock;

    /// <summary>Initializes a new instance of the <see cref="AlarmPredicateContext"/> class.</summary>
    /// <param name="readCache">The cached read values from tags.</param>
    /// <param name="logger">The logger for diagnostics.</param>
    /// <param name="clock">Optional custom clock for testing.</param>
    public AlarmPredicateContext(
        IReadOnlyDictionary<string, DataValueSnapshot> readCache,
        ILogger logger,
        Func<DateTime>? clock = null)
    {
        _readCache = readCache ?? throw new ArgumentNullException(nameof(readCache));
        Logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _clock = clock ?? (() => DateTime.UtcNow);
    }

    /// <inheritdoc />
    public override DataValueSnapshot GetTag(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
            return new DataValueSnapshot(null, 0x80340000u /* BadNodeIdUnknown */, null, _clock());
        return _readCache.TryGetValue(path, out var v)
            ? v
            : new DataValueSnapshot(null, 0x80340000u /* BadNodeIdUnknown */, null, _clock());
    }

    /// <inheritdoc />
    public override void SetVirtualTag(string path, object? value)
    {
        // Predicates must be pure — writing from an alarm script couples alarm state to
        // virtual-tag state in a way that's near-impossible to reason about. Rejected
        // at runtime with a clear message; operators see it in the scripts-*.log.
        throw new InvalidOperationException(
            "Alarm predicate scripts cannot write to virtual tags. Move the write logic " +
            "into a virtual tag whose value the alarm predicate then reads.");
    }

    /// <inheritdoc />
    public override DateTime Now => _clock();

    /// <inheritdoc />
    public override ILogger Logger { get; }
}
