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