using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Globalization;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MQTTnet;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     The plain-MQTT ingest path: holds the driver's authored <c>RawPath → </c>
///     <see cref="MqttTagDefinition"/> table, establishes one broker subscription per distinct authored
///     topic, and turns each inbound message into <see cref="OnDataChange"/> notifications plus a
///     <see cref="LastValueCache"/> update.
/// </summary>
/// <remarks>
///     <para>
///         <b>The published reference is the RawPath.</b> Every <see cref="DataChangeEventArgs"/> this
///         type raises carries <see cref="MqttTagDefinition.Name"/> — the tag's RawPath — as its
///         <see cref="DataChangeEventArgs.FullReference"/>, never the topic and never the authored
///         <c>TagConfig</c> blob. <c>DriverHostActor</c>'s dual-namespace fan-out is keyed by RawPath;
///         publishing under any other key produces a driver that passes every unit test and delivers
///         nothing at all in production.
///     </para>
///     <para>
///         <b>A message fans out to EVERY matching tag.</b> Several tags routinely share one topic
///         (one JSON document, one JSONPath per signal), so the topic index maps a topic to a
///         <i>list</i> of definitions and delivery walks all of them. Stopping at the first match is
///         the silent-starvation bug this shape exists to prevent, and it is pinned by a test.
///     </para>
///     <para>
///         <b>Nothing here may throw on MQTTnet's dispatcher thread.</b>
///         <see cref="HandleMessage"/> runs on it: extraction failures degrade the offending
///         <i>tag</i> to a Bad status code, a throwing downstream <see cref="OnDataChange"/> subscriber
///         is caught (and the tags behind it in the fan-out are still delivered), and no path does I/O
///         or takes a lock.
///     </para>
///     <para>
///         <b>Unauthored topics are ignored.</b> A wildcard-authored tag, or simply a chatty broker,
///         delivers messages this driver never asked for. They are dropped: MQTT ingest never
///         auto-provisions a tag the operator did not author.
///     </para>
///     <para>
///         <b>Wildcards are matched for real.</b> An authored topic carrying <c>+</c> / <c>#</c> is
///         accepted by the parser (warned at deploy, see <c>MqttTagDefinitionFactory.Inspect</c>), so
///         matching uses MQTTnet's own <see cref="MqttTopicFilterComparer"/> rather than string
///         equality — the same comparer a broker applies. Concrete topics stay a single dictionary
///         lookup; the linear wildcard scan runs only when a wildcard tag actually exists.
///     </para>
///     <para>
///         <b>Sparkplug seam.</b> P1 ingests plain topics only. Sparkplug B (Task 21) decodes a
///         protobuf payload and resolves metrics by the stable
///         <c>(group, node, device, metricName)</c> tuple rather than by topic; it belongs in its own
///         handler alongside this one, feeding the same <see cref="OnDataChange"/> +
///         <see cref="Values"/> sinks. Nothing Sparkplug-shaped is built here.
///     </para>
/// </remarks>
public sealed class MqttSubscriptionManager
{
    /// <summary>
    ///     Default <see cref="MaxPayloadBytes"/> — 1 MiB. Generous for MQTT telemetry (which is
    ///     kilobytes) while still bounding the dispatcher-thread work any one publisher can impose.
    /// </summary>
    public const int DefaultMaxPayloadBytes = 1024 * 1024;

    // OPC UA status codes (values verified against Opc.Ua.StatusCodes, not recalled).
    private const uint StatusGood = 0x00000000u;

    /// <summary>The payload could not be decoded, or the JSONPath selected nothing.</summary>
    private const uint StatusBadDecodingError = 0x80070000u;

    /// <summary>The payload decoded but does not fit the tag's declared <c>dataType</c>.</summary>
    private const uint StatusBadTypeMismatch = 0x80740000u;

    /// <summary>The subscribed reference is not an authored MQTT tag.</summary>
    private const uint StatusBadNodeIdUnknown = 0x80340000u;

    /// <summary>The broker refused, or never answered, the SUBSCRIBE for this reference's topic.</summary>
    private const uint StatusBadCommunicationError = 0x80050000u;

    private readonly int _defaultQos;
    private readonly string _driverId;
    private readonly ILogger? _logger;
    private readonly MqttMode _mode;

    /// <summary>RawPath → the handle of the subscription currently covering it.</summary>
    private readonly ConcurrentDictionary<string, ISubscriptionHandle> _handleByRawPath = new(StringComparer.Ordinal);

    /// <summary>Handle → the references it was created for, so unsubscribe can undo exactly its own.</summary>
    private readonly ConcurrentDictionary<ISubscriptionHandle, string[]> _refsByHandle = new();

    /// <summary>Topics already SUBSCRIBEd on the live session — the dedupe set and the re-subscribe set.</summary>
    private readonly ConcurrentDictionary<string, byte> _subscribedTopics = new(StringComparer.Ordinal);

    /// <summary>RawPaths whose extraction failure has already been logged loudly once.</summary>
    private readonly ConcurrentDictionary<string, byte> _warnedRefs = new(StringComparer.Ordinal);

    private readonly EquipmentTagRefResolver<MqttTagDefinition> _resolver;

    private long _handleSeq;

    /// <summary>The authored table + its topic index, swapped atomically by <see cref="Register"/>.</summary>
    private volatile AuthoredTable _authored = AuthoredTable.Empty;

    private IMqttSubscribeTransport? _transport;

    /// <summary>Initializes a new instance of the <see cref="MqttSubscriptionManager"/> class.</summary>
    /// <param name="options">Driver options — supplies the ingest mode and the default QoS.</param>
    /// <param name="driverId">Driver instance id, used only for log correlation.</param>
    /// <param name="transport">
    ///     The SUBSCRIBE seam. <see langword="null"/> records subscriptions without dialling a broker
    ///     (the shape tests use, and the shape a not-yet-connected driver is in);
    ///     <see cref="AttachTo"/> supplies the live connection.
    /// </param>
    /// <param name="logger">Optional logger; never receives credentials or payload bodies.</param>
    /// <param name="cache">
    ///     The last-value store backing <c>IReadable</c>. Defaults to a fresh one owned by this
    ///     manager and exposed as <see cref="Values"/>.
    /// </param>
    /// <param name="maxPayloadBytes">
    ///     Ceiling on an inbound message body; see <see cref="MaxPayloadBytes"/>. Non-positive values
    ///     fall back to <see cref="DefaultMaxPayloadBytes"/> — "unbounded" is not offered, because the
    ///     bound exists to protect a shared dispatcher thread.
    /// </param>
    public MqttSubscriptionManager(
        MqttDriverOptions options,
        string driverId,
        IMqttSubscribeTransport? transport = null,
        ILogger? logger = null,
        LastValueCache? cache = null,
        int maxPayloadBytes = DefaultMaxPayloadBytes)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentException.ThrowIfNullOrWhiteSpace(driverId);

