using System.Collections.Concurrent;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     Thread-safe last-observed-value store backing <see cref="IReadable"/> for MQTT/Sparkplug B.
///     MQTT is a subscribe-first protocol — the driver holds one live broker connection and values
///     arrive pushed, not polled. But the OPC UA server still issues polled <c>IReadable.ReadAsync</c>
///     batches, so this cache is the bridge: the subscription path (message handler) calls
///     <see cref="Update"/> with the newest observed value per reference, and the read path serves
///     the most recent one via <see cref="Read"/>.
/// </summary>
/// <remarks>
///     Keyed by <c>RawPath</c> — the v3 driver-reference identity (see
///     <c>EquipmentTagRefResolver&lt;TDef&gt;</c> and <see cref="MqttTagDefinition.Name"/>,
///     which <b>is</b> the RawPath) — never a topic/JSON-path-derived key. The parameter is
///     therefore named <c>rawPath</c> throughout rather than "topic" or "key" to keep that identity
///     fact visible at every call site.
/// </remarks>
public sealed class LastValueCache
{
    // OPC UA BadWaitingForInitialData (0x80320000) — the repo-wide convention for "no value has
    // been observed yet" (mirrored by CalculationDriver before its first evaluation,
    // VirtualTagEngine for a freshly-materialised node, and OtOpcUaNodeManager /
    // AddressSpaceApplier for a just-deployed variable). MQTT is subscribe-first, so an unseen
    // RawPath is in exactly that state until its first publish arrives. Deliberately NOT
    // GoodNoData — that code is reserved (see NullHistorianDataSource, OtOpcUaNodeManager
    // HistoryRead paths) for "the historian window held no samples", a different question from
    // "has this live reference ever been observed".
    private const uint BadWaitingForInitialData = 0x80320000u;

    private readonly ConcurrentDictionary<string, DataValueSnapshot> _values = new();

    /// <summary>
    ///     Record the newest observed value for <paramref name="rawPath"/>. Called from the MQTT
    ///     subscription/message-handling path. A <c>null</c> or empty <paramref name="rawPath"/>
    ///     is a no-op — never throws.
    /// </summary>
    /// <param name="rawPath">The RawPath identifying the tag.</param>
    /// <param name="snapshot">The newest observed value/quality/timestamps for that tag.</param>
    public void Update(string rawPath, DataValueSnapshot snapshot)
    {
        if (string.IsNullOrEmpty(rawPath)) return;
        _values[rawPath] = snapshot;
    }

    /// <summary>
    ///     Read the last observed value for <paramref name="rawPath"/>. Never throws: a
    ///     <c>null</c>/empty <paramref name="rawPath"/> or a RawPath never observed both return a
    ///     <see cref="BadWaitingForInitialData"/> snapshot rather than an exception, so a batch read
    ///     covering many references degrades per-reference instead of failing the whole call.
    /// </summary>
    /// <param name="rawPath">The RawPath identifying the tag.</param>
    /// <returns>The last observed snapshot, or a BadWaitingForInitialData snapshot if none has been observed.</returns>
    public DataValueSnapshot Read(string rawPath)
    {
        if (!string.IsNullOrEmpty(rawPath) && _values.TryGetValue(rawPath, out var snapshot))
            return snapshot;

        return new DataValueSnapshot(null, BadWaitingForInitialData, null, DateTime.UtcNow);
    }
}
