package com.zb.mom.ww.mxgateway.client;

import java.security.SecureRandom;
import java.time.Duration;
import java.util.HexFormat;
import java.util.List;
import java.util.Objects;
import mxaccess_gateway.v1.MxaccessGateway.AddItem2Command;
import mxaccess_gateway.v1.MxaccessGateway.AddItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.AddItemCommand;
import mxaccess_gateway.v1.MxaccessGateway.AdviseItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.AdviseCommand;
import mxaccess_gateway.v1.MxaccessGateway.BulkReadResult;
import mxaccess_gateway.v1.MxaccessGateway.BulkWriteResult;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxCommand;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.ReadBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.RegisterCommand;
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemCommand;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
import mxaccess_gateway.v1.MxaccessGateway.SubscribeBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseCommand;
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.UnsubscribeBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand;
import mxaccess_gateway.v1.MxaccessGateway.Write2BulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.Write2BulkEntry;
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
import mxaccess_gateway.v1.MxaccessGateway.WriteBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.WriteBulkEntry;
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
import mxaccess_gateway.v1.MxaccessGateway.WriteSecured2BulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.WriteSecured2BulkEntry;
import mxaccess_gateway.v1.MxaccessGateway.WriteSecuredBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.WriteSecuredBulkEntry;

/**
 * Typed handle for a single MXAccess gateway session.
 *
 * <p>Wraps an {@link OpenSessionReply} together with the {@link MxGatewayClient}
 * that opened it and exposes the MXAccess command surface (Register, AddItem,
 * Advise, bulk subscribe variants, Write, event streaming, and close). Each
 * command request carries a freshly generated client correlation id.
 */
public final class MxGatewaySession implements AutoCloseable {
    private static final SecureRandom RANDOM = new SecureRandom();

    private final MxGatewayClient client;
    private final OpenSessionReply openReply;
    private CloseSessionReply closeReply;

    MxGatewaySession(MxGatewayClient client, OpenSessionReply openReply) {
        this.client = Objects.requireNonNull(client, "client");
        this.openReply = Objects.requireNonNull(openReply, "openReply");
    }

    /**
     * Builds a session handle for an existing gateway session id without
     * issuing an {@code OpenSession} call. Useful for CLI tools that operate
     * against a session opened in a separate invocation.
     *
     * @param client the gateway client used for further commands
     * @param sessionId the existing gateway session id
     * @return a session handle bound to the supplied id
     */
    public static MxGatewaySession forSessionId(MxGatewayClient client, String sessionId) {
        return new MxGatewaySession(
                client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
    }

    /**
     * Returns the gateway-assigned session id.
     *
     * @return the session id
     */
    public String sessionId() {
        return openReply.getSessionId();
    }

    /**
     * Returns the original {@link OpenSessionReply} that this session was opened with.
     *
     * @return the open-session reply
     */
    public OpenSessionReply openReply() {
        return openReply;
    }

    /**
     * Sends a {@code CloseSession} RPC and caches the reply so subsequent calls
     * are idempotent.
     *
     * @return the raw close-session reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public synchronized CloseSessionReply closeRaw() {
        if (closeReply == null) {
            closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
                    .setSessionId(sessionId())
                    .setClientCorrelationId(newCorrelationId())
                    .build());
        }
        return closeReply;
    }

    @Override
    public void close() {
        closeRaw();
    }

    /**
     * Invokes MXAccess {@code Register} and returns the server handle.
     *
     * @param clientName the MXAccess client name to register
     * @return the {@code ServerHandle} returned by MXAccess
     * @throws MxGatewayException on transport or protocol failure
     */
    public int register(String clientName) {
        MxCommandReply reply = registerRaw(clientName);
        if (reply.hasRegister()) {
            return reply.getRegister().getServerHandle();
        }
        return reply.getReturnValue().getInt32Value();
    }

    /**
     * Invokes MXAccess {@code Register} and returns the raw reply.
     *
     * @param clientName the MXAccess client name to register
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply registerRaw(String clientName) {
        return invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
                .setRegister(RegisterCommand.newBuilder().setClientName(clientName))
                .build());
    }

    /**
     * Invokes MXAccess {@code Unregister}.
     *
     * @param serverHandle the {@code ServerHandle} returned by {@link #register(String)}
     * @throws MxGatewayException on transport or protocol failure
     */
    public void unregister(int serverHandle) {
        invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
                .setUnregister(UnregisterCommand.newBuilder().setServerHandle(serverHandle))
                .build());
    }

