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

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

/// <summary>
///     Default <see cref="IPermissionEvaluator"/> implementation. Resolves the
///     <see cref="PermissionTrie"/> for the session's cluster (via
///     <see cref="PermissionTrieCache"/>), walks it collecting matched grants, OR-s the
///     permission flags, and maps against the operation-specific required permission.
/// </summary>
public sealed class TriePermissionEvaluator : IPermissionEvaluator
{
    private readonly PermissionTrieCache _cache;
    private readonly TimeProvider _timeProvider;

    /// <summary>Initializes a new instance of the TriePermissionEvaluator class.</summary>
    /// <param name="cache">The permission trie cache.</param>
    /// <param name="timeProvider">The time provider for checking authorization state staleness; if null, uses system time.</param>
    public TriePermissionEvaluator(PermissionTrieCache cache, TimeProvider? timeProvider = null)
    {
        ArgumentNullException.ThrowIfNull(cache);
        _cache = cache;
        _timeProvider = timeProvider ?? TimeProvider.System;
    }

    /// <inheritdoc />
    public AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope)
    {
        ArgumentNullException.ThrowIfNull(session);
        ArgumentNullException.ThrowIfNull(scope);

        if (session.IsStale(_timeProvider.GetUtcNow().UtcDateTime))
            return AuthorizationDecision.NotGranted();

        if (!string.Equals(session.ClusterId, scope.ClusterId, StringComparison.OrdinalIgnoreCase))
            return AuthorizationDecision.NotGranted();

        var trie = _cache.GetTrie(scope.ClusterId);
        if (trie is null) return AuthorizationDecision.NotGranted();

        if (trie.GenerationId != session.AuthGenerationId)
        {
            trie = _cache.GetTrie(scope.ClusterId, session.AuthGenerationId);

            // The session's bound generation has been pruned out of the cache — fail closed and
            // force the caller to re-resolve the session's auth state before retrying.
            if (trie is null) return AuthorizationDecision.NotGranted();
        }

        var matches = trie.CollectMatches(scope, session.LdapGroups);
        if (matches.Count == 0) return AuthorizationDecision.NotGranted();

        var required = MapOperationToPermission(operation);
        var granted = NodePermissions.None;
        foreach (var m in matches) granted |= m.PermissionFlags;

        return (granted & required) == required
            ? AuthorizationDecision.Allowed(matches)
            : AuthorizationDecision.NotGranted();
    }

    /// <summary>Maps each <see cref="OpcUaOperation"/> to the <see cref="NodePermissions"/> bit required to grant it.</summary>
    /// <param name="op">The OPC UA operation.</param>
    /// <returns>The required node permission for the operation.</returns>
    public static NodePermissions MapOperationToPermission(OpcUaOperation op) => op switch
    {
        OpcUaOperation.Browse               => NodePermissions.Browse,
        OpcUaOperation.Read                 => NodePermissions.Read,
        OpcUaOperation.WriteOperate         => NodePermissions.WriteOperate,
        OpcUaOperation.WriteTune            => NodePermissions.WriteTune,
        OpcUaOperation.WriteConfigure       => NodePermissions.WriteConfigure,
        OpcUaOperation.HistoryRead          => NodePermissions.HistoryRead,
        OpcUaOperation.HistoryUpdate        => NodePermissions.HistoryUpdate,
        OpcUaOperation.CreateMonitoredItems => NodePermissions.Subscribe,
        OpcUaOperation.TransferSubscriptions=> NodePermissions.Subscribe,
        OpcUaOperation.Call                 => NodePermissions.MethodCall,
        OpcUaOperation.AlarmAcknowledge     => NodePermissions.AlarmAcknowledge,
        OpcUaOperation.AlarmConfirm         => NodePermissions.AlarmConfirm,
        OpcUaOperation.AlarmShelve          => NodePermissions.AlarmShelve,
        _ => throw new ArgumentOutOfRangeException(nameof(op), op, $"No permission mapping defined for operation {op}."),
    };
}
