namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication;

public sealed record ApiKeyConstraints(
    IReadOnlyList<string> ReadSubtrees,
    IReadOnlyList<string> WriteSubtrees,
    IReadOnlyList<string> ReadTagGlobs,
    IReadOnlyList<string> WriteTagGlobs,
    int? MaxWriteClassification,
    IReadOnlyList<string> BrowseSubtrees,
    bool ReadAlarmOnly,
    bool ReadHistorizedOnly)
{
    private readonly IReadOnlyList<string> _dashboardTags = Array.Empty<string>();

    /// <summary>
    ///     Gets the dashboard event-visibility tags granted to this key (SEC-25).
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This is <em>dashboard event-visibility only</em>. It is <strong>never</strong> a
    ///         data-access constraint: no read, write, browse, or subscribe path consults it, and
    ///         adding a tag neither widens nor narrows what the key may read or write. Sessions
    ///         opened by the key inherit these tags (<c>GatewaySession.Tags</c>), and a dashboard
    ///         Viewer may observe a session's mirrored event metadata only when their granted tags
    ///         intersect the session's. It rides in the same serialized constraints blob purely to
    ///         avoid an auth-store schema migration — see
    ///         <c>docs/plans/2026-07-10-dashboard-session-acl-tst15.md</c> §3.1.
    ///     </para>
    ///     <para>
    ///         Tag values are stored exactly as supplied; comparisons are ordinal-ignore-case at the
    ///         enforcement site, so <c>Team-A</c> and <c>team-a</c> name the same tag. An empty list
    ///         means untagged.
    ///     </para>
    /// </remarks>
    public IReadOnlyList<string> DashboardTags
    {
        get => _dashboardTags;

        // Defensive copy: the tag set is a security-relevant grant, so the record must not alias a
        // caller-owned list that could be mutated after construction. A null or empty value (an old
        // persisted row has no dashboard_tags member at all) normalizes to untagged.
        init => _dashboardTags = value is { Count: > 0 } ? [.. value] : Array.Empty<string>();
    }

    /// <summary>Gets an empty constraints instance with no restrictions.</summary>
    public static ApiKeyConstraints Empty { get; } = new(
        ReadSubtrees: Array.Empty<string>(),
        WriteSubtrees: Array.Empty<string>(),
        ReadTagGlobs: Array.Empty<string>(),
        WriteTagGlobs: Array.Empty<string>(),
        MaxWriteClassification: null,
        BrowseSubtrees: Array.Empty<string>(),
        ReadAlarmOnly: false,
        ReadHistorizedOnly: false);

    /// <summary>Gets a value indicating whether the constraints are empty (no restrictions).</summary>
    /// <remarks>
    ///     <see cref="DashboardTags"/> counts here even though it restricts nothing: an empty
    ///     instance is not persisted at all (<c>ApiKeyConstraintSerializer.Serialize</c> returns
    ///     null), so a key whose only per-key policy is a dashboard tag must still round-trip.
    /// </remarks>
    public bool IsEmpty =>
        ReadSubtrees.Count == 0
        && WriteSubtrees.Count == 0
        && ReadTagGlobs.Count == 0
        && WriteTagGlobs.Count == 0
        && MaxWriteClassification is null
        && BrowseSubtrees.Count == 0
        && !ReadAlarmOnly
        && !ReadHistorizedOnly
        && DashboardTags.Count == 0;

    /// <summary>Gets a value indicating whether any read constraints are defined.</summary>
    public bool HasReadConstraints =>
        ReadSubtrees.Count > 0
        || ReadTagGlobs.Count > 0
        || ReadAlarmOnly
        || ReadHistorizedOnly;

    /// <summary>Gets a value indicating whether any write constraints are defined.</summary>
    public bool HasWriteConstraints =>
        WriteSubtrees.Count > 0
        || WriteTagGlobs.Count > 0
        || MaxWriteClassification is not null;
}