        MaxPayloadBytes = maxPayloadBytes > 0 ? maxPayloadBytes : DefaultMaxPayloadBytes;
        _mode = options.Mode;
        _defaultQos = options.Plain?.DefaultQos ?? 1;
        _driverId = driverId;
        _transport = transport;
        _logger = logger;
        Values = cache ?? new LastValueCache();
        _resolver = new EquipmentTagRefResolver<MqttTagDefinition>(
            rawPath => _authored.ByRawPath.TryGetValue(rawPath, out var def) ? def : null);
    }

    /// <inheritdoc cref="ISubscribable.OnDataChange"/>
    public event EventHandler<DataChangeEventArgs>? OnDataChange;

    /// <summary>
    ///     The last observed value per RawPath — the bridge from MQTT's push model to the OPC UA
    ///     server's polled <c>IReadable.ReadAsync</c> batches.
    /// </summary>
    public LastValueCache Values { get; }

    /// <summary>
    ///     Ceiling on an inbound message body. A larger message is refused before any decode or parse
    ///     and degrades its own tags to <c>BadDecodingError</c>.
    /// </summary>
    /// <remarks>
    ///     Decode and parse both run synchronously on MQTTnet's shared dispatcher thread, so their cost
    ///     is paid by <i>every</i> subscription, not just the offending topic. Without a bound, one
    ///     publisher shipping a multi-megabyte body — or a hostile one shipping a huge nested document
    ///     — stalls the whole driver's delivery for as long as the parse takes. Sized for telemetry
    ///     payloads, which are kilobytes; a deployment that legitimately needs more should raise this
    ///     deliberately rather than run unbounded.
    ///     <para>
    ///         <b>Follow-up:</b> this is a constructor knob, not yet an operator-facing one. Promoting
    ///         it to an <c>MqttDriverOptions</c> key (and pairing it with MQTTnet's
    ///         <c>WithMaximumPacketSize</c>, so an oversized body is refused at the protocol layer
    ///         rather than after the client has already buffered it) needs an edit to the
    ///         <c>.Contracts</c> project that this task was scoped out of.
    ///     </para>
    /// </remarks>
    public int MaxPayloadBytes { get; }

    /// <summary>
    ///     Wires this manager onto a live connection: inbound messages route to
    ///     <see cref="HandleMessage"/> and every reconnect re-establishes the subscriptions through
    ///     <see cref="OnReconnectedAsync"/>. The connection also becomes this manager's SUBSCRIBE
    ///     transport.
    /// </summary>
    /// <remarks>
    ///     The manager does not own the connection's lifetime and never detaches — the driver disposes
    ///     both together. Call once per connection, and <b>before</b> the first
    ///     <see cref="SubscribeAsync"/>: a subscribe issued with no transport attached records its
    ///     intent (and warns) but sends nothing to a broker, and would only be established if the
    ///     connection later dropped and recovered.
    /// </remarks>
    /// <param name="connection">The connection to observe.</param>
    public void AttachTo(MqttConnection connection)
    {
        ArgumentNullException.ThrowIfNull(connection);

        _transport = connection;
        connection.MessageReceived += HandleMessage;
        connection.Reconnected += OnReconnectedAsync;
    }

    /// <summary>
    ///     Replaces the authored tag table with the deploy's raw tags, rebuilding the topic index.
    ///     Wholesale, not additive: a redeploy that drops a tag must stop feeding it.
    /// </summary>
    /// <remarks>
    ///     A <see cref="RawTagEntry"/> whose <c>TagConfig</c> does not map is skipped with a warning,
    ///     never thrown — one bad tag must not fail the whole driver's initialise. Such a reference
    ///     then resolves to nothing and surfaces <c>BadNodeIdUnknown</c>, which is the stated contract
    ///     of <c>MqttTagDefinitionFactory.FromTagConfig</c>.
    /// </remarks>
    /// <param name="rawTags">The authored raw tags delivered by the deploy artifact.</param>
    /// <returns>How many entries mapped to a usable definition.</returns>
    public int Register(IEnumerable<RawTagEntry> rawTags)
    {
        ArgumentNullException.ThrowIfNull(rawTags);

        var byRawPath = new Dictionary<string, MqttTagDefinition>(StringComparer.Ordinal);
        foreach (var entry in rawTags)
        {
            if (MqttTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var def))
            {
                byRawPath[entry.RawPath] = def;
            }
            else
            {
                _logger?.LogWarning(
                    "MQTT driver '{DriverId}': tag config did not map to a definition; skipping. RawPath={RawPath}",
                    _driverId,
                    entry.RawPath);
            }
        }

        var table = AuthoredTable.Build(byRawPath);
        _authored = table;
        _warnedRefs.Clear();

        // Drop state belonging to tags this deploy removed. Without this, a topic whose last tag was
        // deleted stays in _subscribedTopics and is dutifully re-SUBSCRIBEd on every future reconnect,
        // forever — and its RawPaths keep a handle mapping that can never fire again.
        foreach (var topic in _subscribedTopics.Keys)
        {
            if (!table.ByFilter.ContainsKey(topic))
            {
                _subscribedTopics.TryRemove(topic, out _);
            }
        }

        foreach (var rawPath in _handleByRawPath.Keys)
        {
            if (!table.ByRawPath.ContainsKey(rawPath))
            {
                _handleByRawPath.TryRemove(rawPath, out _);
            }
        }

        return byRawPath.Count;
    }

    /// <summary>Resolves a RawPath to its authored definition through the shared v3 resolver.</summary>
    /// <param name="rawPath">The driver wire reference.</param>
    /// <param name="def">The definition, when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the reference is an authored MQTT tag.</returns>
    public bool TryResolve(string rawPath, out MqttTagDefinition def) => _resolver.TryResolve(rawPath, out def!);

    /// <summary>
    ///     Subscribes a batch of RawPath references: records them against a new handle and issues one
    ///     SUBSCRIBE for every distinct authored topic not already established.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b>This is the first subscribe.</b> <see cref="MqttConnection.Reconnected"/> deliberately
    ///         does not fire after the initial <c>ConnectAsync</c>, so the manager must issue the
    ///         opening SUBSCRIBE itself; <see cref="OnReconnectedAsync"/> only re-establishes it later.
    ///     </para>
    ///     <para>
    ///         <b>Never throws, never hangs.</b> A reference that is not an authored tag is recorded as
    ///         <c>BadNodeIdUnknown</c>; a SUBACK rejection, transport failure or subscribe deadline
    ///         degrades exactly the affected references to <c>BadCommunicationError</c>. Either way a
    ///         handle is returned — the OPC UA server's subscribe call must not fail because one topic
    ///         is ACL-denied. Recovery for a transient failure comes from the reconnect path;
    ///         a persistent broker-side refusal is visible as a Bad status on the affected nodes.
    ///     </para>
    /// </remarks>
    /// <param name="fullReferences">The RawPath references to subscribe.</param>
    /// <param name="publishingInterval">
    ///     Ignored. MQTT is push-based — the broker's publish rate is the notification rate, and this
    ///     driver neither polls nor throttles.
    /// </param>
    /// <param name="cancellationToken">Cancellation for the SUBSCRIBE round-trip.</param>
    /// <returns>The handle every resulting <see cref="OnDataChange"/> is attributed to.</returns>
    public async Task<ISubscriptionHandle> SubscribeAsync(
        IReadOnlyList<string> fullReferences,
        TimeSpan publishingInterval,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(fullReferences);

        var defs = new List<MqttTagDefinition>(fullReferences.Count);
        var now = DateTime.UtcNow;
        foreach (var reference in fullReferences)
        {
            if (_resolver.TryResolve(reference, out var def))
            {
                defs.Add(def);
                continue;
            }

            // Not an authored MQTT tag. Say so, rather than leaving it in "waiting for initial data"
            // forever — a reference that will never be fed is a different fault from one that has
            // simply not been fed yet.
            _logger?.LogWarning(
                "MQTT driver '{DriverId}': subscribe reference '{RawPath}' is not an authored MQTT tag.",
                _driverId,
                reference);
            Values.Update(reference, new DataValueSnapshot(null, StatusBadNodeIdUnknown, null, now));
        }

        var handle = new MqttSubscriptionHandle(BuildDiagnosticId(defs));
        _refsByHandle[handle] = [.. defs.Select(d => d.Name)];
        foreach (var def in defs)
        {
            _handleByRawPath[def.Name] = handle;
        }

        // One SUBSCRIBE per distinct topic, and only for topics not already live: re-SUBSCRIBEing an
        // established filter is legal but pointless, and it re-triggers the retained seed.
        var pending = defs.Where(d => !_subscribedTopics.ContainsKey(d.Topic)).ToList();
        await EstablishAsync(BuildFilters(pending, _defaultQos), cancellationToken).ConfigureAwait(false);

        return handle;
    }

    /// <summary>
    ///     Stops attributing notifications to <paramref name="handle"/>. References another live
    ///     subscription also covers keep publishing.
    /// </summary>
    /// <remarks>
    ///     The broker-side subscription is deliberately left in place: MQTT UNSUBSCRIBE is per-filter,
    ///     several handles can share one filter, and an idle filter costs only the traffic a topic
    ///     already produces. The session teardown drops them all.
    /// </remarks>
    /// <param name="handle">The handle returned by <see cref="SubscribeAsync"/>.</param>
    /// <param name="cancellationToken">Unused; present for the <c>ISubscribable</c> shape.</param>
    /// <returns>A completed task.</returns>
    public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
    {
        if (handle is not null && _refsByHandle.TryRemove(handle, out var refs))
        {
            foreach (var rawPath in refs)
            {
                // Only clear the mapping if it is still OURS — a later overlapping subscription may
                // have taken the reference over, and it must keep publishing.
                _handleByRawPath.TryRemove(new KeyValuePair<string, ISubscriptionHandle>(rawPath, handle));
            }
        }

        return Task.CompletedTask;
    }

    /// <summary>
    ///     Re-establishes every live subscription after a reconnect. Wired to
    ///     <see cref="MqttConnection.Reconnected"/> by <see cref="AttachTo"/>.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b>Idempotent</b> — re-SUBSCRIBEing an already-subscribed filter is expected on a
    ///         persistent session and is harmless; missing one is a healthy-looking client that
    ///         receives nothing forever.
    ///     </para>
    ///     <para>
    ///         <b>Total failure THROWS, partial rejection does not.</b> This is a deliberate split.
    ///         <see cref="MqttConnection"/>'s contract is that a throwing <c>Reconnected</c> subscriber
    ///         tears the fresh session down and retries under backoff — the correct response to "no
    ///         subscription was re-established", because the alternative is serving a connected,
    ///         permanently deaf driver. But if the broker granted some filters and refused others
    ///         (an ACL denying one topic), tearing the session down would retry forever and take
    ///         <i>every</i> tag dark to punish one; those references degrade to Bad instead and the
    ///         rest keep flowing. That is the plan's "SUBACK failure → per-ref Bad, not hang", scoped
    ///         to the case where it does not cost the whole driver.
    ///     </para>
    /// </remarks>
    /// <param name="cancellationToken">The connection's lifetime token; cancelled by its dispose.</param>
    /// <returns>A task that completes when the re-subscribe has been answered.</returns>
    /// <exception cref="Exception">
    ///     The subscribe failed outright, or every filter was refused — the caller is expected to tear
    ///     the session down and retry.
    /// </exception>
    public async Task OnReconnectedAsync(CancellationToken cancellationToken)
    {
        var authored = _authored;
        var subscribed = _subscribedTopics.Keys.ToList();

        // ByFilter, NOT ByExactTopic: these keys are subscribed FILTER strings, so a wildcard-authored
        // tag's key is its pattern ("f/+/temp"). Reading the concrete-only index here misses every
        // wildcard tag, drops it out of the re-subscribe set, and leaves it dark forever behind a
        // connection that reports healthy — see the AuthoredTable remarks.
        var live = subscribed
            .Select(topic => authored.ByFilter.TryGetValue(topic, out var defs) ? defs : [])
            .SelectMany(defs => defs)
            .ToList();

        var filters = BuildFilters(live, _defaultQos);
        if (filters.Count == 0)
        {
            // Silence here is how the wildcard defect hid: no exception, no log, no SUBSCRIBE. If any
            // topic was live, resolving zero filters for it is an anomaly and must be loud.
            if (subscribed.Count > 0)
            {
                _logger?.LogWarning(
                    "MQTT driver '{DriverId}': reconnect resolved NO filters for {TopicCount} live topic(s) "
                    + "({Topics}); those subscriptions are not being re-established.",
                    _driverId,
                    subscribed.Count,
                    string.Join(", ", subscribed));
            }
            else
            {
                _logger?.LogDebug(
                    "MQTT driver '{DriverId}': reconnect had no live subscriptions to re-establish.",
                    _driverId);
            }

            return;
        }

        var granted = await EstablishAsync(filters, cancellationToken).ConfigureAwait(false);
        if (granted == 0)
        {
            throw new InvalidOperationException(
                $"MQTT driver '{_driverId}': no subscription could be re-established after reconnect "
                + $"({filters.Count} filter(s) requested); tearing the session down rather than serving a "
                + "connection that receives nothing.");
        }
    }

    /// <summary>
    ///     Routes one inbound message to every authored tag whose topic matches, extracts the value,
    ///     updates <see cref="Values"/>, and raises <see cref="OnDataChange"/> for the tags that are
    ///     currently subscribed.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         Runs on MQTTnet's dispatcher thread: no I/O, no locks, and <b>no path throws</b>. A tag
    ///         whose payload is malformed degrades to a Bad status code and the fan-out continues to
    ///         the next tag.
    ///     </para>
    ///     <para>
    ///         <b>Bounded work.</b> A body larger than <see cref="MaxPayloadBytes"/> is refused
    ///         <i>before</i> any decode or parse: the whole point of the dispatcher discipline is that
    ///         one topic cannot stall delivery for every other subscription, and an unbounded
    ///         <c>GetString</c> / <c>JsonDocument.Parse</c> over a multi-megabyte body does exactly
    ///         that. The oversized message degrades its own tags to <c>BadDecodingError</c>.
    ///     </para>
    ///     <para>
    ///         <b>Parsed once.</b> The documented shape is several tags sharing one JSON document with
    ///         a JSONPath each, so the document is parsed at most once per message and shared across
    ///         the whole fan-out rather than re-parsed per tag.
    ///     </para>
    /// </remarks>
    /// <param name="topic">The concrete topic the message arrived on.</param>
    /// <param name="payload">The message body; valid only for the duration of this call.</param>
    /// <param name="retained">The message's retain flag — <c>true</c> is the broker's subscribe-time seed.</param>
    public void HandleMessage(string topic, ReadOnlySpan<byte> payload, bool retained)
    {
        if (string.IsNullOrEmpty(topic))
        {
            return;
        }

        var authored = _authored;
        var now = DateTime.UtcNow;
        var oversized = payload.Length > MaxPayloadBytes;

        // Zero-alloc until a Json-format tag actually matches; disposed in the finally below.
        var json = default(PayloadJson);
        try
        {
            // Concrete topics — the overwhelmingly common case — cost one lookup, so a chatty broker
            // publishing thousands of topics this driver does not care about stays cheap.
            if (authored.ByExactTopic.TryGetValue(topic, out var exact))
            {
                foreach (var def in exact)
                {
                    Deliver(def, payload, retained, now, oversized, ref json);
                }
            }

            // A published topic name may not itself contain a wildcard, so a wildcard-authored
            // definition can never also be an exact-index hit: no tag is delivered twice.
            foreach (var def in authored.Wildcards)
            {
                if (MqttTopicFilterComparer.Compare(topic, def.Topic) == MqttTopicFilterCompareResult.IsMatch)
                {
                    Deliver(def, payload, retained, now, oversized, ref json);
                }
            }
        }
        finally
        {
            json.Dispose();
        }
    }

    /// <summary>
    ///     Collapses a set of definitions into the filters to SUBSCRIBE: one per distinct topic,
    ///     carrying the <b>strongest</b> QoS any tag on that topic asked for (so no tag silently gets a
    ///     weaker delivery guarantee than it was authored with), and seeding from the retained message
    ///     if <b>any</b> tag on the topic wants it (tags that do not are filtered on the retain flag at
    ///     delivery).
    /// </summary>
    /// <param name="defs">The definitions to collapse.</param>
    /// <param name="defaultQos">The driver-level QoS applied to tags that authored none.</param>
    /// <returns>One filter per distinct topic.</returns>
    internal static IReadOnlyList<MqttTopicSubscription> BuildFilters(
        IEnumerable<MqttTagDefinition> defs,
        int defaultQos)
    {
        var byTopic = new Dictionary<string, MqttTopicSubscription>(StringComparer.Ordinal);
        foreach (var def in defs)
        {
            var qos = def.Qos ?? defaultQos;
            byTopic[def.Topic] = byTopic.TryGetValue(def.Topic, out var existing)
                ? existing with
                {
                    Qos = Math.Max(existing.Qos, qos),
                    SeedRetained = existing.SeedRetained || def.RetainSeed,
                }
                : new MqttTopicSubscription(def.Topic, qos, def.RetainSeed);
        }

        return [.. byTopic.Values];
    }

    /// <summary>
    ///     Issues the SUBSCRIBE and applies its verdict: granted topics join the live set, refused ones
    ///     degrade their references to <c>BadCommunicationError</c>.
    /// </summary>
    /// <param name="filters">The filters to establish; an empty list is a no-op.</param>
    /// <param name="cancellationToken">Cancellation for the round-trip.</param>
    /// <returns>How many filters the broker granted.</returns>
    private async Task<int> EstablishAsync(
        IReadOnlyList<MqttTopicSubscription> filters,
        CancellationToken cancellationToken)
    {
        if (filters.Count == 0)
        {
            return 0;
        }

        var transport = _transport;
        if (transport is null)
        {
            // No transport: the intent is recorded so the reconnect path establishes it, but NOTHING
            // reaches a broker now. Loud, because outside tests this means the composition order is
            // wrong — AttachTo must run before the OPC UA server's first subscribe, or these topics
            // stay unestablished until the connection happens to drop and recover.
            foreach (var filter in filters)
            {
                _subscribedTopics[filter.Topic] = 0;
            }

            _logger?.LogWarning(
                "MQTT driver '{DriverId}': {FilterCount} subscription(s) recorded with no connection attached; "
                + "nothing was sent to a broker. Call AttachTo before subscribing.",
                _driverId,
                filters.Count);

            return filters.Count;
        }

        IReadOnlyList<MqttSubscribeOutcome> outcomes;
        try
        {
            outcomes = await transport.SubscribeAsync(filters, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger?.LogError(
                ex,
                "MQTT driver '{DriverId}': SUBSCRIBE for {FilterCount} filter(s) failed; the affected tags degrade to Bad.",
                _driverId,
                filters.Count);
            foreach (var filter in filters)
            {
                DegradeTopic(filter.Topic, ex.GetType().Name);
            }

            return 0;
        }

        var granted = 0;
        foreach (var outcome in outcomes)
        {
            if (outcome.Granted)
            {
                _subscribedTopics[outcome.Topic] = 0;
                granted++;
            }
            else
            {
                _logger?.LogError(
                    "MQTT driver '{DriverId}': broker refused SUBSCRIBE '{Topic}' ({Reason}); its tags degrade to Bad.",
                    _driverId,
                    outcome.Topic,
                    outcome.Reason);
                DegradeTopic(outcome.Topic, outcome.Reason);
            }
        }

        return granted;
    }

    /// <summary>Marks every authored reference on <paramref name="topic"/> as unreachable.</summary>
    /// <remarks>
    ///     <paramref name="topic"/> is a subscribed <i>filter</i> string (it came from a requested
    ///     filter or a SUBACK outcome), so the lookup must go through <c>ByFilter</c> — the
    ///     concrete-only index misses every wildcard-authored tag and this method would then return
    ///     having silently degraded nothing, leaving the tag at <c>BadWaitingForInitialData</c> (or
    ///     worse, its last Good value) after the broker refused its subscription.
    /// </remarks>
    /// <param name="topic">The topic filter whose subscription could not be established.</param>
    /// <param name="reason">The failure reason, for logs.</param>
    private void DegradeTopic(string topic, string reason)
    {
        _subscribedTopics.TryRemove(topic, out _);

        if (!_authored.ByFilter.TryGetValue(topic, out var defs))
        {
            _logger?.LogWarning(
                "MQTT driver '{DriverId}': cannot degrade tags for unestablished filter '{Topic}' ({Reason}) — "
                + "no authored tag resolves to it.",
                _driverId,
                topic,
                reason);
            return;
        }

        var now = DateTime.UtcNow;
        var snapshot = new DataValueSnapshot(null, StatusBadCommunicationError, null, now);
        foreach (var def in defs)
        {
            Values.Update(def.Name, snapshot);
            Raise(def.Name, snapshot);
        }

        _logger?.LogDebug(
            "MQTT driver '{DriverId}': degraded {Count} tag(s) on '{Topic}' ({Reason}).",
            _driverId,
            defs.Length,
            topic,
            reason);
    }

    /// <summary>
    ///     Delivers one message to one tag: honours the retained-seed opt-out, extracts the value,
    ///     caches it and raises the notification. Never throws.
    /// </summary>
    /// <param name="def">The authored tag this message matched.</param>
    /// <param name="payload">The message body.</param>
    /// <param name="retained">The message's retain flag.</param>
    /// <param name="nowUtc">One clock read shared by the whole fan-out, so siblings agree.</param>
    /// <param name="oversized">Whether the body exceeded <see cref="MaxPayloadBytes"/>.</param>
    /// <param name="json">The fan-out's shared JSON parse, created on first use.</param>
    private void Deliver(
        MqttTagDefinition def,
        ReadOnlySpan<byte> payload,
        bool retained,
        DateTime nowUtc,
        bool oversized,
        ref PayloadJson json)
    {
        // MQTT 3.1.1 has no retain-handling option, so the broker always replays its retained message
        // on subscribe; a tag that opted out of seeding is filtered here rather than at the broker.
        if (retained && !def.RetainSeed)
        {
            return;
        }

        DataValueSnapshot snapshot;
        try
        {
            // The cap is enforced before any decode/parse, so an oversized body costs one comparison
            // per matched tag rather than a full pass over the payload.
            snapshot = oversized
                ? new DataValueSnapshot(null, StatusBadDecodingError, null, nowUtc)
                : Extract(def, payload, nowUtc, ref json);
        }
        catch (Exception ex)
        {
            // Defence in depth: Extract is written not to throw, but this runs on the library's
            // dispatcher and an escape here would stall delivery for every other subscription.
            snapshot = new DataValueSnapshot(null, StatusBadDecodingError, null, nowUtc);
            _logger?.LogDebug(ex, "MQTT driver '{DriverId}': extraction threw for '{RawPath}'.", _driverId, def.Name);
        }

        if (snapshot.StatusCode != StatusGood && _warnedRefs.TryAdd(def.Name, 0))
        {
            // Loud once per reference per deploy, quiet thereafter: a mis-authored jsonPath on a 10 Hz
            // topic must be diagnosable without drowning the log.
            _logger?.LogWarning(
                "MQTT driver '{DriverId}': tag '{RawPath}' (topic '{Topic}', {Format}, jsonPath '{JsonPath}', "
                + "{DataType}) could not be extracted from its payload; status 0x{Status:X8}.",
                _driverId,
                def.Name,
                def.Topic,
                def.PayloadFormat,
                def.JsonPath,
                def.DataType,
                snapshot.StatusCode);
        }

        Values.Update(def.Name, snapshot);
        Raise(def.Name, snapshot);
    }

    /// <summary>
    ///     Raises <see cref="OnDataChange"/> for a reference, if a live subscription covers it.
    ///     A throwing subscriber is contained so it cannot starve the rest of the fan-out — or, worse,
    ///     escape onto MQTTnet's dispatcher thread.
    /// </summary>
    /// <param name="rawPath">The RawPath — the reference the notification is published under.</param>
    /// <param name="snapshot">The value/quality/timestamps to publish.</param>
    private void Raise(string rawPath, DataValueSnapshot snapshot)
    {
        if (!_handleByRawPath.TryGetValue(rawPath, out var handle))
        {
            // Authored but not subscribed: the value is cached (so a read answers) and nothing is
            // published — there is no subscription to attribute it to.
            return;
        }

        try
        {
            OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, rawPath, snapshot));
        }
        catch (Exception ex)
        {
            _logger?.LogError(
                ex,
                "MQTT driver '{DriverId}': a data-change subscriber threw for '{RawPath}'.",
                _driverId,
                rawPath);
        }
    }

    /// <summary>
    ///     Extracts the tag's value from a payload according to its <see cref="MqttPayloadFormat"/>.
    ///     Never throws: every failure becomes a Bad status code on the returned snapshot.
    /// </summary>
    /// <param name="def">The authored tag definition.</param>
    /// <param name="payload">The message body.</param>
    /// <param name="nowUtc">Timestamp for the snapshot — MQTT carries no source timestamp of its own.</param>
    /// <param name="json">The fan-out's shared JSON parse, created on first use by a Json-format tag.</param>
    /// <returns>The extracted snapshot.</returns>
    internal static DataValueSnapshot Extract(
        MqttTagDefinition def,
        ReadOnlySpan<byte> payload,
        DateTime nowUtc,
        ref PayloadJson json)
    {
        switch (def.PayloadFormat)
        {
            case MqttPayloadFormat.Json:
                return ExtractJson(def, payload, nowUtc, ref json);

            case MqttPayloadFormat.Raw:
                // "Bytes as-is": the payload's UTF-8 text, published verbatim. dataType is deliberately
                // NOT applied — Scalar is the format that coerces. Authoring Raw with a numeric
                // dataType is a mis-authoring, and one the deploy-time inspection should grow a
                // warning for.
                return TryDecodeUtf8(payload, out var raw)
                    ? new DataValueSnapshot(raw, StatusGood, nowUtc, nowUtc)
                    : Bad(StatusBadDecodingError, nowUtc);

            case MqttPayloadFormat.Scalar:
                if (!TryDecodeUtf8(payload, out var text))
                {
                    return Bad(StatusBadDecodingError, nowUtc);
                }

                return TryCoerceText(text, def.DataType, out var scalar)
                    ? new DataValueSnapshot(scalar, StatusGood, nowUtc, nowUtc)
                    : Bad(StatusBadTypeMismatch, nowUtc);

            default:
                return Bad(StatusBadDecodingError, nowUtc);
        }
    }

    private static DataValueSnapshot Bad(uint status, DateTime nowUtc) => new(null, status, null, nowUtc);

    /// <summary>
    ///     Selects the tag's JSONPath out of the fan-out's <b>shared</b> parse and coerces it to the
    ///     tag's data type. The document is parsed at most once per message however many tags share the
    ///     topic — the documented "one JSON document, one JSONPath per signal" shape would otherwise
    ///     re-parse it once per tag, on the dispatcher thread.
    /// </summary>
    /// <param name="def">The authored tag definition.</param>
    /// <param name="payload">The message body.</param>
    /// <param name="nowUtc">Timestamp for the snapshot.</param>
    /// <param name="json">The shared parse; parses on first call and caches the outcome.</param>
    /// <returns>The extracted snapshot.</returns>
    private static DataValueSnapshot ExtractJson(
        MqttTagDefinition def,
        ReadOnlySpan<byte> payload,
        DateTime nowUtc,
        ref PayloadJson json)
    {
        if (!json.TryGetRoot(payload, out var root))
        {
            return Bad(StatusBadDecodingError, nowUtc);
        }

        if (!TryEvaluateJsonPath(root, def.JsonPath, out var selected))
        {
            return Bad(StatusBadDecodingError, nowUtc);
        }

        return TryCoerce(selected, def.DataType, out var value)
            ? new DataValueSnapshot(value, StatusGood, nowUtc, nowUtc)
            : Bad(StatusBadTypeMismatch, nowUtc);
    }

    /// <summary>
    ///     One message's JSON parse, shared across the whole fan-out. A mutable struct passed by
    ///     <c>ref</c> so the common case — a message no Json-format tag matched — allocates nothing at
    ///     all, while a message several Json tags share is parsed exactly once.
    /// </summary>
    internal struct PayloadJson : IDisposable
    {
        private JsonDocument? _doc;
        private bool _attempted;
        private bool _parsed;

        /// <summary>
        ///     The parsed document root, parsing on first call. A malformed payload is remembered as a
        ///     failure so the fan-out's remaining tags do not each retry the same doomed parse.
        /// </summary>
        /// <param name="payload">The message body; only read on the first call.</param>
        /// <param name="root">The document root when this returns <see langword="true"/>.</param>
        /// <returns><see langword="true"/> when the payload is valid JSON.</returns>
        public bool TryGetRoot(ReadOnlySpan<byte> payload, out JsonElement root)
        {
            if (!_attempted)
            {
                _attempted = true;
                try
                {
                    var reader = new Utf8JsonReader(payload);
                    _parsed = JsonDocument.TryParseValue(ref reader, out _doc);
                }
                catch (JsonException)
                {
                    _parsed = false;
                }
            }

            root = _parsed ? _doc!.RootElement : default;
            return _parsed;
        }

        /// <inheritdoc/>
        public void Dispose()
        {
            _doc?.Dispose();
            _doc = null;
        }
    }

    /// <summary>
    ///     Evaluates the <b>supported subset</b> of JSONPath against a document: the root (<c>$</c>),
    ///     dotted member access (<c>$.a.b</c>), bracketed member access (<c>$['a']</c> / <c>$["a"]</c>)
    ///     and non-negative array indexing (<c>$.a[0]</c>).
    /// </summary>
    /// <remarks>
    ///     Filters, slices, wildcards and recursive descent (<c>$..x</c>) are deliberately <b>not</b>
    ///     supported: this repo pins no JSONPath package, adding one for an expression form that
    ///     selects a <i>set</i> — while a tag needs exactly one scalar — is the wrong trade. An
    ///     unsupported expression is reported as a miss, so the tag surfaces
    ///     <c>BadDecodingError</c> plus the one-shot warning rather than a wrong value.
    /// </remarks>
    /// <param name="root">The parsed document root.</param>
    /// <param name="path">The authored JSONPath.</param>
    /// <param name="value">The selected element when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the path is supported and selects an element.</returns>
    internal static bool TryEvaluateJsonPath(JsonElement root, string path, out JsonElement value)
    {
        value = root;
        if (string.IsNullOrEmpty(path) || path[0] != '$')
        {
            return false;
        }

        var i = 1;
        while (i < path.Length)
        {
            if (path[i] == '.')
            {
                i++;
                if (i >= path.Length || path[i] == '.')
                {
                    return false; // trailing '.', or recursive descent '..'
                }

                var start = i;
                while (i < path.Length && path[i] != '.' && path[i] != '[')
                {
                    i++;
                }

                var name = path[start..i];
                if (name.Length == 0 || name == "*")
                {
                    return false;
                }

                if (value.ValueKind != JsonValueKind.Object || !value.TryGetProperty(name, out var member))
                {
                    return false;
                }

                value = member;
            }
            else if (path[i] == '[')
            {
                var close = path.IndexOf(']', i);
                if (close < 0)
                {
                    return false;
                }

                var inner = path[(i + 1)..close].Trim();
                i = close + 1;

                if (inner.Length >= 2
                    && ((inner[0] == '\'' && inner[^1] == '\'') || (inner[0] == '"' && inner[^1] == '"')))
                {
                    var name = inner[1..^1];
                    if (value.ValueKind != JsonValueKind.Object || !value.TryGetProperty(name, out var member))
                    {
                        return false;
                    }

                    value = member;
                }
                else if (int.TryParse(inner, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index)
                         && index >= 0)
                {
                    if (value.ValueKind != JsonValueKind.Array || index >= value.GetArrayLength())
                    {
                        return false;
                    }

                    value = value[index];
                }
                else
                {
                    return false; // slice, filter or wildcard — outside the supported subset
                }
            }
            else
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>Coerces a selected JSON element to the tag's declared data type.</summary>
    /// <param name="element">The selected element.</param>
    /// <param name="type">The tag's declared type.</param>
    /// <param name="value">The coerced value when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the element fits the declared type.</returns>
    internal static bool TryCoerce(JsonElement element, DriverDataType type, out object? value)
    {
        value = null;

        if (element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
        {
            return false;
        }

        if (type is DriverDataType.String or DriverDataType.Reference)
        {
            // A String tag pointed at an object/array gets that subtree's JSON text — the authored
            // intent ("give me this fragment") rather than a refusal.
            value = element.ValueKind == JsonValueKind.String ? element.GetString() : element.GetRawText();
            return true;
        }

        // A JSON string holding a number ({"v":"21.5"}) is common in the wild; parse it as text rather
        // than refusing a payload the operator plainly meant.
        if (element.ValueKind == JsonValueKind.String)
        {
            return TryCoerceText(element.GetString() ?? string.Empty, type, out value);
        }

        switch (type)
        {
            case DriverDataType.Boolean:
                if (element.ValueKind == JsonValueKind.True) { value = true; return true; }
                if (element.ValueKind == JsonValueKind.False) { value = false; return true; }
                if (element.ValueKind == JsonValueKind.Number && element.TryGetDouble(out var b)) { value = b != 0d; return true; }
                return false;

            case DriverDataType.Int16:
            case DriverDataType.Int32:
            case DriverDataType.Int64:
            case DriverDataType.UInt16:
            case DriverDataType.UInt32:
            case DriverDataType.UInt64:
                if (element.ValueKind != JsonValueKind.Number)
                {
                    return false;
                }

                // Fast path: the JSON number is already written as an integer.
                if (TryExactInteger(element, type, out value))
                {
                    return true;
                }

                // Fallback: an integral-valued REAL. JS/Python edge gateways routinely serialize an
                // integer as 5.0 (or 5E2), and System.Text.Json's TryGetInt32/64 are syntactic — they
                // refuse it. Refusing here would mean an integer tag fed by such a gateway silently
                // never receives data, so an exactly-integral real is accepted; a genuinely fractional
                // value (5.5) is still refused rather than rounded.
                return TryIntegralReal(element.GetRawText(), type, out value);

            case DriverDataType.Float32:
                if (element.ValueKind == JsonValueKind.Number && element.TryGetSingle(out var f32)) { value = f32; return true; }
                return false;

            case DriverDataType.Float64:
                if (element.ValueKind == JsonValueKind.Number && element.TryGetDouble(out var f64)) { value = f64; return true; }
                return false;

            case DriverDataType.DateTime:
                // Only ISO-8601 text is accepted; a bare number is ambiguous (epoch seconds? ms?) and
                // guessing would silently place samples decades apart.
                return false;

            default:
                return false;
        }
    }

    /// <summary>Parses a textual payload (or JSON string) into the tag's declared data type.</summary>
    /// <param name="text">The decoded text.</param>
    /// <param name="type">The tag's declared type.</param>
    /// <param name="value">The parsed value when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the text parses as the declared type.</returns>
    internal static bool TryCoerceText(string text, DriverDataType type, out object? value)
    {
        value = null;
        var inv = CultureInfo.InvariantCulture;
        var trimmed = text.Trim();

        switch (type)
        {
            case DriverDataType.String:
            case DriverDataType.Reference:
                // Untrimmed: for a String tag the payload IS the value, whitespace included.
                value = text;
                return true;

            case DriverDataType.Boolean:
                if (bool.TryParse(trimmed, out var b)) { value = b; return true; }
                if (long.TryParse(trimmed, NumberStyles.Integer, inv, out var bi)) { value = bi != 0L; return true; }
                return false;

            case DriverDataType.Int16:
            case DriverDataType.Int32:
            case DriverDataType.Int64:
            case DriverDataType.UInt16:
            case DriverDataType.UInt32:
            case DriverDataType.UInt64:
                // Same two-step as the JSON path: exact integer text first, then an integral-valued
                // real ("5.0"), which a Scalar-format edge gateway emits just as readily.
                if (TryExactIntegerText(trimmed, type, out value))
                {
                    return true;
                }

                return TryIntegralReal(trimmed, type, out value);

            case DriverDataType.Float32:
                if (float.TryParse(trimmed, NumberStyles.Float, inv, out var f32)) { value = f32; return true; }
                return false;

            case DriverDataType.Float64:
                if (double.TryParse(trimmed, NumberStyles.Float, inv, out var f64)) { value = f64; return true; }
                return false;

            case DriverDataType.DateTime:
                if (DateTime.TryParse(
                        trimmed,
                        inv,
                        DateTimeStyles.RoundtripKind | DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal,
                        out var dt))
                {
                    value = dt;
                    return true;
                }

                return false;

            default:
                return false;
        }
    }

    /// <summary>Reads a JSON number that is written as an exact integer of the requested type.</summary>
    /// <param name="element">The JSON number.</param>
    /// <param name="type">The target integer type.</param>
    /// <param name="value">The boxed value when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the number is an exact integer in range.</returns>
    private static bool TryExactInteger(JsonElement element, DriverDataType type, out object? value)
    {
        value = type switch
        {
            DriverDataType.Int16 => element.TryGetInt16(out var v) ? v : null,
            DriverDataType.Int32 => element.TryGetInt32(out var v) ? v : null,
            DriverDataType.Int64 => element.TryGetInt64(out var v) ? v : null,
            DriverDataType.UInt16 => element.TryGetUInt16(out var v) ? v : null,
            DriverDataType.UInt32 => element.TryGetUInt32(out var v) ? v : null,
            DriverDataType.UInt64 => element.TryGetUInt64(out var v) ? v : (object?)null,
            _ => null,
        };

        return value is not null;
    }

    /// <summary>Parses text written as an exact integer of the requested type.</summary>
    /// <param name="text">The trimmed text.</param>
    /// <param name="type">The target integer type.</param>
    /// <param name="value">The boxed value when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the text is an exact integer in range.</returns>
    private static bool TryExactIntegerText(string text, DriverDataType type, out object? value)
    {
        var inv = CultureInfo.InvariantCulture;
        value = type switch
        {
            DriverDataType.Int16 => short.TryParse(text, NumberStyles.Integer, inv, out var v) ? v : null,
            DriverDataType.Int32 => int.TryParse(text, NumberStyles.Integer, inv, out var v) ? v : null,
            DriverDataType.Int64 => long.TryParse(text, NumberStyles.Integer, inv, out var v) ? v : null,
            DriverDataType.UInt16 => ushort.TryParse(text, NumberStyles.Integer, inv, out var v) ? v : null,
            DriverDataType.UInt32 => uint.TryParse(text, NumberStyles.Integer, inv, out var v) ? v : null,
            DriverDataType.UInt64 => ulong.TryParse(text, NumberStyles.Integer, inv, out var v) ? v : (object?)null,
            _ => null,
        };

        return value is not null;
    }

    /// <summary>
    ///     Accepts a real that is <b>exactly</b> integral (<c>5.0</c>, <c>5E2</c>) as an integer of the
    ///     requested type. A fractional value is refused, never rounded — silently turning 5.5 into 5
    ///     or 6 would be a wrong value, which is worse than no value.
    /// </summary>
    /// <remarks>
    ///     Parsed as <see cref="decimal"/> rather than <see cref="double"/> so the integrality test and
    ///     the conversion are exact across the whole Int64/UInt64 range, where double loses precision
    ///     above 2^53. Out-of-range values overflow the conversion and are refused.
    /// </remarks>
    /// <param name="text">The number's text (JSON raw text, or a Scalar payload).</param>
    /// <param name="type">The target integer type.</param>
    /// <param name="value">The boxed value when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the text is an integral real in range.</returns>
    private static bool TryIntegralReal(string text, DriverDataType type, out object? value)
    {
        value = null;

        if (!decimal.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)
            || d != decimal.Truncate(d))
        {
            return false;
        }

        try
        {
            value = type switch
            {
                DriverDataType.Int16 => (short)d,
                DriverDataType.Int32 => (int)d,
                DriverDataType.Int64 => (long)d,
                DriverDataType.UInt16 => (ushort)d,
                DriverDataType.UInt32 => (uint)d,
                DriverDataType.UInt64 => (ulong)d,
                _ => null,
            };
        }
        catch (OverflowException)
        {
            // decimal → integer conversions throw rather than wrap; out of range is a refusal.
            value = null;
        }

        return value is not null;
    }

    /// <summary>
    ///     Decodes a payload as UTF-8, refusing invalid byte sequences rather than rendering a genuinely
    ///     binary body as a field of replacement characters (the same choice the browse session makes).
    /// </summary>
    /// <param name="payload">The message body.</param>
    /// <param name="text">The decoded text when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when the payload is valid UTF-8.</returns>
    private static bool TryDecodeUtf8(ReadOnlySpan<byte> payload, out string text)
    {
        try
        {
            text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)
                .GetString(payload);
            return true;
        }
        catch (DecoderFallbackException)
        {
            text = string.Empty;
            return false;
        }
    }

    /// <summary>
    ///     Builds the handle's diagnostic id: the ingest mode plus the topics it covers, bounded so a
    ///     thousand-tag subscription does not produce a thousand-topic string in every log line.
    /// </summary>
    /// <param name="defs">The definitions the subscription covers.</param>
    /// <returns>The diagnostic id.</returns>
    private string BuildDiagnosticId(IReadOnlyCollection<MqttTagDefinition> defs)
    {
        const int MaxNamedTopics = 5;

        var id = Interlocked.Increment(ref _handleSeq);
        var topics = defs.Select(d => d.Topic).Distinct(StringComparer.Ordinal).ToList();
        var named = string.Join(',', topics.Take(MaxNamedTopics));
        var overflow = topics.Count > MaxNamedTopics ? $",+{topics.Count - MaxNamedTopics}" : string.Empty;

        return $"mqtt:{_mode}:{id}:[{named}{overflow}]";
    }

    /// <summary>Opaque subscription identity; the diagnostic id names the mode and its topics.</summary>
    /// <param name="DiagnosticId">The human-readable identity surfaced for diagnostics.</param>
    private sealed record MqttSubscriptionHandle(string DiagnosticId) : ISubscriptionHandle;

    /// <summary>
    ///     The authored tag table and its derived indexes, published as one immutable snapshot so the
    ///     dispatcher thread reads a consistent view while a redeploy rebuilds it.
    /// </summary>
    /// <remarks>
    ///     <b>Two topic indexes, and the difference is load-bearing.</b> Delivery matches an
    ///     <i>incoming published topic</i>, which never carries a wildcard, so it uses
    ///     <see cref="ByExactTopic"/> (concrete only) plus a comparer scan of <see cref="Wildcards"/>.
    ///     Every path that instead reconstructs state from a <i>subscribed filter string</i> —
    ///     reconnect re-subscribe and per-topic degradation — must use <see cref="ByFilter"/>, which is
    ///     keyed by the tag's authored topic <b>including wildcard patterns</b>, because that is what
    ///     lands in <c>_subscribedTopics</c> and in a SUBACK outcome. Looking a wildcard filter up in
    ///     the concrete index always misses, and a miss on those paths is silent: the tag simply stops
    ///     updating behind a healthy-looking connection, keeping its last Good value forever.
    /// </remarks>
    /// <param name="ByRawPath">RawPath → definition; the v3 resolver's backing table.</param>
    /// <param name="ByFilter">
    ///     Authored topic <i>filter</i> string → every definition bound to it, wildcard patterns
    ///     included. Keyed the same way <c>_subscribedTopics</c> and SUBACK outcomes are.
    /// </param>
    /// <param name="ByExactTopic">Concrete topic → its definitions; the delivery fast path.</param>
    /// <param name="Wildcards">Definitions whose authored topic carries an MQTT wildcard.</param>
    private sealed record AuthoredTable(
        FrozenDictionary<string, MqttTagDefinition> ByRawPath,
        FrozenDictionary<string, MqttTagDefinition[]> ByFilter,
        FrozenDictionary<string, MqttTagDefinition[]> ByExactTopic,
        MqttTagDefinition[] Wildcards)
    {
        public static readonly AuthoredTable Empty =
            Build(new Dictionary<string, MqttTagDefinition>(StringComparer.Ordinal));

        /// <summary>True when a topic string carries an MQTT wildcard and is therefore a filter, not a name.</summary>
        /// <param name="topic">The authored topic.</param>
        /// <returns><see langword="true"/> when it contains <c>+</c> or <c>#</c>.</returns>
        public static bool IsWildcard(string topic) =>
            topic.Contains(MqttTopicFilterComparer.SingleLevelWildcard, StringComparison.Ordinal)
            || topic.Contains(MqttTopicFilterComparer.MultiLevelWildcard, StringComparison.Ordinal);

        /// <summary>Builds the snapshot: every def into <c>ByFilter</c>, concrete/wildcard split for delivery.</summary>
        /// <param name="byRawPath">The mapped definitions, keyed by RawPath.</param>
        /// <returns>The immutable snapshot.</returns>
        public static AuthoredTable Build(IReadOnlyDictionary<string, MqttTagDefinition> byRawPath)
        {
            var byFilter = new Dictionary<string, List<MqttTagDefinition>>(StringComparer.Ordinal);
            var concrete = new Dictionary<string, List<MqttTagDefinition>>(StringComparer.Ordinal);
            var wildcards = new List<MqttTagDefinition>();

            foreach (var def in byRawPath.Values)
            {
                // EVERY def lands here, wildcard or not — this is the index the reconnect and degrade
                // paths read, and omitting wildcards from it is exactly the C1 silent-death defect.
                if (!byFilter.TryGetValue(def.Topic, out var all))
                {
                    byFilter[def.Topic] = all = [];
                }

                all.Add(def);

                if (IsWildcard(def.Topic))
                {
                    wildcards.Add(def);
                    continue;
                }

                if (!concrete.TryGetValue(def.Topic, out var list))
                {
                    concrete[def.Topic] = list = [];
                }

                list.Add(def);
            }

            return new AuthoredTable(
                byRawPath.ToFrozenDictionary(StringComparer.Ordinal),
                byFilter.ToFrozenDictionary(kv => kv.Key, kv => kv.Value.ToArray(), StringComparer.Ordinal),
                concrete.ToFrozenDictionary(kv => kv.Key, kv => kv.Value.ToArray(), StringComparer.Ordinal),
                [.. wildcards]);
        }
    }
}
