using System.Collections.Frozen;
using Mbproxy.Bcd;

namespace Mbproxy.Proxy;

/// <summary>Direction of a captured tag-value observation.</summary>
public enum CaptureDirection
{
    /// <summary>FC03/FC04 response — the proxy decoded BCD nibbles → binary for the client.</summary>
    Read,

    /// <summary>FC06/FC16 request — the proxy encoded the client's binary → BCD for the PLC.</summary>
    Write,
}

/// <summary>
/// One immutable observation of a BCD tag's value as it last crossed the proxy.
///
/// <para>"PLC side" is always the BCD-encoded form on the Modbus wire to/from the
/// device; "client side" is always the decoded binary integer the upstream client
/// reads or wrote. <see cref="RawHigh"/> is 0 for a 16-bit tag.</para>
/// </summary>
public sealed record TagValueObservation(
    ushort Address,
    byte Width,
    ushort RawLow,
    ushort RawHigh,
    int DecodedValue,
    CaptureDirection Direction,
    DateTimeOffset? UpdatedAtUtc);

/// <summary>
/// Per-PLC, on-demand store of the last value seen for each configured BCD tag — the
/// data source behind the connection-detail page's real-time debug view.
///
/// <para><b>On-demand.</b> The capture starts disarmed: <see cref="Record"/> is an
/// immediate no-op (one volatile-bool read) until <see cref="Arm"/> is called. The
/// SignalR layer arms a PLC's capture only while its detail page has a live
/// subscriber and <see cref="Disarm"/>s it — clearing all slots — when the last
/// viewer leaves, so a reopened page shows "no traffic yet" rather than stale data.</para>
///
/// <para><b>Concurrency.</b> <see cref="Record"/> is called from many upstream-read
/// tasks (FC06/FC16 requests) and the single backend reader task (FC03/FC04
/// responses); <see cref="Snapshot"/> runs on the admin-push thread. Each slot holds a
/// reference to an immutable <see cref="TagValueObservation"/>, swapped with
/// <see cref="Volatile.Write{T}(ref T, T)"/> — reference assignment is atomic and the
/// record is immutable, so a reader never sees a torn slot. No locks.</para>
/// </summary>
internal sealed class TagValueCapture
{
    // Tag address → slot index. Frozen for allocation-free O(1) lookup on the hot path.
    private readonly FrozenDictionary<ushort, int> _addressToSlot;

    // Slot index → tag identity. Parallel to _slots; immutable after construction.
    private readonly ushort[] _addresses;
    private readonly byte[] _widths;

    // Slot index → last observation (null = no traffic captured yet). Each element is
    // swapped via Volatile.Write; never mutated in place.
    private readonly TagValueObservation?[] _slots;

    private volatile bool _armed;

    /// <summary>
    /// Builds a capture for one PLC's resolved BCD tag set. One slot per tag, ordered
    /// ascending by address for a stable debug-view row order.
    /// </summary>
    public TagValueCapture(IEnumerable<BcdTag> tags)
    {
        var ordered = tags
            .GroupBy(t => t.Address)
            .Select(g => g.First())
            .OrderBy(t => t.Address)
            .ToArray();

        _addresses = new ushort[ordered.Length];
        _widths    = new byte[ordered.Length];
        _slots     = new TagValueObservation?[ordered.Length];

        var index = new Dictionary<ushort, int>(ordered.Length);
        for (int i = 0; i < ordered.Length; i++)
        {
            _addresses[i] = ordered[i].Address;
            _widths[i]    = ordered[i].Width;
            index[ordered[i].Address] = i;
        }
        _addressToSlot = index.ToFrozenDictionary();
    }

    /// <summary>Number of BCD tag slots this capture tracks.</summary>
    public int TagCount => _slots.Length;

    /// <summary>True while a detail-page subscriber has armed this capture.</summary>
    public bool IsArmed => _armed;

    /// <summary>Arms capture — <see cref="Record"/> begins storing observations.</summary>
    public void Arm() => _armed = true;

    /// <summary>
    /// Disarms capture and clears every slot, so the next <see cref="Snapshot"/> after a
    /// re-arm reflects only post-re-arm traffic.
    /// </summary>
    public void Disarm()
    {
        _armed = false;
        for (int i = 0; i < _slots.Length; i++)
            Volatile.Write(ref _slots[i], null);
    }

    /// <summary>
    /// Records the last value of the BCD tag at <paramref name="address"/>. No-op when
    /// disarmed or when <paramref name="address"/> is not a tracked tag.
    /// </summary>
    /// <param name="rawLow">BCD-encoded low word as it sits on the PLC wire.</param>
    /// <param name="rawHigh">BCD-encoded high word (0 for a 16-bit tag).</param>
    /// <param name="decoded">Decoded binary integer the client reads/wrote.</param>
    public void Record(ushort address, ushort rawLow, ushort rawHigh, int decoded, CaptureDirection direction)
    {
        if (!_armed)
            return;
        if (!_addressToSlot.TryGetValue(address, out int idx))
            return;

        Volatile.Write(
            ref _slots[idx],
            new TagValueObservation(
                _addresses[idx], _widths[idx], rawLow, rawHigh, decoded, direction,
                DateTimeOffset.UtcNow));
    }

    /// <summary>
    /// Point-in-time projection of every tracked tag. Slots with no traffic yet are
    /// returned with <see cref="TagValueObservation.UpdatedAtUtc"/> = <c>null</c> and
    /// zero values, so the debug view always renders one row per configured tag.
    /// </summary>
    public IReadOnlyList<TagValueObservation> Snapshot()
    {
        var result = new TagValueObservation[_slots.Length];
        for (int i = 0; i < _slots.Length; i++)
        {
            result[i] = Volatile.Read(ref _slots[i])
                ?? new TagValueObservation(
                    _addresses[i], _widths[i], 0, 0, 0, CaptureDirection.Read, null);
        }
        return result;
    }
}
