namespace ZB.MOM.WW.OtOpcUa.Core.Authorization;
///
/// Per-session authorization state cached on the OPC UA session object + keyed on the
/// session id. Captures the LDAP group memberships resolved at sign-in, the generation
/// the membership was resolved against, and the bounded freshness window.
///
///
/// Per decision #151 the membership is bounded by
/// (default 15 min). After that, the next hot-path authz call re-resolves LDAP group
/// memberships; failure to re-resolve (LDAP unreachable) flips the session to fail-closed
/// until a refresh succeeds.
///
/// Per decision #152 (default 5 min) is separate from
/// Phase 6.1's availability-oriented 24h cache — beyond this window the evaluator returns
/// regardless of config-cache warmth.
///
public sealed record UserAuthorizationState
{
/// Opaque session id (reuse OPC UA session handle when possible).
public required string SessionId { get; init; }
/// Cluster the session is scoped to — every request targets nodes in this cluster.
public required string ClusterId { get; init; }
///
/// LDAP groups the user is a member of as resolved at sign-in / last membership refresh.
/// Case comparison is handled downstream by the evaluator (OrdinalIgnoreCase).
///
public required IReadOnlyList LdapGroups { get; init; }
/// Timestamp when was last resolved from the directory.
public required DateTime MembershipResolvedUtc { get; init; }
///
/// Trie generation the session is currently bound to. When
/// moves to a new generation, the session's
/// (AuthGenerationId, MembershipVersion) stamp no longer matches its
/// MonitoredItems and they re-evaluate on next publish (decision #153).
///
public required long AuthGenerationId { get; init; }
///
/// Monotonic counter incremented every time membership is re-resolved. Combined with
/// into the subscription stamp per decision #153.
///
public required long MembershipVersion { get; init; }
/// Bounded membership freshness window; past this the next authz call refreshes.
public TimeSpan MembershipFreshnessInterval { get; init; } = TimeSpan.FromMinutes(15);
/// Hard staleness ceiling — beyond this, the evaluator fails closed.
public TimeSpan AuthCacheMaxStaleness { get; init; } = TimeSpan.FromMinutes(5);
///
/// True when - exceeds
/// . The evaluator short-circuits to NotGranted
/// whenever this is true.
///
public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness;
///
/// True when membership is past its freshness interval but still within the staleness
/// ceiling — a signal to the caller to kick off an async refresh, while the current
/// call still evaluates against the cached memberships.
///
public bool NeedsRefresh(DateTime utcNow) =>
!IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval;
}