using Google.Protobuf;
using Org.Eclipse.Tahu.Protobuf;
using TahuDataType = Org.Eclipse.Tahu.Protobuf.DataType;

namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Sparkplug;

/// <summary>
///     The one seam through which a Sparkplug NCMD is published — <see cref="MqttConnection"/> is the
///     production implementation once it grows a publish leg (Task 21); tests substitute a fake so the
///     encode/topic/QoS/retain contract is exercisable without a broker. Deliberately narrower than
///     "publish anything": a Sparkplug driver's only outbound traffic is NCMD/DCMD, so this is not a
///     general MQTT client abstraction in waiting.
/// </summary>
public interface IMqttPublishTransport
{
    /// <summary>
    ///     Publishes one application message. Implementations must be bounded — a broker that accepts
    ///     PUBLISH and never completes it (QoS &gt; 0 awaiting PUBACK) must fail at a deadline, not
    ///     hang; <see cref="RebirthRequester.RequestAsync"/> supplies one, but an implementation used
    ///     directly by a future caller must not rely on that.
    /// </summary>
    /// <param name="topic">The concrete topic to publish on.</param>
    /// <param name="payload">The message body.</param>
    /// <param name="qos">Requested QoS, 0–2.</param>
    /// <param name="retain">The MQTT <c>retain</c> flag.</param>
    /// <param name="cancellationToken">Caller cancellation.</param>
    Task PublishAsync(string topic, byte[] payload, int qos, bool retain, CancellationToken cancellationToken);
}

/// <summary>
///     Encodes and publishes a Sparkplug B rebirth request — an NCMD carrying the well-known
///     <c>Node Control/Rebirth</c> boolean metric, addressed to one edge node's command topic. See
///     Sparkplug B v3.0 spec §6/§7 and design doc §3.6.
/// </summary>
/// <remarks>
///     <para>
///         <b><see cref="Build"/> is pure.</b> It does no I/O and touches no transport, so it is
///         trivially unit-testable and safe to call from anywhere (the ingest state machine on a
///         detected gap, the browser on an operator click) without pulling in a live connection. The
///         bounded, transport-facing publish is the separate <see cref="RequestAsync"/> overload.
///     </para>
///     <para>
///         <b>No <c>seq</c>.</b> Sparkplug's sequence-number stream (§6.4.1) belongs to the messages an
///         edge node itself publishes (NBIRTH/NDATA/DBIRTH/DDATA/NDEATH/DDEATH) — it is how a host
///         detects a gap in <i>that node's</i> outbound stream. An NCMD flows the other direction, host
///         → node, and is not a member of the node's sequence at all: stamping one on here would not be
///         "wrong per spec" so much as meaningless, and a strict edge-node implementation that validated
///         it against its own counter would have grounds to reject the command outright.
///     </para>
///     <para>
///         <b>QoS 0, retain <see langword="false"/> — always.</b> NCMD is ordinary Sparkplug command
///         traffic, published at the same QoS 0 the spec uses for NDATA/DDATA/NBIRTH/DBIRTH (only
///         NDEATH — the broker-issued Will — and STATE use QoS 1). <see langword="retain"/> is the one
///         that matters operationally: a retained NCMD would be replayed by the broker to the edge node
///         on <i>every</i> future subscribe — including the node's own reconnect — driving it into a
///         permanent rebirth loop from a single stale command. See the carry-forward landmine on
///         <c>MqttDriverBrowser</c>/NDEATH-as-Will for the sibling failure shape this type must not
///         introduce on the publish side.
///     </para>
///     <para>
///         <b><c>Datatype</c> and both timestamps are set.</b> <c>Node Control/Rebirth</c> is Boolean;
///         setting <see cref="Payload.Types.Metric.Datatype"/> explicitly (rather than leaving it for the
///         node to infer) and stamping both the metric and payload <c>timestamp</c> matches what a
///         well-behaved command payload carries per spec §6.4.13 and what the Task-25 simulator (and any
///         real Cirrus-Link-derived edge node) expects to validate against. An edge node that treats a
///         missing/ambiguous field as malformed and silently drops the command would otherwise leave the
///         driver retrying forever with no visible error — the whole point of a rebirth request is to
///         recover from exactly that kind of silent desync, so the request itself must not risk being
///         the next cause of one.
///     </para>
/// </remarks>
public static class RebirthRequester
{
    /// <summary>The well-known Sparkplug B node-control metric name that triggers a rebirth.</summary>
    public const string RebirthMetricName = "Node Control/Rebirth";

