using System.Diagnostics.CodeAnalysis;
using Grpc.Core;
using Grpc.Net.Client;
using Mishima.Nexus.Wire.V1;
using Robotico.Graph;
using Robotico.Nexus.Client.Document;
using Robotico.Nexus.Client.Graph;
using Robotico.Nexus.Client.Relational;
using Robotico.Nexus.Client.Time;
using Robotico.Result.Errors;
using Robotico.Time;
namespace Robotico.Nexus.Client;
///
/// Connects to a Nexus wire endpoint and exposes plane facades: /
/// implement Robotico.Graph / Robotico.Time ports over gRPC, and
/// / return thin document/relational
/// façades (MVP — no full IMishimaDatabase façade; use /
/// for the full generated Wire.V1 RPC surface).
/// One connection is bound to a single scheme + logical database from the connection URI; open a
/// second to talk to another plane or database.
///
public sealed class NexusClient : IAsyncDisposable, IDisposable
{
private readonly GrpcChannel _channel;
private readonly string? _bearerToken;
private bool _disposed;
private NexusClient(GrpcChannel channel, NexusConnectionUriParts parsed, string boundDatabase, string? bearerToken)
{
_channel = channel;
_bearerToken = bearerToken;
Scheme = parsed.Scheme;
Database = boundDatabase;
Graph = new NexusGraphStore(channel, bearerToken, boundDatabase);
Time = new NexusTimeStore(channel, bearerToken, boundDatabase);
}
/// Connection scheme () resolved at connect time.
public string Scheme { get; }
/// Logical database bound at connect time (URI path segment, or the scheme's canonical default).
public string Database { get; }
///
/// Graph plane store bound to when is
/// ; usable against any logical database name via
/// regardless of scheme.
///
public IGraphStore Graph { get; }
///
/// Time plane store bound to when is
/// ; usable against any logical database name via
/// regardless of scheme.
///
public ITimeStore Time { get; }
/// Connects to a Nexus wire endpoint. Validates the URI and opens a gRPC channel eagerly.
[SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Nexus connection strings use custom nexus+* schemes and are the documented public API surface (mirrors ADO.NET / MongoDB connection-string ergonomics); a System.Uri overload adds no value here.")]
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Ownership of the returned NexusClient (and its channel) transfers to the caller via Result.Success; the caller is responsible for disposing it.")]
public static Robotico.Result.Result Connect(string connectionUri, NexusClientOptions? options = null)
{
Robotico.Result.Result parsedResult = NexusConnectionUri.TryParse(connectionUri);
if (!parsedResult.IsSuccess(out NexusConnectionUriParts? parsed, out IError? parseError))
{
return Robotico.Result.Result.Error(parseError!);
}
options ??= new NexusClientOptions();
NexusTlsMaterial? tls = options.ResolveTls();
bool useHttps = options.ResolveUseHttps(tls);
Robotico.Result.Result channelResult = NexusGrpcChannelFactory.CreateChannel(parsed!, tls, useHttps);
if (!channelResult.IsSuccess(out GrpcChannel? channel, out IError? channelError))
{
return Robotico.Result.Result.Error(channelError!);
}
string database = string.IsNullOrWhiteSpace(parsed!.Database)
? NexusConnectionUri.DefaultDatabase(parsed.Scheme)
: parsed.Database!;
return Robotico.Result.Result.Success(new NexusClient(channel!, parsed, database, options.ResolveBearerToken()));
}
/// Calls NexusHealthService.Check and succeeds only when the server reports ready.
public Robotico.Result.Result CheckHealth()
{
Robotico.Result.Result disposed = EnsureNotDisposed();
if (!disposed.IsSuccess())
{
return disposed;
}
NexusHealthService.NexusHealthServiceClient client = new(_channel);
try
{
HealthCheckResponse response = client.Check(new HealthCheckRequest(), NexusCallMetadata.Build(Database, _bearerToken));
return response.Ready
? Robotico.Result.Result.Success()
: Robotico.Result.Result.Error(NexusErrors.Error("Nexus health check reports not ready.", NexusErrors.Unavailable));
}
catch (RpcException ex)
{
return Robotico.Result.Result.Error(NexusWireStatus.FromException(ex));
}
}
/// Creates a thin document-plane façade (Ping + single-document upsert/get/delete) bound to this channel.
public Robotico.Result.Result CreateDocumentClient()
{
Robotico.Result.Result disposed = EnsureNotDisposed();
return disposed.IsSuccess()
? Robotico.Result.Result.Success(new NexusDocumentClient(_channel, Database, _bearerToken))
: Robotico.Result.Result.Error(AsError(disposed));
}
/// Creates a thin relational-plane façade (Ping only) bound to this channel.
public Robotico.Result.Result CreateRelationalClient()
{
Robotico.Result.Result disposed = EnsureNotDisposed();
return disposed.IsSuccess()
? Robotico.Result.Result.Success(new NexusRelationalClient(_channel, Database, _bearerToken))
: Robotico.Result.Result.Error(AsError(disposed));
}
/// Creates the generated document-plane Wire.V1 client bound to this channel (full RPC surface).
public Robotico.Result.Result CreateDocumentWireClient()
{
Robotico.Result.Result disposed = EnsureNotDisposed();
return disposed.IsSuccess()
? Robotico.Result.Result.Success(new NexusDocumentEngineService.NexusDocumentEngineServiceClient(_channel))
: Robotico.Result.Result.Error(AsError(disposed));
}
/// Creates the generated relational-plane Wire.V1 client bound to this channel (full RPC surface).
public Robotico.Result.Result CreateRelationalWireClient()
{
Robotico.Result.Result disposed = EnsureNotDisposed();
return disposed.IsSuccess()
? Robotico.Result.Result.Success(new NexusRelationalEngineService.NexusRelationalEngineServiceClient(_channel))
: Robotico.Result.Result.Error(AsError(disposed));
}
/// Metadata headers a caller can attach to a manual RPC (document/relational clients) for this database.
public Metadata BuildCallMetadata(string? databaseNameOverride = null) =>
NexusCallMetadata.Build(string.IsNullOrWhiteSpace(databaseNameOverride) ? Database : databaseNameOverride, _bearerToken);
///
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_channel.Dispose();
}
///
public ValueTask DisposeAsync()
{
if (_disposed)
{
return ValueTask.CompletedTask;
}
_disposed = true;
_channel.Dispose();
return ValueTask.CompletedTask;
}
private Robotico.Result.Result EnsureNotDisposed() =>
_disposed
? Robotico.Result.Result.Error(NexusErrors.Error("NexusClient is disposed.", NexusErrors.Disposed))
: Robotico.Result.Result.Success();
private static IError AsError(Robotico.Result.Result failed)
{
failed.IsError(out IError? error);
return error ?? NexusErrors.Error("NexusClient operation failed.");
}
}