using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;

/// <summary>
/// Configuration options for OPC UA connections, parsed from connection details JSON.
/// All values have defaults matching the OPC Foundation SDK's typical settings.
/// </summary>
public record OpcUaConnectionOptions(
    int SessionTimeoutMs = 60000,
    int OperationTimeoutMs = 15000,
    int PublishingIntervalMs = 1000,
    int KeepAliveCount = 10,
    int LifetimeCount = 30,
    int MaxNotificationsPerPublish = 100,
    int SamplingIntervalMs = 1000,
    int QueueSize = 10,
    string SecurityMode = "None",
    // Secure-by-default — untrusted server certificates are
    // rejected unless an operator explicitly opts in per connection. Accepting any
    // certificate defeats the Sign / SignAndEncrypt modes against a man-in-the-middle.
    bool AutoAcceptUntrustedCerts = false,
    bool DiscardOldest = true,
    byte SubscriptionPriority = 0,
    string SubscriptionDisplayName = "ScadaBridge",
    string TimestampsToReturn = "Source",
    OpcUaDeadbandOptions? Deadband = null,
    OpcUaUserIdentityOptions? UserIdentity = null,
    // Monitored items above this ceiling are sharded onto an additional
    // subscription on the same session (see RealOpcUaClient). Per endpoint, because
    // item-count limits are a property of the target server.
    int MaxMonitoredItemsPerSubscription = 5000);

public record OpcUaDeadbandOptions(string Type, double Value);

public record OpcUaUserIdentityOptions(
    string TokenType,
    string Username,
    string Password,
    string CertificatePath,
    string CertificatePassword);

/// <summary>
/// Per-node outcome of a batch monitored-item create. A node that fails to resolve or
/// whose monitored item comes back with a bad create status is reported as a failed row
/// — never by aborting the whole batch (a connection-level fault still throws).
/// </summary>
/// <param name="NodeId">The requested node id, verbatim.</param>
/// <param name="Success">Whether the monitored item was created.</param>
/// <param name="SubscriptionHandle">Handle for <see cref="IOpcUaClient.RemoveSubscriptionAsync"/> when successful.</param>
/// <param name="Error">Failure reason when not successful.</param>
public record OpcUaSubscribeOutcome(string NodeId, bool Success, string? SubscriptionHandle, string? Error);

/// <summary>Per-node outcome of a batch read.</summary>
/// <param name="NodeId">The requested node id, verbatim.</param>
/// <param name="Value">The value read, when the read produced one.</param>
/// <param name="SourceTimestamp">Source timestamp reported by the server.</param>
/// <param name="StatusCode">OPC UA status code for this node's read.</param>
/// <param name="Error">Set when the node could not be read at all (e.g. an unresolvable node id).</param>
public record OpcUaReadOutcome(
    string NodeId, object? Value, DateTime SourceTimestamp, uint StatusCode, string? Error);

/// <summary>Per-node outcome of a batch write.</summary>
/// <param name="NodeId">The requested node id, verbatim.</param>
/// <param name="StatusCode">OPC UA status code for this node's write (0 = Good).</param>
/// <param name="Error">Set when the node could not be written at all (e.g. an unresolvable node id).</param>
public record OpcUaWriteOutcome(string NodeId, uint StatusCode, string? Error);

/// <summary>
/// Abstraction over OPC UA client library for testability.
/// The real implementation would wrap an OPC UA SDK (e.g., OPC Foundation .NET Standard Library).
/// </summary>
public interface IOpcUaClient : IAsyncDisposable
{
    /// <summary>
    /// Connects to an OPC UA server at the specified endpoint URL.
    /// </summary>
    /// <param name="endpointUrl">The OPC UA server endpoint URL.</param>
    /// <param name="options">Connection options; if null, defaults are used.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task ConnectAsync(string endpointUrl, OpcUaConnectionOptions? options = null, CancellationToken cancellationToken = default);
    /// <summary>
    /// Disconnects from the OPC UA server.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task DisconnectAsync(CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets a value indicating whether the client is currently connected to the server.
    /// </summary>
    bool IsConnected { get; }

