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

/// <summary>
///     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.
/// </summary>
/// <remarks>
///     The membership is bounded by <see cref="MembershipFreshnessInterval"/>
///     (default 5 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.
///
///     <see cref="AuthCacheMaxStaleness"/> (default 15 min) is separate from
///     Phase 6.1's availability-oriented 24h cache — beyond this window the evaluator returns
///     <see cref="AuthorizationVerdict.NotGranted"/> regardless of config-cache warmth.
///
///     The freshness window is the inner trigger and the staleness ceiling the outer hard
///     limit: <see cref="MembershipFreshnessInterval"/> MUST be strictly less than
///     <see cref="AuthCacheMaxStaleness"/> so that <see cref="NeedsRefresh"/> ("re-resolve
///     while still serving cached memberships") has a non-empty window before
///     <see cref="IsStale"/> fails the session closed.
/// </remarks>
public sealed record UserAuthorizationState
{
    /// <summary>Opaque session id (reuse OPC UA session handle when possible).</summary>
    public required string SessionId { get; init; }

    /// <summary>Cluster the session is scoped to — every request targets nodes in this cluster.</summary>
    public required string ClusterId { get; init; }

    /// <summary>
    ///     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).
    /// </summary>
    public required IReadOnlyList<string> LdapGroups { get; init; }

    /// <summary>Timestamp when <see cref="LdapGroups"/> was last resolved from the directory.</summary>
    public required DateTime MembershipResolvedUtc { get; init; }

    /// <summary>
    ///     Trie generation the session is currently bound to. When
    ///     <see cref="PermissionTrieCache"/> moves to a new generation, the session's
    ///     <c>(AuthGenerationId, MembershipVersion)</c> stamp no longer matches its
    ///     MonitoredItems and they re-evaluate on next publish.
    /// </summary>
    public required long AuthGenerationId { get; init; }

    /// <summary>
    ///     Monotonic counter incremented every time membership is re-resolved. Combined with
    ///     <see cref="AuthGenerationId"/> into the subscription stamp.
    /// </summary>
    public required long MembershipVersion { get; init; }

    /// <summary>Bounded membership freshness window; past this the next authz call refreshes.</summary>
    public TimeSpan MembershipFreshnessInterval { get; init; } = TimeSpan.FromMinutes(5);

    /// <summary>Hard staleness ceiling — beyond this, the evaluator fails closed.</summary>
    public TimeSpan AuthCacheMaxStaleness { get; init; } = TimeSpan.FromMinutes(15);

    /// <summary>
    ///     True when <paramref name="utcNow"/> - <see cref="MembershipResolvedUtc"/> exceeds
    ///     <see cref="AuthCacheMaxStaleness"/>. The evaluator short-circuits to NotGranted
    ///     whenever this is true.
    /// </summary>
    /// <param name="utcNow">The current UTC time.</param>
    /// <returns><see langword="true"/> if the cached membership has exceeded the staleness ceiling.</returns>
    public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness;

    /// <summary>
    ///     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.
    /// </summary>
    /// <param name="utcNow">The current UTC time.</param>
    /// <returns><see langword="true"/> if membership is past its freshness interval but not yet stale, signaling an async refresh is due.</returns>
    public bool NeedsRefresh(DateTime utcNow) =>
        !IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval;
}
