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>
    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>
    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>
    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>
    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 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>
    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>
    /// 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>
    /// Closes the session and releases resources.
    /// </summary>
    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);
    }

}
