namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
///
/// Validates an operator-typed Sparkplug rebirth scope for the raw browse modal.
///
///
///
/// Why a typed scope exists at all. The Sparkplug browse tree is birth-driven, and
/// births are never retained. On a quiet plant — every edge node birthed long before the browse
/// window opened, and is now publishing only NDATA/DDATA — the tree comes up empty. The
/// Request-rebirth scope was derived solely from a tree selection, so the one action that would
/// populate the tree could not be armed: the action needed the tree, and the tree needed the action.
///
///
/// This is advisory, not the gate. MqttBrowseSession.ResolveRebirthTargets re-resolves
/// the scope and refuses an unusable one before any message leaves, and the
/// DriverOperator policy is enforced server-side in BrowserSessionService. This exists
/// so the modal does not let an operator arm a two-click confirm that cannot succeed. Being the
/// looser of the two is therefore safe; being the stricter one would block a scope the session
/// accepts, which is the failure mode the repo's authoring surfaces avoid by convention.
///
///
/// A group-only scope is rejected on purpose. The session resolves a bare group by enumerating
/// the edge nodes it has observed — of which an empty tree has none — so it could not help the
/// case this input exists for, and on a populated tree it would fan one click across the whole group.
/// The tree-selection path still offers a group scope, with its own confirm-time cap and warning.
///
///
public static class SparkplugRebirthScope
{
/// MQTT wildcards. A rebirth addresses one edge node exactly; a wildcard cannot name one.
private static readonly char[] Wildcards = ['+', '#'];
///
/// Describes why is not a usable {group}/{edgeNode} rebirth scope.
///
/// The operator-typed scope, untrimmed; is treated as blank.
/// The operator-facing error text, or when the scope is usable.
public static string? Describe(string? raw)
{
var scope = (raw ?? "").Trim();
if (scope.Length == 0) { return "Enter the edge node as 'group/edgeNode'."; }
if (scope.IndexOfAny(Wildcards) >= 0)
{
return "An MQTT wildcard (+ or #) cannot address an edge node; name one exactly.";
}
var segments = scope.Split('/');
if (segments.Length != 2 || Array.Exists(segments, string.IsNullOrWhiteSpace))
{
return "Use exactly 'group/edgeNode' — a group on its own would ask every edge node in it to "
+ "republish, which this window cannot enumerate while the tree is empty.";
}
return null;
}
/// True when is a usable rebirth scope.
/// The operator-typed scope.
/// when finds no fault.
public static bool IsUsable(string? raw) => Describe(raw) is null;
///
/// Splits a scope already accepted by into its two segments.
///
/// A scope for which is .
/// The trimmed (group, edgeNode) pair.
/// The scope is not usable.
public static (string GroupId, string EdgeNodeId) Split(string? raw)
{
if (Describe(raw) is { } error)
{
throw new ArgumentException($"'{raw}' is not a usable rebirth scope: {error}", nameof(raw));
}
var segments = raw!.Trim().Split('/');
return (segments[0].Trim(), segments[1].Trim());
}
}