using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.Core.Authorization;
///
/// Builds a from a set of rows anchored
/// in one generation. The trie is keyed on the rows' scope hierarchy — rows with
/// land at the trie root, rows with
/// land at a leaf, etc.
///
///
/// Intended to be called by once per published
/// generation; the resulting trie is immutable for the life of the cache entry. Idempotent —
/// two builds from the same rows produce equal tries (grant lists may be in insertion order;
/// evaluators don't depend on order).
///
/// The builder deliberately does not know about the node-row metadata the trie path
/// will be walked with. The caller assembles values from the live
/// config (UnsArea parent of UnsLine, etc.); this class only honors the ScopeId
/// each row carries.
///
public static class PermissionTrieBuilder
{
///
/// Build a trie for one cluster/generation from the supplied rows. The caller is
/// responsible for pre-filtering rows to the target generation + cluster.
///
public static PermissionTrie Build(
string clusterId,
long generationId,
IReadOnlyList rows,
IReadOnlyDictionary? scopePaths = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
ArgumentNullException.ThrowIfNull(rows);
var trie = new PermissionTrie { ClusterId = clusterId, GenerationId = generationId };
foreach (var row in rows)
{
if (!string.Equals(row.ClusterId, clusterId, StringComparison.OrdinalIgnoreCase)) continue;
var grant = new TrieGrant(row.LdapGroup, row.PermissionFlags);
var node = row.ScopeKind switch
{
NodeAclScopeKind.Cluster => trie.Root,
_ => Descend(trie.Root, row, scopePaths),
};
if (node is not null)
node.Grants.Add(grant);
}
return trie;
}
private static PermissionTrieNode? Descend(PermissionTrieNode root, NodeAcl row, IReadOnlyDictionary? scopePaths)
{
if (string.IsNullOrEmpty(row.ScopeId)) return null;
// For sub-cluster scopes the caller supplies a path lookup so we know the containing
// namespace / UnsArea / UnsLine ids. Without a path lookup we fall back to putting the
// row directly under the root using its ScopeId — works for deterministic tests, not
// for production where the hierarchy must be honored.
if (scopePaths is null || !scopePaths.TryGetValue(row.ScopeId, out var path))
{
return EnsureChild(root, row.ScopeId);
}
var node = root;
foreach (var segment in path.Segments)
node = EnsureChild(node, segment);
return node;
}
private static PermissionTrieNode EnsureChild(PermissionTrieNode parent, string key)
{
if (!parent.Children.TryGetValue(key, out var child))
{
child = new PermissionTrieNode();
parent.Children[key] = child;
}
return child;
}
}
///
/// Ordered list of trie-path segments from root to the target node. Supplied to
/// so the builder knows where a
/// -scoped row sits in the hierarchy.
///
///
/// Namespace id, then (for Equipment kind) UnsAreaId / UnsLineId / EquipmentId / TagId as
/// applicable; or (for SystemPlatform kind) NamespaceId / FolderSegment / .../TagId.
///
public sealed record NodeAclPath(IReadOnlyList Segments);