using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

namespace ZB.MOM.WW.OtOpcUa.Core.Authorization;

/// <summary>
///     Builds a <see cref="PermissionTrie"/> from a set of <see cref="NodeAcl"/> rows anchored
///     in one generation. The trie is keyed on the rows' scope hierarchy — rows with
///     <see cref="NodeAclScopeKind.Cluster"/> land at the trie root, rows with
///     <see cref="NodeAclScopeKind.Tag"/> land at a leaf, etc.
/// </summary>
/// <remarks>
///     <para>Intended to be called by <see cref="PermissionTrieCache"/> 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).</para>
///
///     <para>The builder deliberately does not know about the node-row metadata the trie path
///     will be walked with. The caller assembles <see cref="NodeScope"/> values from the live
///     config (UnsArea parent of UnsLine, etc.); this class only honors the <c>ScopeId</c>
///     each row carries.</para>
/// </remarks>
public static class PermissionTrieBuilder
{
    /// <summary>
    ///     Build a trie for one cluster/generation from the supplied rows. The caller is
    ///     responsible for pre-filtering rows to the target generation + cluster.
    /// </summary>
    public static PermissionTrie Build(
        string clusterId,
        long generationId,
        IReadOnlyList<NodeAcl> rows,
        IReadOnlyDictionary<string, NodeAclPath>? 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<string, NodeAclPath>? 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;
    }
}

/// <summary>
///     Ordered list of trie-path segments from root to the target node. Supplied to
///     <see cref="PermissionTrieBuilder.Build"/> so the builder knows where a
///     <see cref="NodeAclScopeKind.UnsLine"/>-scoped row sits in the hierarchy.
/// </summary>
/// <param name="Segments">
///     Namespace id, then (for Equipment kind) UnsAreaId / UnsLineId / EquipmentId / TagId as
///     applicable; or (for SystemPlatform kind) NamespaceId / FolderSegment / .../TagId.
/// </param>
public sealed record NodeAclPath(IReadOnlyList<string> Segments);
