using Microsoft.Extensions.Logging;
using ZB.MOM.WW.HistorianGateway.Client;
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
///
/// Concrete backed by the published
/// package client. Each seam method forwards directly to the
/// matching client wrapper — both sides speak the same generated historian_gateway.v1 proto
/// types, so no shape translation happens here. The package client's typed exception hierarchy
/// (HistorianGatewayUnavailableException et al.) is allowed to surface unchanged; the
/// records it as a health failure and the node manager
/// turns it into a Bad HistoryRead result.
///
///
///
/// Lazy channel. calls ,
/// which constructs a GrpcChannel over a SocketsHttpHandler without opening a
/// connection — the first RPC dials. Constructing the adapter therefore performs no network I/O,
/// which the offline seam tests rely on (they build from bogus endpoints and must not connect).
///
///
public sealed class HistorianGatewayClientAdapter : IHistorianGatewayClient, IDisposable
{
private readonly HistorianGatewayClient _inner;
private HistorianGatewayClientAdapter(HistorianGatewayClient inner) => _inner = inner;
///
/// Builds an adapter over a freshly created package client mapped from the bound
/// . No connection is opened (lazy channel).
///
/// The bound ServerHistorian configuration (endpoint, key, TLS posture).
/// Logger factory threaded into the package client's channel diagnostics.
/// A ready-to-use adapter whose underlying channel has not yet dialed the gateway.
///
/// is empty or not an absolute http(s) URI.
/// Defense in depth (archreview 06/S-11): the Host's ServerHistorianOptionsValidator fails
/// these configs at startup, but any caller that bypasses ValidateOnStart (tests, the live
/// fixture, future non-Host wiring) still gets a named, config-key-carrying error here instead of
/// the raw the bare new Uri(...) ctor throws.
///
public static HistorianGatewayClientAdapter Create(ServerHistorianOptions options, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(loggerFactory);
// Fail fast with a named, actionable error rather than letting `new Uri("")` throw a raw
// UriFormatException deep in the factory (archreview 06/S-11). The Endpoint is not a secret,
// so it is safe to echo; the ApiKey is never surfaced.
if (!Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var endpointUri)
|| (endpointUri.Scheme != Uri.UriSchemeHttp && endpointUri.Scheme != Uri.UriSchemeHttps))
{
throw new InvalidOperationException(
$"ServerHistorian:Endpoint must be an absolute http(s) URI (e.g. https://host:5222); got '{options.Endpoint}'.");
}
var clientOptions = new HistorianGatewayClientOptions
{
Endpoint = endpointUri,
ApiKey = options.ApiKey,
UseTls = options.UseTls,
CaCertificatePath = options.CaCertificatePath,
// INVERTED mapping: ServerHistorianOptions.AllowUntrustedServerCertificate (opt-in to accept
// a self-signed cert) is the negation of the client's RequireCertificateValidation. Allowing
// an untrusted cert == not requiring validation; a pinned CaCertificatePath always verifies.
RequireCertificateValidation = !options.AllowUntrustedServerCertificate,
DefaultCallTimeout = options.CallTimeout,
LoggerFactory = loggerFactory,
};
return new HistorianGatewayClientAdapter(HistorianGatewayClient.Create(clientOptions));
}
///
public IAsyncEnumerable ReadRawAsync(
string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken ct) =>
_inner.ReadRawAsync(tag, startUtc, endUtc, maxValues, ct);
///
public IAsyncEnumerable ReadAggregateAsync(
string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken ct) =>
_inner.ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, ct);
///
public Task> ReadAtTimeAsync(
string tag, IReadOnlyList timestampsUtc, CancellationToken ct) =>
_inner.ReadAtTimeAsync(tag, timestampsUtc, ct);
///
public IAsyncEnumerable ReadEventsAsync(
string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken ct)
{
HistorianEventFilter? filter = sourceName is null
? null
: new HistorianEventFilter
{
PropertyName = "Source_Object",
Comparison = HistorianEventComparison.Equal,
Value = sourceName,
};
return _inner.ReadEventsAsync(startUtc, endUtc, filter, ct);
}
///
public Task WriteLiveValuesAsync(
string tag, IReadOnlyList values, CancellationToken ct) =>
_inner.WriteLiveValuesAsync(tag, values, ct);
///
public Task SendEventAsync(HistorianEvent evt, CancellationToken ct) =>
_inner.SendEventAsync(evt, ct);
///
public Task EnsureTagsAsync(
IReadOnlyList definitions, CancellationToken ct) =>
_inner.EnsureTagsAsync(definitions, ct);
///
public Task ProbeAsync(CancellationToken ct) => _inner.ProbeAsync(ct);
///
public Task GetConnectionStatusAsync(CancellationToken ct) =>
_inner.GetConnectionStatusAsync(ct);
/// Disposes the underlying package client (and its channel). Prefer .
public void Dispose() => _inner.Dispose();
/// Asynchronously disposes the underlying package client (and its channel).
/// A task that completes when the client has been disposed.
public ValueTask DisposeAsync() => _inner.DisposeAsync();
}