using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Client;

/// <summary>
/// Represents one gateway-backed MXAccess session.
/// </summary>
public sealed class MxGatewaySession : IAsyncDisposable
{
    private readonly MxGatewayClient _client;
    private readonly SemaphoreSlim _closeLock = new(1, 1);
    private CloseSessionReply? _closeReply;

    /// <summary>
    /// Initializes a new session backed by the given MXAccess gateway client.
    /// </summary>
    /// <param name="client">The gateway client used for commands and events.</param>
    /// <param name="openSessionReply">The server's session creation response.</param>
    internal MxGatewaySession(
        MxGatewayClient client,
        OpenSessionReply openSessionReply)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
        OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
    }

    /// <summary>
    /// The session ID assigned by the gateway.
    /// </summary>
    public string SessionId => OpenSessionReply.SessionId;

    /// <summary>
    /// The server's session creation response containing metadata.
    /// </summary>
    public OpenSessionReply OpenSessionReply { get; }

    /// <summary>
    /// Closes the session on the gateway. Idempotent.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The server's close-session reply.</returns>
    public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
    {
        if (_closeReply is not null)
        {
            return _closeReply;
        }

        await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_closeReply is not null)
            {
                return _closeReply;
            }

            _closeReply = await _client.CloseSessionRawAsync(
                    new CloseSessionRequest { SessionId = SessionId },
                    cancellationToken)
                .ConfigureAwait(false);
            return _closeReply;
        }
        finally
        {
            _closeLock.Release();
        }
    }

    /// <summary>
    /// Registers a client with the MXAccess session, returning a ServerHandle.
    /// </summary>
    /// <param name="clientName">Name to register.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The server handle assigned to the registered client.</returns>
    public async Task<int> RegisterAsync(
        string clientName,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await RegisterRawAsync(clientName, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
    }

    /// <summary>
    /// Registers a client with the MXAccess session without error checking.
    /// </summary>
    /// <param name="clientName">Name to register.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> RegisterRawAsync(
        string clientName,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(clientName);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.Register,
                Register = new RegisterCommand { ClientName = clientName },
            },
            cancellationToken);
    }

    /// <summary>
    /// Adds an item to the MXAccess session, returning an ItemHandle.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemDefinition">The item tag address.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The item handle assigned to the new item.</returns>
    public async Task<int> AddItemAsync(
        int serverHandle,
        string itemDefinition,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await AddItemRawAsync(
                serverHandle,
                itemDefinition,
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
    }

    /// <summary>
    /// Adds an item to the MXAccess session without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemDefinition">The item tag address.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> AddItemRawAsync(
        int serverHandle,
        string itemDefinition,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.AddItem,
                AddItem = new AddItemCommand
                {
                    ServerHandle = serverHandle,
                    ItemDefinition = itemDefinition,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Adds an item with context to the MXAccess session, returning an ItemHandle.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemDefinition">The item tag address.</param>
    /// <param name="itemContext">Additional context for the item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The item handle assigned to the new item.</returns>
    public async Task<int> AddItem2Async(
        int serverHandle,
        string itemDefinition,
        string itemContext,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await AddItem2RawAsync(
                serverHandle,
                itemDefinition,
                itemContext,
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
    }

    /// <summary>
    /// Adds an item with context to the MXAccess session without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemDefinition">The item tag address.</param>
    /// <param name="itemContext">Additional context for the item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> AddItem2RawAsync(
        int serverHandle,
        string itemDefinition,
        string itemContext,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.AddItem2,
                AddItem2 = new AddItem2Command
                {
                    ServerHandle = serverHandle,
                    ItemDefinition = itemDefinition,
                    ItemContext = itemContext ?? string.Empty,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Subscribes to events for an item (advises in MXAccess terminology).
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task AdviseAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await AdviseRawAsync(serverHandle, itemHandle, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Subscribes to events for an item without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> AdviseRawAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.Advise,
                Advise = new AdviseCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Unsubscribes from events for an item (unadvises in MXAccess terminology).
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task UnAdviseAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await UnAdviseRawAsync(serverHandle, itemHandle, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Unsubscribes from events for an item without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> UnAdviseRawAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.UnAdvise,
                UnAdvise = new UnAdviseCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Removes an item from the MXAccess session.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task RemoveItemAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await RemoveItemRawAsync(serverHandle, itemHandle, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Removes an item from the MXAccess session without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> RemoveItemRawAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.RemoveItem,
                RemoveItem = new RemoveItemCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Adds multiple items to the MXAccess session in a single command.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="tagAddresses">The item tag addresses to add.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Per-item subscription results.</returns>
    public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
        int serverHandle,
        IReadOnlyList<string> tagAddresses,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(tagAddresses);

        AddItemBulkCommand command = new() { ServerHandle = serverHandle };
        command.TagAddresses.Add(tagAddresses);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.AddItemBulk,
                    AddItemBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.AddItemBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Advises multiple items in a single command.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandles">The ItemHandles to advise.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Per-item subscription results.</returns>
    public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
        int serverHandle,
        IReadOnlyList<int> itemHandles,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(itemHandles);

        AdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
        command.ItemHandles.Add(itemHandles);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.AdviseItemBulk,
                    AdviseItemBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.AdviseItemBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Removes multiple items in a single command.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandles">The ItemHandles to remove.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Per-item subscription results.</returns>
    public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
        int serverHandle,
        IReadOnlyList<int> itemHandles,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(itemHandles);

        RemoveItemBulkCommand command = new() { ServerHandle = serverHandle };
        command.ItemHandles.Add(itemHandles);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.RemoveItemBulk,
                    RemoveItemBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.RemoveItemBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Unadvises multiple items in a single command.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandles">The ItemHandles to unadvise.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Per-item subscription results.</returns>
    public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
        int serverHandle,
        IReadOnlyList<int> itemHandles,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(itemHandles);

        UnAdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
        command.ItemHandles.Add(itemHandles);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.UnAdviseItemBulk,
                    UnAdviseItemBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Adds and advises multiple items in a single command.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="tagAddresses">The item tag addresses to add and advise.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Per-item subscription results.</returns>
    public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
        int serverHandle,
        IReadOnlyList<string> tagAddresses,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(tagAddresses);

        SubscribeBulkCommand command = new() { ServerHandle = serverHandle };
        command.TagAddresses.Add(tagAddresses);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.SubscribeBulk,
                    SubscribeBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.SubscribeBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Unadvises and removes multiple items in a single command.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandles">The ItemHandles to unsubscribe.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Per-item subscription results.</returns>
    public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
        int serverHandle,
        IReadOnlyList<int> itemHandles,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(itemHandles);

        UnsubscribeBulkCommand command = new() { ServerHandle = serverHandle };
        command.ItemHandles.Add(itemHandles);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.UnsubscribeBulk,
                    UnsubscribeBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Bulk Write — sequential MXAccess Write per entry on the worker's STA.
    /// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
    /// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
    /// Protocol-level failures still throw via EnsureProtocolSuccess.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="entries">Per-item write entries; each carries the item handle, value, and user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
    public async Task<IReadOnlyList<BulkWriteResult>> WriteBulkAsync(
        int serverHandle,
        IReadOnlyList<WriteBulkEntry> entries,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entries);

        WriteBulkCommand command = new() { ServerHandle = serverHandle };
        command.Entries.Add(entries);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.WriteBulk,
                    WriteBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.WriteBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Bulk Write2 — sequential MXAccess Write2 (timestamped) per entry.
    /// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
    /// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, and user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
    public async Task<IReadOnlyList<BulkWriteResult>> Write2BulkAsync(
        int serverHandle,
        IReadOnlyList<Write2BulkEntry> entries,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entries);

        Write2BulkCommand command = new() { ServerHandle = serverHandle };
        command.Entries.Add(entries);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.Write2Bulk,
                    Write2Bulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.Write2Bulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Bulk WriteSecured — sequential MXAccess WriteSecured per entry.
    /// Credential-sensitive values must never reach logs; the client mirrors
    /// the single-item WriteSecured redaction contract.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="entries">Per-item write entries; each carries the item handle, value, current user id, and verifier user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
    public async Task<IReadOnlyList<BulkWriteResult>> WriteSecuredBulkAsync(
        int serverHandle,
        IReadOnlyList<WriteSecuredBulkEntry> entries,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entries);

        WriteSecuredBulkCommand command = new() { ServerHandle = serverHandle };
        command.Entries.Add(entries);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.WriteSecuredBulk,
                    WriteSecuredBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.WriteSecuredBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Bulk WriteSecured2 — sequential MXAccess WriteSecured2 (timestamped) per entry.
    /// Same redaction rules as <see cref="WriteSecuredBulkAsync"/>.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, current user id, and verifier user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
    public async Task<IReadOnlyList<BulkWriteResult>> WriteSecured2BulkAsync(
        int serverHandle,
        IReadOnlyList<WriteSecured2BulkEntry> entries,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entries);

        WriteSecured2BulkCommand command = new() { ServerHandle = serverHandle };
        command.Entries.Add(entries);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.WriteSecured2Bulk,
                    WriteSecured2Bulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.WriteSecured2Bulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Bulk Read — snapshot the current value for each requested tag.
    /// Returns the cached OnDataChange value when the tag is already advised
    /// (<c>WasCached = true</c>), otherwise the worker takes the full AddItem +
    /// Advise + wait + UnAdvise + RemoveItem snapshot lifecycle. Per-tag
    /// failures (timeout, invalid tag) appear as <see cref="BulkReadResult"/>
    /// entries with <c>WasSuccessful = false</c>; the call never throws on
    /// per-tag errors.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="tagAddresses">Tag addresses to read (one per result).</param>
    /// <param name="timeout">Per-call timeout for the snapshot lifecycle path; <see cref="TimeSpan.Zero"/> uses the gateway default.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>One <see cref="BulkReadResult"/> per requested tag, in request order.</returns>
    public async Task<IReadOnlyList<BulkReadResult>> ReadBulkAsync(
        int serverHandle,
        IReadOnlyList<string> tagAddresses,
        TimeSpan timeout,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(tagAddresses);

        ReadBulkCommand command = new()
        {
            ServerHandle = serverHandle,
            TimeoutMs = timeout <= TimeSpan.Zero ? 0u : (uint)Math.Min(timeout.TotalMilliseconds, uint.MaxValue),
        };
        command.TagAddresses.Add(tagAddresses);

        MxCommandReply reply = await InvokeCommandAsync(
                new MxCommand
                {
                    Kind = MxCommandKind.ReadBulk,
                    ReadBulk = command,
                },
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.ReadBulk?.Results.ToArray() ?? [];
    }

    /// <summary>
    /// Writes a value to an item on the MXAccess server.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="userId">User ID context for the write.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task WriteAsync(
        int serverHandle,
        int itemHandle,
        MxValue value,
        int userId,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await WriteRawAsync(serverHandle, itemHandle, value, userId, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Writes specific array indices to an item using default-fill semantics.
    /// </summary>
    /// <remarks>
    /// The gateway expands the sparse descriptor into a full <c>totalLength</c>-element array
    /// before forwarding to the worker. Indices not listed in <paramref name="elements"/> are
    /// written as the element type's default value — this is a RESET, not a preserve. The
    /// current values at those positions are discarded. <paramref name="totalLength"/> is
    /// required and must match the declared length of the array attribute.
    /// </remarks>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="elementDataType">The MXAccess data type of each element.</param>
    /// <param name="totalLength">The total declared length of the target array attribute.</param>
    /// <param name="elements">Map of zero-based array index to scalar <see cref="MxValue"/>.</param>
    /// <param name="userId">User ID context for the write.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public Task WriteArrayElementsAsync(
        int serverHandle,
        int itemHandle,
        MxDataType elementDataType,
        uint totalLength,
        IReadOnlyDictionary<uint, MxValue> elements,
        int userId = 0,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(elements);
        MxValue value = BuildSparseArray(elementDataType, totalLength, elements);
        return WriteAsync(serverHandle, itemHandle, value, userId, cancellationToken);
    }

    /// <summary>
    /// Builds an <see cref="MxValue"/> whose <see cref="MxValue.SparseArrayValue"/> describes a
    /// default-fill partial array write.
    /// </summary>
    /// <param name="elementDataType">The MXAccess data type of each element.</param>
    /// <param name="totalLength">The total declared length of the target array attribute.</param>
    /// <param name="elements">Map of zero-based array index to scalar <see cref="MxValue"/>.</param>
    /// <returns>An <see cref="MxValue"/> with <see cref="MxValue.KindOneofCase.SparseArrayValue"/> set.</returns>
    internal static MxValue BuildSparseArray(
        MxDataType elementDataType,
        uint totalLength,
        IReadOnlyDictionary<uint, MxValue> elements)
    {
        MxSparseArray sparse = new()
        {
            ElementDataType = elementDataType,
            TotalLength = totalLength,
        };
        foreach (KeyValuePair<uint, MxValue> kv in elements)
        {
            sparse.Elements.Add(new MxSparseElement { Index = kv.Key, Value = kv.Value });
        }

        return new MxValue { SparseArrayValue = sparse };
    }

    /// <summary>
    /// Writes a value to an item on the MXAccess server without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="userId">User ID context for the write.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> WriteRawAsync(
        int serverHandle,
        int itemHandle,
        MxValue value,
        int userId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(value);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.Write,
                Write = new WriteCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                    Value = value,
                    UserId = userId,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Writes a value and timestamp to an item on the MXAccess server.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="timestampValue">The timestamp to write with the value.</param>
    /// <param name="userId">User ID context for the write.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task Write2Async(
        int serverHandle,
        int itemHandle,
        MxValue value,
        MxValue timestampValue,
        int userId,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await Write2RawAsync(
                serverHandle,
                itemHandle,
                value,
                timestampValue,
                userId,
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Writes a value and timestamp to an item on the MXAccess server without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="timestampValue">The timestamp to write with the value.</param>
    /// <param name="userId">User ID context for the write.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> Write2RawAsync(
        int serverHandle,
        int itemHandle,
        MxValue value,
        MxValue timestampValue,
        int userId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(value);
        ArgumentNullException.ThrowIfNull(timestampValue);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.Write2,
                Write2 = new Write2Command
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                    Value = value,
                    TimestampValue = timestampValue,
                    UserId = userId,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Unregisters a previously registered client from the MXAccess session
    /// (MXAccess <c>Unregister</c>), releasing its ServerHandle.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task UnregisterAsync(
        int serverHandle,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await UnregisterRawAsync(serverHandle, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Unregisters a previously registered client without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> UnregisterRawAsync(
        int serverHandle,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.Unregister,
                Unregister = new UnregisterCommand { ServerHandle = serverHandle },
            },
            cancellationToken);
    }

    /// <summary>
    /// Subscribes to supervisory events for an item (MXAccess <c>AdviseSupervisory</c>).
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task AdviseSupervisoryAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await AdviseSupervisoryRawAsync(serverHandle, itemHandle, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Subscribes to supervisory events for an item without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> AdviseSupervisoryRawAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.AdviseSupervisory,
                AdviseSupervisory = new AdviseSupervisoryCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Adds a buffered item to the MXAccess session (MXAccess <c>AddBufferedItem</c>),
    /// returning an ItemHandle.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemDefinition">The item tag address.</param>
    /// <param name="itemContext">Additional context for the item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The item handle assigned to the new buffered item.</returns>
    public async Task<int> AddBufferedItemAsync(
        int serverHandle,
        string itemDefinition,
        string itemContext,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await AddBufferedItemRawAsync(
                serverHandle,
                itemDefinition,
                itemContext,
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return ResolveInt32Result(reply.AddBufferedItem?.ItemHandle, reply, "AddBufferedItem");
    }

    /// <summary>
    /// Adds a buffered item to the MXAccess session without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemDefinition">The item tag address.</param>
    /// <param name="itemContext">Additional context for the item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> AddBufferedItemRawAsync(
        int serverHandle,
        string itemDefinition,
        string itemContext,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.AddBufferedItem,
                AddBufferedItem = new AddBufferedItemCommand
                {
                    ServerHandle = serverHandle,
                    ItemDefinition = itemDefinition,
                    ItemContext = itemContext ?? string.Empty,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Sets the buffered-item update interval on the MXAccess session
    /// (MXAccess <c>SetBufferedUpdateInterval</c>).
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task SetBufferedUpdateIntervalAsync(
        int serverHandle,
        int updateIntervalMilliseconds,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await SetBufferedUpdateIntervalRawAsync(
                serverHandle,
                updateIntervalMilliseconds,
                cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
    }

    /// <summary>
    /// Sets the buffered-item update interval without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> SetBufferedUpdateIntervalRawAsync(
        int serverHandle,
        int updateIntervalMilliseconds,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.SetBufferedUpdateInterval,
                SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
                {
                    ServerHandle = serverHandle,
                    UpdateIntervalMilliseconds = updateIntervalMilliseconds,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Suspends updates for an item (MXAccess <c>Suspend</c>).
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
    public async Task<MxStatusProxy?> SuspendAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await SuspendRawAsync(serverHandle, itemHandle, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.Suspend?.Status;
    }

    /// <summary>
    /// Suspends updates for an item without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> SuspendRawAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.Suspend,
                Suspend = new SuspendCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Resumes updates for a suspended item (MXAccess <c>Activate</c>).
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
    public async Task<MxStatusProxy?> ActivateAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await ActivateRawAsync(serverHandle, itemHandle, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return reply.Activate?.Status;
    }

    /// <summary>
    /// Resumes updates for a suspended item without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> ActivateRawAsync(
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken = default)
    {
        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.Activate,
                Activate = new ActivateCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Writes a secured value to an item on the MXAccess server (MXAccess <c>WriteSecured</c>).
    /// </summary>
    /// <remarks>
    /// MXAccess parity: <c>WriteSecured</c> fails when it is issued before a value-bearing
    /// NMX body or before a prior <c>AuthenticateUser</c> + <c>AdviseSupervisory</c>. That
    /// native failure is surfaced unchanged — the client does not pre-validate or reorder it.
    /// The <paramref name="value"/> is credential-sensitive and must never reach logs; the
    /// client mirrors the single-item WriteSecured redaction contract.
    /// </remarks>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The secured value to write.</param>
    /// <param name="currentUserId">The current operator user id.</param>
    /// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task WriteSecuredAsync(
        int serverHandle,
        int itemHandle,
        MxValue value,
        int currentUserId,
        int verifierUserId,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await WriteSecuredRawAsync(
                serverHandle,
                itemHandle,
                value,
                currentUserId,
                verifierUserId,
                cancellationToken)
            .ConfigureAwait(false);
        try
        {
            reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        }
        catch (MxGatewayException ex)
        {
            throw MxGatewaySecretRedaction.Redacted(ex, ExtractSecretString(value));
        }
    }

    /// <summary>
    /// Writes a secured value to an item without error checking. See
    /// <see cref="WriteSecuredAsync"/> for the parity and redaction contract.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The secured value to write.</param>
    /// <param name="currentUserId">The current operator user id.</param>
    /// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> WriteSecuredRawAsync(
        int serverHandle,
        int itemHandle,
        MxValue value,
        int currentUserId,
        int verifierUserId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(value);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.WriteSecured,
                WriteSecured = new WriteSecuredCommand
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                    CurrentUserId = currentUserId,
                    VerifierUserId = verifierUserId,
                    Value = value,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Writes a secured value and timestamp to an item (MXAccess <c>WriteSecured2</c>).
    /// </summary>
    /// <remarks>
    /// Same parity and redaction contract as <see cref="WriteSecuredAsync"/>: the native
    /// failure that occurs before a value-bearing NMX body or a prior authenticate is
    /// surfaced unchanged, and the credential-sensitive <paramref name="value"/> must never
    /// reach logs.
    /// </remarks>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The secured value to write.</param>
    /// <param name="timestampValue">The timestamp to write with the value.</param>
    /// <param name="currentUserId">The current operator user id.</param>
    /// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task WriteSecured2Async(
        int serverHandle,
        int itemHandle,
        MxValue value,
        MxValue timestampValue,
        int currentUserId,
        int verifierUserId,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await WriteSecured2RawAsync(
                serverHandle,
                itemHandle,
                value,
                timestampValue,
                currentUserId,
                verifierUserId,
                cancellationToken)
            .ConfigureAwait(false);
        try
        {
            reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        }
        catch (MxGatewayException ex)
        {
            throw MxGatewaySecretRedaction.Redacted(ex, ExtractSecretString(value));
        }
    }

    /// <summary>
    /// Writes a secured value and timestamp to an item without error checking. See
    /// <see cref="WriteSecured2Async"/> for the parity and redaction contract.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="itemHandle">The ItemHandle from add-item.</param>
    /// <param name="value">The secured value to write.</param>
    /// <param name="timestampValue">The timestamp to write with the value.</param>
    /// <param name="currentUserId">The current operator user id.</param>
    /// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> WriteSecured2RawAsync(
        int serverHandle,
        int itemHandle,
        MxValue value,
        MxValue timestampValue,
        int currentUserId,
        int verifierUserId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(value);
        ArgumentNullException.ThrowIfNull(timestampValue);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.WriteSecured2,
                WriteSecured2 = new WriteSecured2Command
                {
                    ServerHandle = serverHandle,
                    ItemHandle = itemHandle,
                    CurrentUserId = currentUserId,
                    VerifierUserId = verifierUserId,
                    Value = value,
                    TimestampValue = timestampValue,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Authenticates an MXAccess verify-user (MXAccess <c>AuthenticateUser</c>), returning
    /// the resolved user id used by <c>WriteSecured</c> / <c>WriteSecured2</c>.
    /// </summary>
    /// <remarks>
    /// The <paramref name="verifyUserPassword"/> is a raw MXAccess credential. It is never
    /// logged and never placed on the exception path: gateway/MXAccess failures surface only
    /// reply-derived diagnostics (kind, HRESULT, MXSTATUS_PROXY), never the request payload.
    /// </remarks>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="verifyUser">The user to verify.</param>
    /// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The authenticated user id.</returns>
    public async Task<int> AuthenticateUserAsync(
        int serverHandle,
        string verifyUser,
        string verifyUserPassword,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await AuthenticateUserRawAsync(
                serverHandle,
                verifyUser,
                verifyUserPassword,
                cancellationToken)
            .ConfigureAwait(false);
        try
        {
            reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
            return ResolveInt32Result(reply.AuthenticateUser?.UserId, reply, "AuthenticateUser");
        }
        catch (MxGatewayException ex)
        {
            throw MxGatewaySecretRedaction.Redacted(ex, verifyUserPassword);
        }
    }

    /// <summary>
    /// Authenticates an MXAccess verify-user without error checking. See
    /// <see cref="AuthenticateUserAsync"/> for the credential-handling contract.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="verifyUser">The user to verify.</param>
    /// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> AuthenticateUserRawAsync(
        int serverHandle,
        string verifyUser,
        string verifyUserPassword,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(verifyUser);
        ArgumentNullException.ThrowIfNull(verifyUserPassword);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.AuthenticateUser,
                AuthenticateUser = new AuthenticateUserCommand
                {
                    ServerHandle = serverHandle,
                    VerifyUser = verifyUser,
                    VerifyUserPassword = verifyUserPassword,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Resolves an ArchestrA user GUID to its MXAccess user id
    /// (MXAccess <c>ArchestrAUserToId</c>).
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The resolved MXAccess user id.</returns>
    public async Task<int> ArchestraUserToIdAsync(
        int serverHandle,
        string userIdGuid,
        CancellationToken cancellationToken = default)
    {
        MxCommandReply reply = await ArchestraUserToIdRawAsync(serverHandle, userIdGuid, cancellationToken)
            .ConfigureAwait(false);
        reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
        return ResolveInt32Result(reply.ArchestraUserToId?.UserId, reply, "ArchestrAUserToId");
    }

    /// <summary>
    /// Resolves an ArchestrA user GUID to its MXAccess user id without error checking.
    /// </summary>
    /// <param name="serverHandle">The ServerHandle from register.</param>
    /// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> ArchestraUserToIdRawAsync(
        int serverHandle,
        string userIdGuid,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(userIdGuid);

        return InvokeCommandAsync(
            new MxCommand
            {
                Kind = MxCommandKind.ArchestraUserToId,
                ArchestraUserToId = new ArchestrAUserToIdCommand
                {
                    ServerHandle = serverHandle,
                    UserIdGuid = userIdGuid,
                },
            },
            cancellationToken);
    }

    /// <summary>
    /// Resolves the int32 result of an OK command reply: the typed payload value when present,
    /// otherwise an int32 <c>return_value</c> when the reply carries one. A reply that provides
    /// neither is malformed and surfaces as <see cref="MxGatewayMalformedReplyException"/>
    /// rather than the historical <see cref="NullReferenceException"/>.
    /// </summary>
    /// <param name="typedValue">The typed payload value, or <see langword="null"/> when absent.</param>
    /// <param name="reply">The OK command reply.</param>
    /// <param name="operation">The MXAccess operation name, for the diagnostic message.</param>
    /// <returns>The resolved int32 result.</returns>
    private static int ResolveInt32Result(int? typedValue, MxCommandReply reply, string operation)
    {
        if (typedValue.HasValue)
        {
            return typedValue.Value;
        }

        if (reply.ReturnValue is not null
            && reply.ReturnValue.KindCase == MxValue.KindOneofCase.Int32Value)
        {
            return reply.ReturnValue.Int32Value;
        }

        throw new MxGatewayMalformedReplyException(
            $"{operation} returned a malformed reply: OK reply carried neither the typed payload nor an int32 return_value",
            reply.SessionId,
            reply.CorrelationId,
            reply.ProtocolStatus,
            reply.HasHresult ? reply.Hresult : null,
            reply.Statuses.ToArray());
    }

    /// <summary>
    /// Extracts the raw string form of a credential-bearing <see cref="MxValue"/> for redaction,
    /// or <see langword="null"/> when the value does not carry a string.
    /// </summary>
    /// <param name="value">The value written by a secured write.</param>
    /// <returns>The string payload, or <see langword="null"/>.</returns>
    private static string? ExtractSecretString(MxValue value)
    {
        return value.KindCase == MxValue.KindOneofCase.StringValue
            ? value.StringValue
            : null;
    }

    /// <summary>
    /// Invokes an MXAccess command on this session.
    /// </summary>
    /// <param name="request">The command request.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<MxCommandReply> InvokeAsync(
        MxCommandRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        return _client.InvokeAsync(request, cancellationToken);
    }

    /// <summary>
    /// Streams events from the worker for this session, optionally starting after a given sequence number.
    /// </summary>
    /// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>An async enumerable of events.</returns>
    public IAsyncEnumerable<MxEvent> StreamEventsAsync(
        ulong afterWorkerSequence = 0,
        CancellationToken cancellationToken = default)
    {
        return _client.StreamEventsAsync(
            new StreamEventsRequest
            {
                SessionId = SessionId,
                AfterWorkerSequence = afterWorkerSequence,
            },
            cancellationToken);
    }

    /// <summary>
    /// Streams events as typed <see cref="MxEventStreamItem"/> values, surfacing
    /// the gateway's reconnect-replay gap sentinel as an observable, typed signal.
    /// </summary>
    /// <remarks>
    /// When resuming with a stale <paramref name="afterWorkerSequence"/> (older than
    /// the oldest event still retained in the session replay ring), the gateway emits
    /// a single gap sentinel at the head of the stream. It arrives here as an item
    /// with <see cref="MxEventStreamItem.IsReplayGap"/> true and
    /// <see cref="MxEventStreamItem.ReplayGap"/> populated, meaning the consumer
    /// missed events and MUST discard local state and re-snapshot. To resume without
    /// incurring another gap, reconnect with
    /// <c>afterWorkerSequence = item.ReplayGap.OldestAvailableSequence - 1</c>.
    /// All other events pass through with <see cref="MxEventStreamItem.IsReplayGap"/>
    /// false. Use <see cref="StreamEventsAsync"/> when raw generated
    /// <see cref="MxEvent"/> messages are needed instead.
    /// </remarks>
    /// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>An async enumerable of typed event items.</returns>
    public IAsyncEnumerable<MxEventStreamItem> StreamEventItemsAsync(
        ulong afterWorkerSequence = 0,
        CancellationToken cancellationToken = default)
    {
        return StreamEventsAsync(afterWorkerSequence, cancellationToken)
            .AsStreamItemsAsync(cancellationToken);
    }

    /// <summary>
    /// Closes the session and releases resources.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async ValueTask DisposeAsync()
    {
        await CloseAsync().ConfigureAwait(false);
        _closeLock.Dispose();
    }

    private Task<MxCommandReply> InvokeCommandAsync(
        MxCommand command,
        CancellationToken cancellationToken)
    {
        return _client.InvokeAsync(
            new MxCommandRequest
            {
                SessionId = SessionId,
                ClientCorrelationId = Guid.NewGuid().ToString("N"),
                Command = command,
            },
            cancellationToken);
    }

}
