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

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

/// <summary>
///     In-memory permission trie for one <c>(ClusterId, GenerationId)</c>. Walk from the cluster
///     root down through namespace → UNS levels → tag, OR-ing the
///     <see cref="TrieGrant.PermissionFlags"/> granted at each visited level for each of the session's
///     LDAP groups. The accumulated bitmask is compared to the permission required by the
///     requested <see cref="Abstractions.OpcUaOperation"/>.
/// </summary>
/// <remarks>
///     The walk is pure union (additive grants; no explicit Deny in v2.0):
///     encountering a grant at any level contributes its flags, never revokes them. A grant at
///     the Cluster root therefore cascades to every tag below it; a grant at a deep equipment
///     leaf is visible only on that equipment subtree.
/// </remarks>
public sealed class PermissionTrie
{
    /// <summary>Cluster this trie belongs to.</summary>
    public required string ClusterId { get; init; }

    /// <summary>Config generation the trie was built from — used by the cache for invalidation.</summary>
    public required long GenerationId { get; init; }

    /// <summary>Root of the trie. Level 0 (cluster-level grants) live directly here.</summary>
    public PermissionTrieNode Root { get; init; } = new();

    /// <summary>
    ///     Walk the trie collecting grants that apply to <paramref name="scope"/> for any of the
    ///     session's <paramref name="ldapGroups"/>. Returns the matched-grant list; the caller
    ///     OR-s the flag bits to decide whether the requested permission is carried.
    /// </summary>
    /// <param name="scope">The node scope to match permissions for.</param>
    /// <param name="ldapGroups">The user's LDAP group memberships.</param>
    /// <returns>The matched grants accumulated across every trie level visited for the scope.</returns>
    public IReadOnlyList<MatchedGrant> CollectMatches(NodeScope scope, IEnumerable<string> ldapGroups)
    {
        ArgumentNullException.ThrowIfNull(scope);
        ArgumentNullException.ThrowIfNull(ldapGroups);

        var groups = ldapGroups.ToHashSet(StringComparer.OrdinalIgnoreCase);
        if (groups.Count == 0) return [];

        var matches = new List<MatchedGrant>();

        // Level 0 — cluster-scoped grants.
        CollectAtLevel(Root, NodeAclScopeKind.Cluster, groups, matches);

        // Level 1 — namespace.
        if (scope.NamespaceId is null) return matches;
        if (!Root.Children.TryGetValue(scope.NamespaceId, out var ns)) return matches;
        CollectAtLevel(ns, NodeAclScopeKind.Namespace, groups, matches);

        // Below the namespace every kind walks the Equipment hierarchy — Galaxy is a standard
        // Equipment-kind driver, so Galaxy points are ordinary equipment tags keyed by
        // EquipmentId/path and resolve through this same walk.
        WalkEquipment(ns, scope, groups, matches);

        return matches;
    }

    private static void WalkEquipment(PermissionTrieNode ns, NodeScope scope, HashSet<string> groups, List<MatchedGrant> matches)
    {
        if (scope.UnsAreaId is null) return;
        if (!ns.Children.TryGetValue(scope.UnsAreaId, out var area)) return;
        CollectAtLevel(area, NodeAclScopeKind.UnsArea, groups, matches);

        if (scope.UnsLineId is null) return;
        if (!area.Children.TryGetValue(scope.UnsLineId, out var line)) return;
        CollectAtLevel(line, NodeAclScopeKind.UnsLine, groups, matches);

        if (scope.EquipmentId is null) return;
        if (!line.Children.TryGetValue(scope.EquipmentId, out var eq)) return;
        CollectAtLevel(eq, NodeAclScopeKind.Equipment, groups, matches);

        if (scope.TagId is null) return;
        if (!eq.Children.TryGetValue(scope.TagId, out var tag)) return;
        CollectAtLevel(tag, NodeAclScopeKind.Tag, groups, matches);
    }

    private static void CollectAtLevel(PermissionTrieNode node, NodeAclScopeKind level, HashSet<string> groups, List<MatchedGrant> matches)
    {
        foreach (var grant in node.Grants)
        {
            if (groups.Contains(grant.LdapGroup))
                matches.Add(new MatchedGrant(grant.LdapGroup, level, grant.PermissionFlags));
        }
    }
}

/// <summary>One node in a <see cref="PermissionTrie"/>.</summary>
public sealed class PermissionTrieNode
{
    /// <summary>Grants anchored at this trie level.</summary>
    public List<TrieGrant> Grants { get; } = [];

    /// <summary>
    ///     Children keyed by the next level's id — namespace id under cluster; UnsAreaId or
    ///     folder-segment name under namespace; etc. Comparer is OrdinalIgnoreCase so the walk
    ///     tolerates case drift between the NodeAcl row and the requested scope.
    /// </summary>
    public Dictionary<string, PermissionTrieNode> Children { get; } = new(StringComparer.OrdinalIgnoreCase);
}

/// <summary>Projection of a <see cref="Configuration.Entities.NodeAcl"/> row into the trie.</summary>
public sealed record TrieGrant(string LdapGroup, NodePermissions PermissionFlags);