    /**
     * Invokes MXAccess {@code AddItem} and returns the new item handle.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemDefinition the MXAccess item definition (tag reference)
     * @return the {@code ItemHandle} assigned by MXAccess
     * @throws MxGatewayException on transport or protocol failure
     */
    public int addItem(int serverHandle, String itemDefinition) {
        MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
        if (reply.hasAddItem()) {
            return reply.getAddItem().getItemHandle();
        }
        return reply.getReturnValue().getInt32Value();
    }

    /**
     * Invokes MXAccess {@code AddItem} and returns the raw reply.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemDefinition the MXAccess item definition
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
        return invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
                .setAddItem(AddItemCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemDefinition(itemDefinition))
                .build());
    }

    /**
     * Invokes MXAccess {@code AddItem2} and returns the new item handle.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemDefinition the MXAccess item definition
     * @param itemContext the MXAccess item context (e.g. galaxy/object scope)
     * @return the {@code ItemHandle} assigned by MXAccess
     * @throws MxGatewayException on transport or protocol failure
     */
    public int addItem2(int serverHandle, String itemDefinition, String itemContext) {
        MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
        if (reply.hasAddItem2()) {
            return reply.getAddItem2().getItemHandle();
        }
        return reply.getReturnValue().getInt32Value();
    }

    /**
     * Invokes MXAccess {@code AddItem2} and returns the raw reply.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemDefinition the MXAccess item definition
     * @param itemContext the MXAccess item context
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply addItem2Raw(int serverHandle, String itemDefinition, String itemContext) {
        return invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
                .setAddItem2(AddItem2Command.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemDefinition(itemDefinition)
                        .setItemContext(itemContext))
                .build());
    }

    /**
     * Invokes MXAccess {@code RemoveItem}.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to remove
     * @throws MxGatewayException on transport or protocol failure
     */
    public void removeItem(int serverHandle, int itemHandle) {
        removeItemRaw(serverHandle, itemHandle);
    }

    /**
     * Invokes MXAccess {@code RemoveItem} and returns the raw reply.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to remove
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply removeItemRaw(int serverHandle, int itemHandle) {
        return invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
                .setRemoveItem(RemoveItemCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(itemHandle))
                .build());
    }

    /**
     * Invokes MXAccess {@code Advise} so the item starts emitting data-change events.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to advise
     * @throws MxGatewayException on transport or protocol failure
     */
    public void advise(int serverHandle, int itemHandle) {
        adviseRaw(serverHandle, itemHandle);
    }

    /**
     * Invokes MXAccess {@code Advise} and returns the raw reply.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to advise
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
        return invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
                .setAdvise(AdviseCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(itemHandle))
                .build());
    }

    /**
     * Invokes MXAccess {@code UnAdvise} so the item stops emitting data-change events.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to un-advise
     * @throws MxGatewayException on transport or protocol failure
     */
    public void unAdvise(int serverHandle, int itemHandle) {
        unAdviseRaw(serverHandle, itemHandle);
    }