    /// <summary>
    ///     Default bounded deadline for <see cref="RequestAsync"/> when the caller does not supply one.
    ///     Deliberately this type's own value rather than <c>MqttDriverOptions.ConnectTimeoutSeconds</c>
    ///     — that knob already governs every MQTTnet connect/subscribe operation, and repurposing it
    ///     would couple the rebirth-publish budget to the connect budget in a way neither side could
    ///     tune independently (the same reasoning <c>MqttConnection.SubscribeAsync</c> documents for its
    ///     own deadline).
    /// </summary>
    public static readonly TimeSpan DefaultRequestTimeout = TimeSpan.FromSeconds(5);

    /// <summary>
    ///     Builds the wire bytes and target topic for a rebirth-request NCMD, without publishing it.
    /// </summary>
    /// <param name="groupId">The Sparkplug group id.</param>
    /// <param name="edgeNodeId">The Sparkplug edge-node id to address the command to.</param>
    /// <returns>The NCMD topic (<c>spBv1.0/{groupId}/NCMD/{edgeNodeId}</c>) and its encoded payload.</returns>
    /// <exception cref="ArgumentException"><paramref name="groupId"/>/<paramref name="edgeNodeId"/> is null or empty.</exception>
    public static (string Topic, byte[] Bytes) Build(string groupId, string edgeNodeId)
    {
        // Validates both ids and does the topic assembly — see the "don't hand-concatenate" remark on
        // SparkplugTopic.Format itself.
        var topic = SparkplugTopic.Format(groupId, SparkplugMessageType.NCMD, edgeNodeId);

        var timestampMs = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

        var payload = new Payload { Timestamp = timestampMs };
        payload.Metrics.Add(new Payload.Types.Metric
        {
            Name = RebirthMetricName,
            Datatype = (uint)TahuDataType.Boolean,
            Timestamp = timestampMs,
            BooleanValue = true,
        });

        return (topic, payload.ToByteArray());
    }

    /// <summary>
    ///     Builds and publishes a rebirth-request NCMD under a bounded deadline. QoS 0, retain
    ///     <see langword="false"/> — see the type remarks for why both are fixed rather than
    ///     configurable.
    /// </summary>
    /// <param name="transport">The publish seam — the live connection, or a test fake.</param>
    /// <param name="groupId">The Sparkplug group id.</param>
    /// <param name="edgeNodeId">The Sparkplug edge-node id to address the command to.</param>
    /// <param name="cancellationToken">Caller cancellation; linked with the publish deadline.</param>
    /// <param name="timeout">
    ///     The publish deadline. Defaults to <see cref="DefaultRequestTimeout"/> when omitted or
    ///     non-positive.
    /// </param>
    /// <exception cref="ArgumentException"><paramref name="groupId"/>/<paramref name="edgeNodeId"/> is null or empty.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="transport"/> is null.</exception>
    public static async Task RequestAsync(
        IMqttPublishTransport transport,
        string groupId,
        string edgeNodeId,
        CancellationToken cancellationToken,
        TimeSpan? timeout = null)
    {
        ArgumentNullException.ThrowIfNull(transport);

        var (topic, bytes) = Build(groupId, edgeNodeId);

        var deadlineSpan = timeout is { } t && t > TimeSpan.Zero ? t : DefaultRequestTimeout;
        using var deadline = new CancellationTokenSource(deadlineSpan);
        using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, deadline.Token);

        await transport.PublishAsync(topic, bytes, qos: 0, retain: false, linked.Token).ConfigureAwait(false);
    }
}
