using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;

namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;

/// <summary>
///     Central-side per-node dialer for the Phase 5 telemetry stream. Owns one h2c
///     <see cref="GrpcChannel"/> to a single driver node's <c>TelemetryStreamService</c>, opens the
///     server-streaming <c>Subscribe</c> call, and pumps every <see cref="TelemetryEvent"/> to a
///     caller-supplied sink. The raw proto envelope is delivered as-is — the caller maps it via
///     <see cref="TelemetryProtoMapCentral"/>.
/// </summary>
/// <remarks>
///     <para>
///         <b>Deliberately dumb.</b> Connect, pump, surface transport errors — no reconnect, no
///         backoff, no buffering. A normal shutdown (the caller's <see cref="CancellationToken"/>
///         firing, or the server ending the stream with <see cref="StatusCode.Cancelled"/>) completes
///         <see cref="RunAsync"/> without calling <c>onError</c>.
///     </para>
///     <para>
///         <b>onError is strictly the transport-failure signal.</b> Only a failure from the wire itself
///         — an exception thrown out of <c>call.ResponseStream</c> — reaches <c>onError</c>, and exactly
///         once, so the supervisor's reconnect loop reacts only to a genuinely unreachable/faulted node.
///         Everything that is NOT a transport fault is kept off that path:
///         <list type="bullet">
///             <item>
///                 <b>Programming faults</b> (null/empty <c>correlationId</c>, use-after-dispose) are
///                 validated <em>before</em> the pump and throw synchronously out of
///                 <see cref="RunAsync"/> — a caller bug must surface as a caller bug, never as "node
///                 down".
///             </item>
///             <item>
///                 <b>Consumer-callback faults</b> (a poison/unmappable event throwing inside
///                 <c>onEvent</c> — e.g. a future oneof case the mapper rejects, or a malformed required
///                 Timestamp) are caught <em>per event</em>, logged at Warning, and the pump
///                 <em>continues</em>. A code/version defect on one event must not tear down the stream
///                 or trigger a reconnect.
///             </item>
///         </list>
///     </para>
///     <para>
///         <b>h2c + Bearer.</b> The endpoint is a prior-knowledge <c>http://host:port</c> address (no
///         TLS); the shared node bearer key rides the <c>authorization</c> header on every call. The
///         channel sets HTTP/2 keepalive pings so a long-idle telemetry stream is not silently dropped
///         by an intermediary.
///     </para>
/// </remarks>
public sealed class TelemetryStreamClient : IDisposable
{
    private readonly string _apiKey;
    private readonly GrpcChannel? _ownedChannel;
    private readonly TelemetryStreamService.TelemetryStreamServiceClient _client;
    private readonly ILogger<TelemetryStreamClient> _log;
    private bool _disposed;