    /**
     * Invokes MXAccess {@code UnAdvise} and returns the raw reply.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to un-advise
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply unAdviseRaw(int serverHandle, int itemHandle) {
        return invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
                .setUnAdvise(UnAdviseCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(itemHandle))
                .build());
    }

    /**
     * Invokes the bulk {@code AddItem} variant.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param tagAddresses the MXAccess tag addresses to add
     * @return a per-tag {@link SubscribeResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code tagAddresses} is {@code null}
     */
    public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses) {
        Objects.requireNonNull(tagAddresses, "tagAddresses");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM_BULK)
                .setAddItemBulk(AddItemBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllTagAddresses(tagAddresses))
                .build());
        return reply.getAddItemBulk().getResultsList();
    }

    /**
     * Invokes the bulk {@code Advise} variant.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param itemHandles the {@code ItemHandle} list to advise
     * @return a per-item {@link SubscribeResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code itemHandles} is {@code null}
     */
    public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles) {
        Objects.requireNonNull(itemHandles, "itemHandles");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_ITEM_BULK)
                .setAdviseItemBulk(AdviseItemBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllItemHandles(itemHandles))
                .build());
        return reply.getAdviseItemBulk().getResultsList();
    }

    /**
     * Invokes the bulk {@code RemoveItem} variant.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param itemHandles the {@code ItemHandle} list to remove
     * @return a per-item {@link SubscribeResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code itemHandles} is {@code null}
     */
    public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles) {
        Objects.requireNonNull(itemHandles, "itemHandles");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM_BULK)
                .setRemoveItemBulk(RemoveItemBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllItemHandles(itemHandles))
                .build());
        return reply.getRemoveItemBulk().getResultsList();
    }

    /**
     * Invokes the bulk {@code UnAdvise} variant.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param itemHandles the {@code ItemHandle} list to un-advise
     * @return a per-item {@link SubscribeResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code itemHandles} is {@code null}
     */
    public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles) {
        Objects.requireNonNull(itemHandles, "itemHandles");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK)
                .setUnAdviseItemBulk(UnAdviseItemBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllItemHandles(itemHandles))
                .build());
        return reply.getUnAdviseItemBulk().getResultsList();
    }

    /**
     * Invokes the gateway {@code SubscribeBulk} convenience that combines
     * AddItem and Advise for the supplied tag addresses.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param tagAddresses the MXAccess tag addresses to subscribe
     * @return a per-tag {@link SubscribeResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code tagAddresses} is {@code null}
     */
    public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses) {
        Objects.requireNonNull(tagAddresses, "tagAddresses");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_SUBSCRIBE_BULK)
                .setSubscribeBulk(SubscribeBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllTagAddresses(tagAddresses))
                .build());
        return reply.getSubscribeBulk().getResultsList();
    }

    /**
     * Invokes the gateway {@code UnsubscribeBulk} convenience that combines
     * UnAdvise and RemoveItem for the supplied item handles.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param itemHandles the {@code ItemHandle} list to unsubscribe
     * @return a per-item {@link SubscribeResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code itemHandles} is {@code null}
     */
    public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
        Objects.requireNonNull(itemHandles, "itemHandles");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_UNSUBSCRIBE_BULK)
                .setUnsubscribeBulk(UnsubscribeBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllItemHandles(itemHandles))
                .build());
        return reply.getUnsubscribeBulk().getResultsList();
    }

    /**
     * Bulk {@code Write} — sequential MXAccess Write per entry on the worker's STA.
     *
     * <p>Per-entry failures appear as {@link BulkWriteResult} entries with
     * {@code wasSuccessful == false}; this method does not throw for per-entry
     * MXAccess failures (it still throws {@link MxGatewayException} on transport
     * or protocol-level failures).
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param entries the per-item (handle, value, user id) tuples
     * @return a per-entry {@link BulkWriteResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code entries} is {@code null}
     */
    public List<BulkWriteResult> writeBulk(int serverHandle, List<WriteBulkEntry> entries) {
        Objects.requireNonNull(entries, "entries");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE_BULK)
                .setWriteBulk(WriteBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllEntries(entries))
                .build());
        return reply.getWriteBulk().getResultsList();
    }

    /**
     * Bulk {@code Write2} — sequential MXAccess Write2 (timestamped) per entry.
     *
     * <p>Per-entry semantics mirror {@link #writeBulk(int, List)}.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param entries the per-item (handle, value, timestamp, user id) tuples
     * @return a per-entry {@link BulkWriteResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code entries} is {@code null}
     */
    public List<BulkWriteResult> write2Bulk(int serverHandle, List<Write2BulkEntry> entries) {
        Objects.requireNonNull(entries, "entries");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2_BULK)
                .setWrite2Bulk(Write2BulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllEntries(entries))
                .build());
        return reply.getWrite2Bulk().getResultsList();
    }

    /**
     * Bulk {@code WriteSecured} — credential-sensitive values must not be logged
     * by callers; mirrors the single-item write-secured redaction contract.
     *
     * <p>Per-entry semantics mirror {@link #writeBulk(int, List)}.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param entries the per-item (handle, value, current+verifier user id) tuples
     * @return a per-entry {@link BulkWriteResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code entries} is {@code null}
     */
    public List<BulkWriteResult> writeSecuredBulk(int serverHandle, List<WriteSecuredBulkEntry> entries) {
        Objects.requireNonNull(entries, "entries");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE_SECURED_BULK)
                .setWriteSecuredBulk(WriteSecuredBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllEntries(entries))
                .build());
        return reply.getWriteSecuredBulk().getResultsList();
    }

    /**
     * Bulk {@code WriteSecured2} — sequential timestamped + verified write per entry.
     *
     * <p>Per-entry semantics mirror {@link #writeBulk(int, List)}.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param entries the per-item (handle, value, timestamp, current+verifier user id) tuples
     * @return a per-entry {@link BulkWriteResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code entries} is {@code null}
     */
    public List<BulkWriteResult> writeSecured2Bulk(int serverHandle, List<WriteSecured2BulkEntry> entries) {
        Objects.requireNonNull(entries, "entries");
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE_SECURED2_BULK)
                .setWriteSecured2Bulk(WriteSecured2BulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllEntries(entries))
                .build());
        return reply.getWriteSecured2Bulk().getResultsList();
    }

    /**
     * Bulk {@code Read} — snapshot the current value of each requested tag.
     *
     * <p>MXAccess COM has no synchronous read; the worker returns the cached
     * {@code OnDataChange} value for any tag that is already advised
     * ({@code wasCached == true}) without modifying the existing subscription,
     * and falls back to a full AddItem + Advise + wait + UnAdvise + RemoveItem
     * snapshot lifecycle otherwise. The supplied {@code timeout} bounds the
     * per-tag wait in the snapshot case; pass {@link Duration#ZERO} (or
     * {@code null}) to use the worker default (1000 ms). Per-tag failures
     * appear as {@link BulkReadResult} entries with {@code wasSuccessful == false};
     * this method does not throw for per-tag MXAccess failures.
     *
     * @param serverHandle the {@code ServerHandle} owning the items
     * @param tagAddresses the tag addresses to read
     * @param timeout per-tag snapshot timeout (zero or null = worker default)
     * @return a per-tag {@link BulkReadResult} list
     * @throws MxGatewayException on transport or protocol failure
     * @throws NullPointerException if {@code tagAddresses} is {@code null}
     * @throws IllegalArgumentException if {@code timeout} is negative or exceeds {@link Integer#MAX_VALUE} milliseconds
     */
    public List<BulkReadResult> readBulk(int serverHandle, List<String> tagAddresses, Duration timeout) {
        Objects.requireNonNull(tagAddresses, "tagAddresses");
        int timeoutMs = 0;
        if (timeout != null) {
            if (timeout.isNegative()) {
                throw new IllegalArgumentException("timeout must be non-negative");
            }
            long millis = timeout.toMillis();
            if (millis > Integer.MAX_VALUE) {
                throw new IllegalArgumentException("timeout exceeds Integer.MAX_VALUE milliseconds");
            }
            timeoutMs = (int) millis;
        }
        MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_READ_BULK)
                .setReadBulk(ReadBulkCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .addAllTagAddresses(tagAddresses)
                        .setTimeoutMs(timeoutMs))
                .build());
        return reply.getReadBulk().getResultsList();
    }

    /**
     * Invokes MXAccess {@code Write}.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to write
     * @param value the value to write
     * @param userId the MXAccess user id used for security checks
     * @throws MxGatewayException on transport or protocol failure
     */
    public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
        writeRaw(serverHandle, itemHandle, value, userId);
    }

    /**
     * Invokes MXAccess {@code Write} and returns the raw reply.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to write
     * @param value the value to write
     * @param userId the MXAccess user id used for security checks
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
        return invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
                .setWrite(WriteCommand.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(itemHandle)
                        .setValue(value)
                        .setUserId(userId))
                .build());
    }

    /**
     * Invokes MXAccess {@code Write2}, which carries an explicit timestamp.
     *
     * @param serverHandle the {@code ServerHandle} owning the item
     * @param itemHandle the {@code ItemHandle} to write
     * @param value the value to write
     * @param timestampValue the timestamp value to associate with the write
     * @param userId the MXAccess user id used for security checks
     * @throws MxGatewayException on transport or protocol failure
     */
    public void write2(int serverHandle, int itemHandle, MxValue value, MxValue timestampValue, int userId) {
        invokeCommand(MxCommand.newBuilder()
                .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
                .setWrite2(Write2Command.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(itemHandle)
                        .setValue(value)
                        .setTimestampValue(timestampValue)
                        .setUserId(userId))
                .build());
    }

    /**
     * Subscribes to gateway events for this session starting from the
     * beginning of the worker event log.
     *
     * @return an iterator-style stream of events
     */
    public MxEventStream streamEvents() {
        return streamEventsAfter(0);
    }

    /**
     * Subscribes to gateway events for this session starting after the
     * supplied worker sequence number.
     *
     * @param afterWorkerSequence the resume cursor; events with worker sequence
     *     greater than this value are delivered
     * @return an iterator-style stream of events
     */
    public MxEventStream streamEventsAfter(long afterWorkerSequence) {
        return client.streamEvents(StreamEventsRequest.newBuilder()
                .setSessionId(sessionId())
                .setAfterWorkerSequence(afterWorkerSequence)
                .build());
    }

    /**
     * Sends a pre-built {@link MxCommand} for this session and returns the raw
     * reply, attaching a freshly generated client correlation id.
     *
     * @param command the command to send
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxCommandReply invokeCommand(MxCommand command) {
        return client.invoke(MxCommandRequest.newBuilder()
                .setSessionId(sessionId())
                .setClientCorrelationId(newCorrelationId())
                .setCommand(command)
                .build());
    }

    private static String newCorrelationId() {
        byte[] bytes = new byte[16];
        RANDOM.nextBytes(bytes);
        return HexFormat.of().formatHex(bytes);
    }
}
