using System.Collections.Frozen;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;

namespace ZB.MOM.WW.OtOpcUa.Server.Security;

/// <summary>
///     Maps a driver-side full reference (e.g. <c>"TestMachine_001/Oven/SetPoint"</c>) to the
///     <see cref="NodeScope"/> the Phase 6.2 evaluator walks. Supports two modes:
///     <list type="bullet">
///     <item>
///         <b>Cluster-only (pre-ADR-001)</b> — when no path index is supplied the resolver
///         returns a flat <c>ClusterId + TagId</c> 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.
///     </item>
///     <item>
///         <b>Full-path (post-ADR-001 Task B)</b> — when an index is supplied, the resolver
///         joins the full reference against the index to produce a complete
///         <c>Cluster → Namespace → UnsArea → UnsLine → Equipment → Tag</c> scope. Unblocks
///         per-Equipment / per-UnsLine ACL grants at the dispatch layer.
///     </item>
///     </list>
/// </summary>
/// <remarks>
///     <para>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.</para>
///
///     <para>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.</para>
/// </remarks>
public sealed class NodeScopeResolver
{
    private readonly string _clusterId;
    private readonly FrozenDictionary<string, NodeScope>? _index;

    /// <summary>Cluster-only resolver — pre-ADR-001 behavior. Kept for Server processes that
    /// haven't wired the Config-DB snapshot flow yet.</summary>
    public NodeScopeResolver(string clusterId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
        _clusterId = clusterId;
        _index = null;
    }

    /// <summary>
    ///     Full-path resolver (ADR-001 Task B). <paramref name="pathIndex"/> maps each known
    ///     driver-side full reference to its pre-resolved <see cref="NodeScope"/> carrying
    ///     every UNS level populated. Entries are typically produced by joining
    ///     <c>Tag → Equipment → UnsLine → UnsArea</c> rows of the published generation against
    ///     the driver's discovered full references (or against <c>Tag.TagConfig</c> directly
    ///     when the walker is config-primary per ADR-001 Option A).
    /// </summary>
    public NodeScopeResolver(string clusterId, IReadOnlyDictionary<string, NodeScope> pathIndex)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
        ArgumentNullException.ThrowIfNull(pathIndex);
        _clusterId = clusterId;
        _index = pathIndex.ToFrozenDictionary(StringComparer.Ordinal);
    }

    /// <summary>
    ///     Resolve a node scope for the given driver-side <paramref name="fullReference"/>.
    ///     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.
    /// </summary>
    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,
        };
    }
}