    /// <summary>
    /// Creates a monitored item subscription for a node. Returns a subscription handle.
    /// </summary>
    /// <param name="nodeId">The OPC UA node ID to monitor.</param>
    /// <param name="onValueChanged">Callback invoked when the monitored value changes, receiving nodeId, value, sourceTimestamp, and statusCode.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task that completes with a subscription handle string.</returns>
    Task<string> CreateSubscriptionAsync(
        string nodeId,
        Action<string, object?, DateTime, uint> onValueChanged,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Creates monitored items for MANY nodes with ONE ApplyChanges per touched
    /// subscription — the batch counterpart of <see cref="CreateSubscriptionAsync"/> and
    /// the reason the DCL no longer issues one ApplyChanges per tag. All nodes share the
    /// single <paramref name="onValueChanged"/> callback (its first argument is the node
    /// id, so per-node delegates would carry nothing extra).
    /// </summary>
    /// <param name="nodeIds">The node ids to monitor.</param>
    /// <param name="onValueChanged">Callback invoked with (nodeId, value, sourceTimestamp, statusCode) on each change.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>One outcome per requested node id, in request order.</returns>
    Task<IReadOnlyList<OpcUaSubscribeOutcome>> CreateSubscriptionsAsync(
        IReadOnlyList<string> nodeIds,
        Action<string, object?, DateTime, uint> onValueChanged,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Removes a monitored item subscription by handle.
    /// </summary>
    /// <param name="subscriptionHandle">The subscription handle returned by CreateSubscriptionAsync.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task RemoveSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default);

    /// <summary>
    /// Removes many monitored items, grouping ONE ApplyChanges per touched subscription.
    /// Unknown handles are ignored.
    /// </summary>
    /// <param name="subscriptionHandles">Handles previously returned by a create call.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task RemoveSubscriptionsAsync(
        IReadOnlyList<string> subscriptionHandles, CancellationToken cancellationToken = default);

    /// <summary>
    /// Subscribes to OPC UA Alarms &amp; Conditions events under
    /// <paramref name="sourceNodeId"/> (or the Server object when null). On
    /// (re)subscribe the adapter issues a ConditionRefresh and replays the
    /// active conditions as Snapshot…SnapshotComplete transitions. Returns a
    /// handle for <see cref="RemoveAlarmSubscriptionAsync"/>.
    /// </summary>
    /// <param name="sourceNodeId">OPC UA node ID to monitor; null subscribes to the Server object.</param>
    /// <param name="conditionFilter">Optional OPC UA condition type filter; null subscribes to all conditions.</param>
    /// <param name="onTransition">Callback invoked for each alarm transition received from the server.</param>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    /// <returns>A task that resolves to a subscription handle string for use with <see cref="RemoveAlarmSubscriptionAsync"/>.</returns>
    Task<string> CreateAlarmSubscriptionAsync(
        string? sourceNodeId,
        string? conditionFilter,
        Action<NativeAlarmTransition> onTransition,
        CancellationToken cancellationToken = default);

    /// <summary>Removes an alarm-event subscription by handle.</summary>
    /// <param name="subscriptionHandle">The handle returned by <see cref="CreateAlarmSubscriptionAsync"/>.</param>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task RemoveAlarmSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default);

    /// <summary>
    /// Reads the current value of a node.
    /// </summary>
    /// <param name="nodeId">The OPC UA node ID to read.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task that completes with a tuple of (value, sourceTimestamp, statusCode).</returns>
    Task<(object? Value, DateTime SourceTimestamp, uint StatusCode)> ReadValueAsync(
        string nodeId, CancellationToken cancellationToken = default);

    /// <summary>
    /// Writes a value to a node.
    /// </summary>
    /// <param name="nodeId">The OPC UA node ID to write to.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task that completes with the OPC UA status code of the write operation.</returns>
    Task<uint> WriteValueAsync(string nodeId, object? value, CancellationToken cancellationToken = default);

    /// <summary>
    /// Reads many nodes in ONE OPC UA Read service call (chunked internally against the
    /// server's operation limits).
    /// </summary>
    /// <param name="nodeIds">The node ids to read.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>One outcome per requested node id, in request order.</returns>
    Task<IReadOnlyList<OpcUaReadOutcome>> ReadValuesAsync(
        IReadOnlyList<string> nodeIds, CancellationToken cancellationToken = default);

    /// <summary>
    /// Writes many nodes in ONE OPC UA Write service call (chunked internally against the
    /// server's operation limits).
    /// </summary>
    /// <param name="values">The node id / value pairs to write.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>One outcome per requested node id, in request order.</returns>
    Task<IReadOnlyList<OpcUaWriteOutcome>> WriteValuesAsync(
        IReadOnlyList<(string NodeId, object? Value)> values, CancellationToken cancellationToken = default);

    /// <summary>
    /// Raised when the OPC UA session detects a keep-alive failure or the server
    /// becomes unreachable. The adapter layer uses this to trigger reconnection.
    /// </summary>
    event Action? ConnectionLost;

    /// <summary>
    /// Enumerates the immediate children of <paramref name="parentNodeId"/>
    /// (or the server's ObjectsFolder when null). Pageable via the OPC UA
    /// continuation-point mechanism: when a prior call returned a non-null
    /// <see cref="BrowseChildrenResult.ContinuationToken"/>, pass it back via
    /// <paramref name="continuationToken"/> to fetch the next page (BrowseNext);
    /// a null/empty token starts a fresh browse. Throws
    /// <see cref="ConnectionNotConnectedException"/> when the session is not
    /// currently up.
    /// </summary>
    /// <param name="parentNodeId">Node id whose children to browse, or null for the server root.</param>
    /// <param name="continuationToken">Opaque token from a prior <see cref="BrowseChildrenResult.ContinuationToken"/> to fetch the next page via BrowseNext; null/empty starts a fresh browse.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task that completes with the immediate children of the requested node and a continuation token for the next page (null when exhausted).</returns>
    Task<BrowseChildrenResult> BrowseChildrenAsync(
        string? parentNodeId,
        string? continuationToken = null,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Walks the server's address space breadth-first from the
    /// root (ObjectsFolder), bounded by <paramref name="maxDepth"/> and
    /// <paramref name="maxResults"/>, returning the nodes whose DisplayName or
    /// root-relative path contains <paramref name="query"/> (case-insensitive
    /// substring). Implemented over this client's own
    /// <see cref="BrowseChildrenAsync"/> (paging the continuation per node so
    /// no children are missed). Throws
    /// <see cref="ConnectionNotConnectedException"/> when the session is not
    /// currently up. An empty/whitespace query matches nothing.
    /// </summary>
    /// <param name="query">Case-insensitive substring matched against each node's DisplayName and root-relative path; empty/whitespace matches nothing.</param>
    /// <param name="maxDepth">Maximum number of levels below the root to descend.</param>
    /// <param name="maxResults">Maximum matches to return; when reached the walk stops early and the result's CapReached flag is set.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task that completes with the matches found and a flag indicating whether a bound cut the walk short.</returns>
    Task<AddressSpaceSearchResult> SearchAddressSpaceAsync(
        string query,
        int maxDepth,
        int maxResults,
        CancellationToken cancellationToken = default);
}

/// <summary>
/// Shared bounded breadth-first address-space search, expressed
/// purely over an injected browse delegate so the stub and real OPC UA clients
/// run the IDENTICAL walk (matching, continuation-paging, depth bound, result
/// cap, node-visit ceiling, cancellation). The delegate is each client's own
/// <see cref="IOpcUaClient.BrowseChildrenAsync"/>.
/// </summary>
internal static class AddressSpaceSearch
{
    // Total nodes visited before the walk gives up and reports CapReached, even
    // if maxResults was not hit. Guards against a pathological / cyclic address
    // space (v1 keeps no visited-set) and runaway breadth. Generous relative to
    // any realistic depth+result bound a UI search would request.
    internal const int VisitedNodeCeiling = 10_000;

    /// <summary>
    /// Runs the bounded BFS. <paramref name="browse"/> mirrors
    /// <see cref="IBrowsableDataConnection.BrowseChildrenAsync"/>
    /// (parentNodeId, continuationToken, ct) → page of children.
    /// </summary>
    /// <param name="browse">Delegate that pages the children of a node: (parentNodeId, continuationToken, ct) → page of children.</param>
    /// <param name="query">Case-insensitive substring matched against each node's DisplayName and root-relative path; empty/whitespace matches nothing.</param>
    /// <param name="maxDepth">Maximum number of levels below the root to descend.</param>
    /// <param name="maxResults">Maximum matches to return; when reached the walk stops early and the result's CapReached flag is set.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task that completes with the matches found and a flag indicating whether a bound cut the walk short.</returns>
    internal static async Task<AddressSpaceSearchResult> SearchAsync(
        Func<string?, string?, CancellationToken, Task<BrowseChildrenResult>> browse,
        string query,
        int maxDepth,
        int maxResults,
        CancellationToken cancellationToken)
    {
        // Empty/whitespace query → no walk, no matches (matches nothing).
        if (string.IsNullOrWhiteSpace(query) || maxResults <= 0)
            return new AddressSpaceSearchResult(Array.Empty<AddressSpaceMatch>(), CapReached: false);

        var matches = new List<AddressSpaceMatch>();
        var capReached = false;
        var visited = 0;

        // Queue of (nodeId, pathPrefix, depth). Root = null nodeId, "" prefix,
        // depth 0; its children sit at depth 1.
        var queue = new Queue<(string? NodeId, string PathPrefix, int Depth)>();
        queue.Enqueue((null, string.Empty, 0));

        while (queue.Count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var (nodeId, pathPrefix, depth) = queue.Dequeue();

            // Page the continuation per node so no children are missed: the
            // first call starts a fresh browse; while a continuation token is
            // returned, fetch the next page and keep processing.
            string? continuationToken = null;
            do
            {
                cancellationToken.ThrowIfCancellationRequested();
                var page = await browse(nodeId, continuationToken, cancellationToken).ConfigureAwait(false);

                foreach (var child in page.Children)
                {
                    if (++visited > VisitedNodeCeiling)
                        return new AddressSpaceSearchResult(matches, CapReached: true);

                    var path = pathPrefix + "/" + child.DisplayName;
                    if (Contains(child.DisplayName, query) || Contains(path, query))
                    {
                        matches.Add(new AddressSpaceMatch(child, path));
                        if (matches.Count >= maxResults)
                            return new AddressSpaceSearchResult(matches, CapReached: true);
                    }

                    // Descend into Object nodes within the depth budget. depth is
                    // the parent's depth; children sit at depth + 1.
                    if (child.NodeClass == BrowseNodeClass.Object && depth < maxDepth)
                        queue.Enqueue((child.NodeId, path, depth + 1));
                }

                continuationToken = page.Truncated ? page.ContinuationToken : null;
            }
            while (!string.IsNullOrEmpty(continuationToken));
        }

        return new AddressSpaceSearchResult(matches, capReached);
    }

    private static bool Contains(string haystack, string needle) =>
        haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Factory for creating IOpcUaClient instances.
/// </summary>
public interface IOpcUaClientFactory
{
    /// <summary>
    /// Creates a new IOpcUaClient instance.
    /// </summary>
    /// <returns>A new IOpcUaClient instance.</returns>
    IOpcUaClient Create();
}

/// <summary>
/// Default factory that creates stub OPC UA clients.
/// In production, this would create real OPC UA SDK client instances.
/// </summary>
public class DefaultOpcUaClientFactory : IOpcUaClientFactory
{
    /// <inheritdoc />
    public IOpcUaClient Create() => new StubOpcUaClient();
}

/// <summary>
/// Stub OPC UA client for development/testing. A real implementation would
/// wrap the OPC Foundation .NET Standard Library.
/// </summary>
internal class StubOpcUaClient : IOpcUaClient
{
    /// <inheritdoc />
    public bool IsConnected { get; private set; }
#pragma warning disable CS0067
    /// <summary>Raised when the OPC UA session detects a keep-alive failure or the server becomes unreachable.</summary>
    public event Action? ConnectionLost;
#pragma warning restore CS0067

    /// <inheritdoc />
    public Task ConnectAsync(string endpointUrl, OpcUaConnectionOptions? options = null, CancellationToken cancellationToken = default)
    {
        IsConnected = true;
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        IsConnected = false;
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task<string> CreateSubscriptionAsync(
        string nodeId, Action<string, object?, DateTime, uint> onValueChanged,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult(Guid.NewGuid().ToString());
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<OpcUaSubscribeOutcome>> CreateSubscriptionsAsync(
        IReadOnlyList<string> nodeIds,
        Action<string, object?, DateTime, uint> onValueChanged,
        CancellationToken cancellationToken = default)
    {
        IReadOnlyList<OpcUaSubscribeOutcome> outcomes = nodeIds
            .Select(n => new OpcUaSubscribeOutcome(n, true, Guid.NewGuid().ToString(), null))
            .ToList();
        return Task.FromResult(outcomes);
    }

    /// <inheritdoc />
    public Task RemoveSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task RemoveSubscriptionsAsync(
        IReadOnlyList<string> subscriptionHandles, CancellationToken cancellationToken = default)
        => Task.CompletedTask;

    /// <inheritdoc />
    public Task<string> CreateAlarmSubscriptionAsync(
        string? sourceNodeId, string? conditionFilter,
        Action<NativeAlarmTransition> onTransition, CancellationToken cancellationToken = default)
    {
        // Stub: no events. Real A&C subscription lives in RealOpcUaClient.
        return Task.FromResult(Guid.NewGuid().ToString());
    }

    /// <inheritdoc />
    public Task RemoveAlarmSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default)
        => Task.CompletedTask;

    /// <inheritdoc />
    public Task<(object? Value, DateTime SourceTimestamp, uint StatusCode)> ReadValueAsync(
        string nodeId, CancellationToken cancellationToken = default)
    {
        return Task.FromResult<(object?, DateTime, uint)>((null, DateTime.UtcNow, 0));
    }

    /// <inheritdoc />
    public Task<uint> WriteValueAsync(string nodeId, object? value, CancellationToken cancellationToken = default)
    {
        return Task.FromResult<uint>(0); // Good status
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<OpcUaReadOutcome>> ReadValuesAsync(
        IReadOnlyList<string> nodeIds, CancellationToken cancellationToken = default)
    {
        IReadOnlyList<OpcUaReadOutcome> outcomes = nodeIds
            .Select(n => new OpcUaReadOutcome(n, null, DateTime.UtcNow, 0u, null))
            .ToList();
        return Task.FromResult(outcomes);
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<OpcUaWriteOutcome>> WriteValuesAsync(
        IReadOnlyList<(string NodeId, object? Value)> values, CancellationToken cancellationToken = default)
    {
        IReadOnlyList<OpcUaWriteOutcome> outcomes = values
            .Select(v => new OpcUaWriteOutcome(v.NodeId, 0u, null))
            .ToList();
        return Task.FromResult(outcomes);
    }

    /// <inheritdoc />
    public Task<BrowseChildrenResult> BrowseChildrenAsync(
        string? parentNodeId, string? continuationToken = null, CancellationToken cancellationToken = default)
    {
        // Canned address-space tree so DCL browse/paging flows can be exercised
        // without a live OPC UA server:
        //   (root)
        //   ├─ Folder1  (Object, HasChildren=true)
        //   │   ├─ Tag1 (Variable)      ← page 1
        //   │   └─ Tag2 (Variable)      ← page 2 (continuation token "STUB_PAGE2")
        //   └─ Folder2  (Object, HasChildren=false, leaf)
        // Folder1 fakes BrowseNext continuation paging so the continuation-token
        // round-trip is testable; everything else is a single, exhausted page.
        if (string.IsNullOrEmpty(parentNodeId))
        {
            return Task.FromResult(new BrowseChildrenResult(
                new[]
                {
                    new BrowseNode("Folder1", "Folder1", BrowseNodeClass.Object, HasChildren: true),
                    new BrowseNode("Folder2", "Folder2", BrowseNodeClass.Object, HasChildren: false),
                },
                Truncated: false));
        }

        if (parentNodeId == "Folder1")
        {
            // Page 2: the caller passed back the continuation token from page 1.
            if (continuationToken == "STUB_PAGE2")
            {
                return Task.FromResult(new BrowseChildrenResult(
                    new[] { new BrowseNode("Tag2", "Tag2", BrowseNodeClass.Variable, HasChildren: false) },
                    Truncated: false));
            }

            // Page 1: return only Tag1 and a continuation token for the next page.
            return Task.FromResult(new BrowseChildrenResult(
                new[] { new BrowseNode("Tag1", "Tag1", BrowseNodeClass.Variable, HasChildren: false) },
                Truncated: true,
                ContinuationToken: "STUB_PAGE2"));
        }

        // Leaves (Folder2, Variable nodes, anything unknown) have no children.
        return Task.FromResult(new BrowseChildrenResult(Array.Empty<BrowseNode>(), Truncated: false));
    }

    /// <inheritdoc />
    public Task<AddressSpaceSearchResult> SearchAddressSpaceAsync(
        string query, int maxDepth, int maxResults, CancellationToken cancellationToken = default)
        // Search the canned tree by walking the stub's OWN BrowseChildrenAsync via
        // the shared BFS — identical algorithm to RealOpcUaClient. Because the
        // helper pages Folder1's continuation, a "Tag" query finds both Tag1 (page 1)
        // and Tag2 (page 2).
        => AddressSpaceSearch.SearchAsync(
            BrowseChildrenAsync, query, maxDepth, maxResults, cancellationToken);

    /// <summary>Disposes this stub client and marks the connection as closed.</summary>
    /// <returns>A completed <see cref="ValueTask"/>.</returns>
    public ValueTask DisposeAsync()
    {
        IsConnected = false;
        return ValueTask.CompletedTask;
    }
}
