using System.Collections.Frozen;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
namespace ZB.MOM.WW.OtOpcUa.Server.Security;
///
/// Maps a driver-side full reference (e.g. "TestMachine_001/Oven/SetPoint") to the
/// the Phase 6.2 evaluator walks. Supports two modes:
///
/// -
/// Cluster-only (pre-ADR-001) — when no path index is supplied the resolver
/// returns a flat ClusterId + TagId scope. Sufficient while the
/// Config-DB-driven Equipment walker isn't live; Cluster-level grants cascade to every
/// tag below per decision #129, so finer per-Equipment grants are effectively
/// cluster-wide at dispatch.
///
/// -
/// Full-path (post-ADR-001 Task B) — when an index is supplied, the resolver
/// joins the full reference against the index to produce a complete
/// Cluster → Namespace → UnsArea → UnsLine → Equipment → Tag scope. Unblocks
/// per-Equipment / per-UnsLine ACL grants at the dispatch layer.
///
///
///
///
/// The index is pre-loaded by the Server bootstrap against the published generation;
/// the resolver itself does no live DB access. Resolve is O(1) dictionary lookup on the
/// hot path; the fallback for unknown fullReference strings produces the same cluster-only
/// scope the pre-ADR-001 resolver returned — new tags picked up via driver discovery but
/// not yet indexed (e.g. between a DiscoverAsync result and the next generation publish)
/// stay addressable without a scope-resolver crash.
///
/// Thread-safety: both constructor paths freeze inputs into immutable state. Callers
/// may cache a single instance per DriverNodeManager without locks. Swap atomically on
/// generation change via the server's publish pipeline.
///
public sealed class NodeScopeResolver
{
private readonly string _clusterId;
private readonly FrozenDictionary? _index;
/// Cluster-only resolver — pre-ADR-001 behavior. Kept for Server processes that
/// haven't wired the Config-DB snapshot flow yet.
public NodeScopeResolver(string clusterId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
_clusterId = clusterId;
_index = null;
}
///
/// Full-path resolver (ADR-001 Task B). maps each known
/// driver-side full reference to its pre-resolved carrying
/// every UNS level populated. Entries are typically produced by joining
/// Tag → Equipment → UnsLine → UnsArea rows of the published generation against
/// the driver's discovered full references (or against Tag.TagConfig directly
/// when the walker is config-primary per ADR-001 Option A).
///
public NodeScopeResolver(string clusterId, IReadOnlyDictionary pathIndex)
{
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
ArgumentNullException.ThrowIfNull(pathIndex);
_clusterId = clusterId;
_index = pathIndex.ToFrozenDictionary(StringComparer.Ordinal);
}
///
/// Resolve a node scope for the given driver-side .
/// Returns the indexed full-path scope when available; falls back to cluster-only
/// (TagId populated only) when the index is absent or the reference isn't indexed.
/// The fallback is the same shape the pre-ADR-001 resolver produced, so the authz
/// evaluator behaves identically for un-indexed references.
///
public NodeScope Resolve(string fullReference)
{
ArgumentException.ThrowIfNullOrWhiteSpace(fullReference);
if (_index is not null && _index.TryGetValue(fullReference, out var indexed))
return indexed;
return new NodeScope
{
ClusterId = _clusterId,
TagId = fullReference,
Kind = NodeHierarchyKind.Equipment,
};
}
}