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>
    /// <param name="clusterId">Cluster the trie is being built for; rows for other clusters are skipped.</param>
    /// <param name="generationId">Config-generation the rows belong to; stamped on the returned trie.</param>
    /// <param name="rows">ACL rows for this cluster + generation.</param>
    /// <param name="scopePaths">
    ///     Optional <c>ScopeId</c> → multi-level trie-path lookup. When supplied, sub-cluster rows
    ///     descend to their structurally-correct trie node. When null, sub-cluster rows fall back
    ///     to a direct child of the trie root keyed on <c>ScopeId</c> — deterministic-test mode.
    /// </param>
    /// <param name="diagnostic">
    ///     Optional callback invoked when a sub-cluster row's <c>ScopeId</c> cannot be located
    ///     in <paramref name="scopePaths"/>. Production callers should wire a logger here so
    ///     orphaned grants surface — silently dropping them under the wrong trie level was a
    ///     production hazard. The callback fires only when <paramref name="scopePaths"/>
    ///     is non-null (a null lookup is the explicit deterministic-test fallback mode).
    /// </param>
    /// <returns>The built <see cref="PermissionTrie"/> for the cluster + generation.</returns>
    public static PermissionTrie Build(
        string clusterId,
        long generationId,
        IReadOnlyList<NodeAcl> rows,
        IReadOnlyDictionary<string, NodeAclPath>? scopePaths = null,
        Action<PermissionTrieBuildDiagnostic>? diagnostic = 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, diagnostic),
            };

            if (node is not null)
                node.Grants.Add(grant);
        }

        return trie;
    }

    private static PermissionTrieNode? Descend(
        PermissionTrieNode root,
        NodeAcl row,
        IReadOnlyDictionary<string, NodeAclPath>? scopePaths,
        Action<PermissionTrieBuildDiagnostic>? diagnostic)
    {
        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 a scopePaths lookup IS
        // provided but is missing the row's ScopeId, surface a diagnostic so the caller can
        // log the orphan instead of silently dropping the grant under an unreachable node.
        if (scopePaths is null || !scopePaths.TryGetValue(row.ScopeId, out var path))
        {
            if (scopePaths is not null)
            {
                diagnostic?.Invoke(new PermissionTrieBuildDiagnostic(
                    NodeAclId: row.NodeAclId,
                    ScopeKind: row.ScopeKind,
                    ScopeId: row.ScopeId,
                    Reason: PermissionTrieBuildDiagnosticReason.MissingScopePath));
            }
            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 UnsAreaId / UnsLineId / EquipmentId / TagId as applicable.
/// </param>
public sealed record NodeAclPath(IReadOnlyList<string> Segments);

/// <summary>
///     Diagnostic emitted by <see cref="PermissionTrieBuilder.Build"/> when a row could not be
///     placed at its structurally-correct trie node. Production callers should log these so
///     orphaned grants surface instead of being silently dropped under an unreachable node.
/// </summary>
/// <param name="NodeAclId">The offending row's logical id.</param>
/// <param name="ScopeKind">The row's <see cref="NodeAclScopeKind"/>.</param>
/// <param name="ScopeId">The row's <c>ScopeId</c> that could not be located.</param>
/// <param name="Reason">Why the diagnostic fired.</param>
public sealed record PermissionTrieBuildDiagnostic(
    string NodeAclId,
    NodeAclScopeKind ScopeKind,
    string ScopeId,
    PermissionTrieBuildDiagnosticReason Reason);

/// <summary>Reasons <see cref="PermissionTrieBuildDiagnostic"/> can be emitted.</summary>
public enum PermissionTrieBuildDiagnosticReason
{
    /// <summary>
    ///     The row's <c>ScopeId</c> was not present in the supplied <c>scopePaths</c> lookup.
    ///     The grant is placed as a direct child of the trie root keyed on <c>ScopeId</c> — a
    ///     position the production trie walker cannot reach for multi-level scopes.
    /// </summary>
    MissingScopePath,
}