    /// <summary>
    ///     Initializes a new instance of the <see cref="TelemetryStreamClient"/> class dialing a live
    ///     driver node over h2c.
    /// </summary>
    /// <param name="endpoint">Prior-knowledge <c>http://host:port</c> address of the node's telemetry server.</param>
    /// <param name="apiKey">Shared node bearer key sent as <c>authorization: Bearer &lt;key&gt;</c>.</param>
    /// <param name="log">Logger for dropped-malformed-event diagnostics; defaults to a no-op logger.</param>
    public TelemetryStreamClient(string endpoint, string apiKey, ILogger<TelemetryStreamClient>? log = null)
    {
        ArgumentException.ThrowIfNullOrEmpty(endpoint);
        ArgumentNullException.ThrowIfNull(apiKey);

        _apiKey = apiKey;
        _log = log ?? NullLogger<TelemetryStreamClient>.Instance;
        _ownedChannel = GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions
        {
            HttpHandler = new SocketsHttpHandler
            {
                KeepAlivePingDelay = TimeSpan.FromSeconds(15),
                KeepAlivePingTimeout = TimeSpan.FromSeconds(10),
                KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
                EnableMultipleHttp2Connections = true,
            },
        });
        _client = new TelemetryStreamService.TelemetryStreamServiceClient(_ownedChannel);
    }

    /// <summary>
    ///     Initializes a new instance of the <see cref="TelemetryStreamClient"/> class over a
    ///     caller-supplied client — the test seam. No channel is owned or disposed.
    /// </summary>
    /// <param name="client">The generated client to drive (typically a fake in tests).</param>
    /// <param name="apiKey">Shared node bearer key sent as <c>authorization: Bearer &lt;key&gt;</c>.</param>
    /// <param name="log">Logger for dropped-malformed-event diagnostics; defaults to a no-op logger.</param>
    public TelemetryStreamClient(
        TelemetryStreamService.TelemetryStreamServiceClient client,
        string apiKey,
        ILogger<TelemetryStreamClient>? log = null)
    {
        ArgumentNullException.ThrowIfNull(client);
        ArgumentNullException.ThrowIfNull(apiKey);

        _client = client;
        _apiKey = apiKey;
        _log = log ?? NullLogger<TelemetryStreamClient>.Instance;
        _ownedChannel = null;
    }

    /// <summary>
    ///     Opens the telemetry stream and pumps every envelope to <paramref name="onEvent"/> until the
    ///     server ends the stream or <paramref name="ct"/> fires. A normal shutdown returns without
    ///     invoking <paramref name="onError"/>; only a transport failure invokes
    ///     <paramref name="onError"/> (exactly once). A consumer-callback exception on one event is
    ///     logged and the pump continues; programming faults throw synchronously (see the type remarks).
    /// </summary>
    /// <param name="correlationId">Stream correlation id echoed on every envelope.</param>
    /// <param name="onEvent">Sink for each received raw <see cref="TelemetryEvent"/>.</param>
    /// <param name="onError">Invoked once on an abnormal transport failure (the reconnect trigger).</param>
    /// <param name="ct">Cancels the stream (normal shutdown).</param>
    /// <exception cref="ArgumentException"><paramref name="correlationId"/> is null or empty.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="onEvent"/> or <paramref name="onError"/> is null.</exception>
    /// <exception cref="ObjectDisposedException">This client has been disposed.</exception>
    public async Task RunAsync(
        string correlationId,
        Action<TelemetryEvent> onEvent,
        Action<Exception> onError,
        CancellationToken ct)
    {
        // Programming faults are validated BEFORE the pump so they throw synchronously out of RunAsync
        // rather than funnelling to onError and masquerading as a transport fault (supervisor spin).
        ArgumentException.ThrowIfNullOrEmpty(correlationId);
        ArgumentNullException.ThrowIfNull(onEvent);
        ArgumentNullException.ThrowIfNull(onError);
        ObjectDisposedException.ThrowIf(_disposed, this);

        var headers = new Metadata { { "authorization", $"Bearer {_apiKey}" } };

        try
        {
            using var call = _client.Subscribe(
                new TelemetryStreamRequest { CorrelationId = correlationId }, headers, cancellationToken: ct);

            await foreach (var evt in call.ResponseStream.ReadAllAsync(ct))
            {
                // Isolate the consumer callback: a poison/unmappable event is a code/version fault, not
                // "node down". Log and continue — never break the stream, never call onError.
                try
                {
                    onEvent(evt);
                }
                catch (Exception ex)
                {
                    _log.LogWarning(
                        ex,
                        "Dropping a malformed/unhandled telemetry event on stream {CorrelationId}; " +
                        "the consumer callback threw. Continuing the stream (not a transport fault).",
                        correlationId);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // Normal shutdown: the caller's token fired.
        }
        catch (RpcException rex) when (rex.StatusCode == StatusCode.Cancelled)
        {
            // Normal shutdown: the server (or a token-driven cancel) ended the stream.
        }
        catch (Exception ex)
        {
            // The transport-failure signal — a later supervisor task decides retry/backoff.
            onError(ex);
        }
    }

    /// <inheritdoc />
    public void Dispose()
    {
        _disposed = true;
        _ownedChannel?.Dispose();
    }
}
