using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// How an attribute change should be routed to a child (script/alarm) actor.
internal enum TriggerRoutingKind
{
/// Reacts only to changes of a single named attribute.
MonitoredAttribute,
/// Reacts to ANY attribute change (Expression triggers; or fail-open when the
/// trigger config is unparseable — the child's own gate then discards what it ignores).
AllChanges,
/// Never reacts to attribute changes (interval / call-only scripts).
None
}
///
/// Result of classifying a child's trigger for attribute-change routing.
///
internal readonly record struct TriggerRoutingDecision(TriggerRoutingKind Kind, string? MonitoredAttribute)
{
public static readonly TriggerRoutingDecision AllChanges = new(TriggerRoutingKind.AllChanges, null);
public static readonly TriggerRoutingDecision None = new(TriggerRoutingKind.None, null);
/// Creates a decision routing on changes to a single named attribute.
/// The name of the attribute to monitor.
/// A of kind .
public static TriggerRoutingDecision Monitored(string attribute) => new(TriggerRoutingKind.MonitoredAttribute, attribute);
}
///
/// Single source of truth for the monitored attribute a script/alarm trigger reacts to (P2).
/// Shared by (attribute-name extraction) and
/// InstanceActor.CreateChildActors (routing-map construction) so both agree on the
/// attribute for a given trigger config.
///
/// NOTE: routing has three outcomes (monitored / all-changes / none), so this returns a
/// rather than the plan's original bool-out signature —
/// a bool cannot distinguish "hears everything" from "hears nothing".
///
internal static class TriggerRouting
{
///
/// Reads the monitored attribute name from a trigger config, honoring both the
/// "attributeName" (scripts + alarms) and "attribute" (alarm alias) keys.
/// Returns false when the JSON is missing, malformed, or carries no attribute name.
///
/// The trigger config JSON to read from.
/// When this method returns, the monitored attribute name, or an empty string if not found.
/// true if an attribute name was found; otherwise false.
public static bool TryReadAttributeName(string? triggerConfigJson, out string attribute)
{
attribute = "";
if (string.IsNullOrEmpty(triggerConfigJson)) return false;
try
{
using var doc = JsonDocument.Parse(triggerConfigJson);
var root = doc.RootElement;
if (root.TryGetProperty("attributeName", out var a) && a.GetString() is { Length: > 0 } n)
{
attribute = n;
return true;
}
if (root.TryGetProperty("attribute", out var a2) && a2.GetString() is { Length: > 0 } n2)
{
attribute = n2;
return true;
}
return false;
}
catch (JsonException)
{
return false;
}
}
///
/// Classifies a script trigger. valuechange/conditional → the named attribute (fail open to
/// all-changes if unparseable); expression → all-changes; interval/call → none; anything
/// unknown → all-changes (fail open — a misconfigured script still reacts, its own gate
/// remains the correctness check).
///
/// The script's trigger type (e.g. "valuechange", "expression", "interval", "call").
/// The trigger config JSON, used to extract the monitored attribute name where applicable.
/// The routing decision for the script's trigger.
public static TriggerRoutingDecision ForScript(string? triggerType, string? triggerConfigJson)
{
switch (triggerType?.ToLowerInvariant())
{
case "valuechange":
case "conditional":
return TryReadAttributeName(triggerConfigJson, out var attr)
? TriggerRoutingDecision.Monitored(attr)
: TriggerRoutingDecision.AllChanges;
case "expression":
return TriggerRoutingDecision.AllChanges;
case "interval":
case "call":
return TriggerRoutingDecision.None;
default:
return TriggerRoutingDecision.AllChanges;
}
}
///
/// Classifies an alarm trigger. Expression → all-changes; HiLo/ValueMatch/RangeViolation/
/// RateOfChange → the named attribute (fail open to all-changes if unparseable); an
/// unknown/unparseable trigger type → all-changes (fail open).
///
/// The alarm's trigger type (e.g. "Expression", "HiLo", "ValueMatch").
/// The trigger config JSON, used to extract the monitored attribute name where applicable.
/// The routing decision for the alarm's trigger.
public static TriggerRoutingDecision ForAlarm(string? triggerType, string? triggerConfigJson)
{
if (Enum.TryParse(triggerType, ignoreCase: true, out var tt))
{
switch (tt)
{
case AlarmTriggerType.Expression:
return TriggerRoutingDecision.AllChanges;
case AlarmTriggerType.HiLo:
case AlarmTriggerType.ValueMatch:
case AlarmTriggerType.RangeViolation:
case AlarmTriggerType.RateOfChange:
return TryReadAttributeName(triggerConfigJson, out var attr)
? TriggerRoutingDecision.Monitored(attr)
: TriggerRoutingDecision.AllChanges;
}
}
return TriggerRoutingDecision.AllChanges;
}
}