using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
///
/// Which kind of trigger a template script has. is no
/// trigger; is a stored trigger-type string the runtime
/// does not recognize (preserved as-is by the editor).
///
internal enum ScriptTriggerKind { None, Interval, ValueChange, Conditional, Call, Expression, Unknown }
///
/// When a Conditional/Expression trigger fires. fires once
/// as the condition becomes true; re-fires on a timer
/// (cadence = the script's MinTimeBetweenRuns) while the condition stays true.
///
internal enum ScriptTriggerMode { OnTrue, WhileTrue }
/// A script's trigger as the editor emits it: a type string + config JSON.
public sealed record ScriptTriggerValue(string? TriggerType, string? Config);
/// Parsed/editable view of a script trigger's configuration.
internal sealed class ScriptTriggerModel
{
/// Interval period in milliseconds.
public long? IntervalMs { get; set; }
/// Monitored attribute (ValueChange + Conditional).
public string? AttributeName { get; set; }
/// Comparison operator (Conditional) — one of .
public string Operator { get; set; } = ">";
/// Comparison threshold (Conditional).
public double? Threshold { get; set; }
/// Boolean C# expression (Expression).
public string? Expression { get; set; }
/// Fire mode (Conditional + Expression). Defaults to .
public ScriptTriggerMode Mode { get; set; } = ScriptTriggerMode.OnTrue;
// Per-trigger analysis kind (Expression only). When true the
// codec serializes "analysisKind":"Strict"; when false (Advisory, the
// default) the key is omitted. Matches ValidationService.IsStrictAnalysis.
///
/// When , the trigger config carries
/// "analysisKind":"Strict" so ValidationService escalates the
/// blank-expression advisory to a deploy-blocking error.
///
public bool IsStrictAnalysisKind { get; set; }
}
///
/// Round-trip codec for a template script's TriggerType +
/// TriggerConfiguration, shared by (UI
/// editing) and consumed at the site by ScriptActor.ParseTriggerConfig.
/// Serialized config shapes:
/// Interval { intervalMs }
/// ValueChange { attributeName }
/// Conditional { attributeName, operator, threshold, mode }
/// Call { }
/// Expression { expression, mode }
///
/// mode (Conditional + Expression) is OnTrue or WhileTrue;
/// an absent or unrecognized value parses as OnTrue.
///
/// Parsing also accepts the legacy aliases attribute and value so
/// older configs survive a round-trip through the editor.
///
internal static class ScriptTriggerConfigCodec
{
/// The six comparison operators ScriptActor.EvaluateCondition accepts.
internal static readonly string[] Operators = { ">", ">=", "<", "<=", "==", "!=" };
/// Classifies a raw TriggerType string (case-insensitive).
/// The raw trigger type string from the template script entity.
/// The matching , or for null/empty.
internal static ScriptTriggerKind ParseKind(string? triggerType)
{
if (string.IsNullOrWhiteSpace(triggerType)) return ScriptTriggerKind.None;
return triggerType.Trim().ToLowerInvariant() switch
{
"interval" => ScriptTriggerKind.Interval,
"valuechange" => ScriptTriggerKind.ValueChange,
"conditional" => ScriptTriggerKind.Conditional,
"call" => ScriptTriggerKind.Call,
"expression" => ScriptTriggerKind.Expression,
_ => ScriptTriggerKind.Unknown
};
}
///
/// Whether a trigger type honours the script's MinTimeBetweenRuns.
/// True for the auto-firing triggers it throttles — ValueChange, Conditional,
/// Expression. False for Interval (its own period is the cadence), Call
/// (invoked explicitly, never throttled), and None/Unknown.
///
/// The raw trigger type string to classify.
/// if the trigger honours MinTimeBetweenRuns; otherwise .
internal static bool SupportsMinTimeBetweenRuns(string? triggerType) =>
ParseKind(triggerType) is ScriptTriggerKind.ValueChange
or ScriptTriggerKind.Conditional
or ScriptTriggerKind.Expression;
/// Canonical TriggerType string for a kind; null for None/Unknown.
/// The trigger kind to convert.
/// The canonical trigger-type string, or null for /.
internal static string? KindToString(ScriptTriggerKind kind) => kind switch
{
ScriptTriggerKind.Interval => "Interval",
ScriptTriggerKind.ValueChange => "ValueChange",
ScriptTriggerKind.Conditional => "Conditional",
ScriptTriggerKind.Call => "Call",
ScriptTriggerKind.Expression => "Expression",
_ => null
};
///
/// Parses a trigger configuration JSON in the context of the given kind.
/// Returns a model with default values on null/empty/malformed input or for
/// missing keys — never throws.
///
/// The raw JSON trigger configuration string.
/// The trigger kind, used to determine which fields to parse.
/// A populated from the JSON; defaults are used for absent or malformed fields.
internal static ScriptTriggerModel Parse(string? json, ScriptTriggerKind kind)
{
var model = new ScriptTriggerModel();
if (string.IsNullOrWhiteSpace(json)) return model;
try
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
switch (kind)
{
case ScriptTriggerKind.Interval:
model.IntervalMs = TryReadLong(root, "intervalMs");
break;
case ScriptTriggerKind.ValueChange:
model.AttributeName = TryReadAttributeName(root);
break;
case ScriptTriggerKind.Conditional:
model.AttributeName = TryReadAttributeName(root);
var op = root.TryGetProperty("operator", out var o) ? o.GetString() : null;
model.Operator = NormalizeOperator(op);
model.Threshold = TryReadDouble(root, "threshold") ?? TryReadDouble(root, "value");
model.Mode = ReadMode(root);
break;
case ScriptTriggerKind.Expression:
model.Expression = root.TryGetProperty("expression", out var e) ? e.GetString() : null;
model.Mode = ReadMode(root);
// Read optional analysisKind discriminator (matches
// ValidationService.IsStrictAnalysis — "Strict" case-insensitive → strict;
// absent/"Advisory"/anything else → Advisory default).
if (root.TryGetProperty("analysisKind", out var ak)
&& ak.ValueKind == JsonValueKind.String)
{
model.IsStrictAnalysisKind = string.Equals(
ak.GetString(), "Strict", StringComparison.OrdinalIgnoreCase);
}
break;
}
}
catch (JsonException)
{
// Malformed JSON — fall through with default model.
}
return model;
}
///
/// Serializes the model to the JSON shape ScriptActor.ParseTriggerConfig
/// expects. Returns null for None/Unknown (no structured config to emit).
///
/// The trigger model to serialize.
/// The trigger kind, used to determine which fields to emit.
/// The JSON configuration string, or null for /.
internal static string? Serialize(ScriptTriggerModel model, ScriptTriggerKind kind)
{
if (kind is ScriptTriggerKind.None or ScriptTriggerKind.Unknown) return null;
using var stream = new MemoryStream();
using (var w = new Utf8JsonWriter(stream))
{
w.WriteStartObject();
switch (kind)
{
case ScriptTriggerKind.Interval:
if (model.IntervalMs.HasValue)
w.WriteNumber("intervalMs", model.IntervalMs.Value);
break;
case ScriptTriggerKind.ValueChange:
w.WriteString("attributeName", model.AttributeName ?? "");
break;
case ScriptTriggerKind.Conditional:
w.WriteString("attributeName", model.AttributeName ?? "");
w.WriteString("operator", model.Operator);
if (model.Threshold.HasValue)
w.WriteNumber("threshold", model.Threshold.Value);
w.WriteString("mode", model.Mode.ToString());
break;
case ScriptTriggerKind.Expression:
w.WriteString("expression", model.Expression ?? "");
w.WriteString("mode", model.Mode.ToString());
// Emit "analysisKind":"Strict" only when explicitly set;
// Advisory is the default so the key is omitted to stay backward-compatible.
if (model.IsStrictAnalysisKind)
w.WriteString("analysisKind", "Strict");
break;
// Call → empty object.
}
w.WriteEndObject();
}
return Encoding.UTF8.GetString(stream.ToArray());
}
///
/// Reads the optional mode property; an absent or unrecognized value
/// (case-insensitive) yields .
///
private static ScriptTriggerMode ReadMode(JsonElement root)
{
var raw = root.TryGetProperty("mode", out var m) ? m.GetString() : null;
return string.Equals(raw?.Trim(), "WhileTrue", StringComparison.OrdinalIgnoreCase)
? ScriptTriggerMode.WhileTrue
: ScriptTriggerMode.OnTrue;
}
/// Returns if it is a recognized operator, else ">".
/// The raw operator string to normalize.
/// A valid operator string from ; defaults to ">" for unrecognized input.
internal static string NormalizeOperator(string? raw)
{
var op = raw?.Trim();
return op != null && Array.IndexOf(Operators, op) >= 0 ? op : ">";
}
private static string? TryReadAttributeName(JsonElement root) =>
root.TryGetProperty("attributeName", out var a) ? a.GetString()
: root.TryGetProperty("attribute", out var a2) ? a2.GetString()
: null;
private static long? TryReadLong(JsonElement el, string name)
{
if (!el.TryGetProperty(name, out var p)) return null;
return p.ValueKind switch
{
JsonValueKind.Number when p.TryGetInt64(out var i) => i,
JsonValueKind.Number => (long)p.GetDouble(),
JsonValueKind.String when long.TryParse(
p.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) => v,
_ => null
};
}
private static double? TryReadDouble(JsonElement el, string name)
{
if (!el.TryGetProperty(name, out var p)) return null;
return p.ValueKind switch
{
JsonValueKind.Number => p.GetDouble(),
JsonValueKind.String when double.TryParse(
p.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var v) => v,
_ => null
};
}
}