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

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

/// <summary>
///     The API user scripts see as the global <c>ctx</c>. Abstract — concrete subclasses
///     (e.g. <c>VirtualTagScriptContext</c>, <c>AlarmScriptContext</c>) plug in the
///     actual tag-backend + logger + virtual-tag writer for each evaluation. By design
///     (see <c>docs/v2/plan.md</c>), scripts can read any tag, write only to virtual
///     tags, and have no other .NET reach — no HttpClient, no File, no Process, no
///     reflection.
/// </summary>
/// <remarks>
///     <para>
///         Every member on this type MUST be serializable in the narrow sense that
///         <c>DependencyExtractor</c> can recognize tag-access call sites from the
///         script AST. Method names used from scripts are locked — renaming
///         <see cref="GetTag"/> or <see cref="SetVirtualTag"/> is a breaking change for every
///         authored script and the dependency extractor must update in lockstep.
///     </para>
///     <para>
///         New helpers (<see cref="Now"/>, <see cref="Deadband"/>) are additive: adding a
///         method doesn't invalidate existing scripts. Do not remove or rename without a
///         plan-level decision + migration for authored scripts.
///     </para>
/// </remarks>
public abstract class ScriptContext
{
    /// <summary>
    ///     Read a tag's current value + quality + source timestamp. Path syntax is
    ///     <c>Enterprise/Site/Area/Line/Equipment/TagName</c> (forward-slash delimited,
    ///     matching the Equipment-namespace browse tree). Returns a
    ///     <see cref="DataValueSnapshot"/> so scripts branch on quality without a second
    ///     call.
    /// </summary>
    /// <remarks>
    ///     <paramref name="path"/> MUST be a string literal in the script source — dynamic
    ///     paths (variables, concatenation, method-returned strings) are rejected at
    ///     publish by <c>DependencyExtractor</c>. This is intentional: the static
    ///     dependency set is required for the change-driven scheduler to subscribe to the
    ///     right upstream tags at load time.
    ///     <para>
    ///         The reserved <c>{{equip}}</c> token may appear in the path literal; it is
    ///         substituted with the owning equipment's tag-base prefix at deployment time.
    ///         See <c>docs/ScriptEditor.md</c> — Equipment-relative tag paths.
    ///     </para>
    /// </remarks>
    /// <param name="path">The literal tag path to read.</param>
    /// <returns>A snapshot of the tag's current value, quality, and source timestamp.</returns>
    public abstract DataValueSnapshot GetTag(string path);

    /// <summary>
    ///     Write a value to a virtual tag. Operator scripts cannot write to driver-sourced
    ///     tags — the OPC UA dispatch in <c>DriverNodeManager</c> rejects that separately
    ///     with <c>BadUserAccessDenied</c>. This method is the only write path
    ///     virtual tags have.
    /// </summary>
    /// <remarks>
    ///     Path rules identical to <see cref="GetTag"/> — literal only, dependency
    ///     extractor tracks the write targets so the engine knows what downstream
    ///     subscribers to notify. The <c>{{equip}}</c> token is supported in the same way
    ///     as for <see cref="GetTag"/>.
    /// </remarks>
    /// <param name="path">The literal tag path to write to.</param>
    /// <param name="value">The value to write to the virtual tag.</param>
    public abstract void SetVirtualTag(string path, object? value);

    /// <summary>
    ///     Current UTC timestamp. Prefer this over <see cref="DateTime.UtcNow"/> in
    ///     scripts so the harness can supply a deterministic clock for tests.
    /// </summary>
    public abstract DateTime Now { get; }

    /// <summary>
    ///     Per-script Serilog logger. Output lands in the dedicated <c>scripts-*.log</c>
    ///     sink with structured property <c>ScriptName</c> = the script's configured name.
    ///     Use at error level to surface problems; main <c>opcua-*.log</c> receives a
    ///     companion WARN entry so operators see script errors in the primary log.
    /// </summary>
    public abstract ILogger Logger { get; }

    /// <summary>
    ///     Deadband helper — returns <c>true</c> when <paramref name="current"/> differs
    ///     from <paramref name="previous"/> by more than <paramref name="tolerance"/>.
    ///     Useful for alarm predicates that shouldn't flicker on small noise. Pure
    ///     function; no side effects.
    /// </summary>
    /// <param name="current">The current value to check.</param>
    /// <param name="previous">The previous value to compare against.</param>
    /// <param name="tolerance">The minimum change magnitude to detect (must be &gt;= 0).
    ///     Negative values cause the function to always return true; NaN always returns false.</param>
    /// <returns><c>true</c> when <paramref name="current"/> differs from <paramref name="previous"/>
    ///     by more than <paramref name="tolerance"/>; otherwise <c>false</c>.</returns>
    public static bool Deadband(double current, double previous, double tolerance)
        => Math.Abs(current - previous) > tolerance;
}
