using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;

/// <summary>
///     Adapts the gateway client's <c>WriteLiveValues</c> RPC to the Runtime recorder's
///     <see cref="IHistorianValueWriter"/> seam. Maps each <see cref="HistorizationValue"/> onto a
///     proto <see cref="HistorianLiveValue"/> (numeric value + quality, with an optional timestamp —
///     a null timestamp leaves the proto field unset so the gateway's SQL writer server-stamps the
///     current time) and folds the returned <see cref="WriteAck"/> to a single retry/ack boolean.
/// </summary>
/// <remarks>
///     <para>
///         <b>Non-throwing by contract.</b> The recorder's drain loop stays simple by treating the
///         writer as never throwing: any gateway/transport error (and a non-success, non-queued ack)
///         is mapped to <c>false</c> so the recorder retains the outbox entries and retries. Only the
///         failure category (the exception type name) is logged — never tag values, hostnames, or
///         credentials.
///     </para>
///     <para>
///         A success ack OR a store-forward-queued ack maps to <c>true</c>: a value the gateway
///         durably queued must not be re-drained.
///     </para>
///     <para>
///         <b>Quality is a documented-dead input</b> on the <c>WriteLiveValues</c> SQL path
///         (HistorianGateway 0.2.0 Contracts C-002): <c>HistorianLiveValue.Quality</c> /
///         <c>QualityDetail</c> are server-managed columns — any value sent is accepted on the wire and
///         then silently discarded. We populate <c>Quality</c> anyway (see the mapping below): proto3
///         transmits <c>0</c> when a field is left unset, and <c>0</c> is "Bad" in OPC-DA terms, so
///         <em>not</em> setting it would send a <em>wrong</em> quality to any future gateway version that
///         starts honoring the field — whereas the recorder's <c>192</c> ("Good") is the correct value.
///         <b>Do NOT extend the recorder to capture real node quality expecting it to persist</b> — it
///         will not, until the gateway honors the field.
///     </para>
/// </remarks>
public sealed class GatewayHistorianValueWriter : IHistorianValueWriter, IAsyncDisposable
{
    private readonly IHistorianGatewayClient _client;
    private readonly ILogger<GatewayHistorianValueWriter> _logger;

    /// <summary>Creates the writer over a gateway client seam.</summary>
    /// <param name="client">The gateway client used for the <c>WriteLiveValues</c> write path.</param>
    /// <param name="logger">Logger for failure-category diagnostics (never logs value content).</param>
    public GatewayHistorianValueWriter(IHistorianGatewayClient client, ILogger<GatewayHistorianValueWriter> logger)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <inheritdoc />
    public async Task<bool> WriteLiveValuesAsync(
        string tag, IReadOnlyList<HistorizationValue> values, CancellationToken ct)
    {
        ArgumentException.ThrowIfNullOrEmpty(tag);
        ArgumentNullException.ThrowIfNull(values);

        if (values.Count == 0)
        {
            // Nothing to write is a trivially-successful ack — the recorder treats it as drained.
            return true;
        }

        try
        {
            var liveValues = new List<HistorianLiveValue>(values.Count);
            foreach (HistorizationValue value in values)
            {
                var live = new HistorianLiveValue
                {
                    NumericValue = value.Value,
                    // Documented-dead input on the WriteLiveValues SQL path (0.2.0 Contracts C-002 —
                    // server-managed, silently discarded). Populated deliberately; see the class <remarks>.
                    Quality = value.Quality,
                };

                if (value.TimestampUtc is { } timestampUtc)
                {
                    // Timestamp.FromDateTime requires Utc kind; coerce defensively. A null timestamp
                    // leaves the proto field unset -> the gateway's SQL writer server-stamps now.
                    live.Timestamp = Timestamp.FromDateTime(DateTime.SpecifyKind(timestampUtc, DateTimeKind.Utc));
                }

                liveValues.Add(live);
            }

            WriteAck ack = await _client.WriteLiveValuesAsync(tag, liveValues, ct).ConfigureAwait(false);
            return ack.Success || ack.Queued;
        }
        catch (Exception exception)
        {
            // NEVER throw out of the writer — the recorder's drain expects a bool so its retain/retry
            // logic stays simple. Log only the failure category (no value content, hostnames, or creds).
            _logger.LogDebug(
                "WriteLiveValues failed ({Exception}); recorder will retain and retry.",
                exception.GetType().Name);
            return false;
        }
    }

    /// <summary>
    ///     Disposes the underlying gateway client (and its gRPC channel). The DI container owns this
    ///     writer as a singleton, so this fires once at host shutdown — closing the channel gracefully.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public ValueTask DisposeAsync() => _client.DisposeAsync();
}
