using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ZB.MOM.WW.ScadaBridge.Commons.Tests")]

namespace ZB.MOM.WW.ScadaBridge.Commons.Types;

/// <summary>
/// Monitors a heartbeat tag subscription for staleness. If no value is received
/// within <see cref="MaxSilence"/>, the <see cref="Stale"/> event fires.
/// Composable into any IDataConnection adapter.
/// </summary>
/// <remarks>
/// Thread-safe: <see cref="Start"/>, <see cref="OnValueReceived"/> and <see cref="Stop"/>
/// may be called from any thread and race the internal timer callback. Each call to
/// <see cref="Start"/> or <see cref="OnValueReceived"/> begins a new monitoring period
/// identified by a generation token; a timer callback only raises <see cref="Stale"/>
/// if it still belongs to the current period. A fresh value, a restart, or a
/// <see cref="Stop"/> arriving while a previous-period callback is in flight bumps the
/// generation, so that callback observes the mismatch and declines to fire — no spurious
/// staleness signal is emitted after the period it was scheduled for has ended.
/// </remarks>
public sealed class StaleTagMonitor : IDisposable
{
    private readonly TimeSpan _maxSilence;
    private readonly object _gate = new();
    private Timer? _timer;

    /// <summary>
    /// Monotonically increasing token identifying the current monitoring period.
    /// Bumped on every <see cref="Start"/>, <see cref="OnValueReceived"/> and
    /// <see cref="Stop"/> so that a timer callback scheduled for an earlier period
    /// can detect that it is stale and decline to fire.
    /// </summary>
    private long _generation;

    /// <summary>Initializes a new <see cref="StaleTagMonitor"/> that fires <see cref="Stale"/> if no value is received within <paramref name="maxSilence"/>.</summary>
    /// <param name="maxSilence">The maximum time with no received value before the <see cref="Stale"/> event fires; must be positive.</param>
    public StaleTagMonitor(TimeSpan maxSilence)
    {
        if (maxSilence <= TimeSpan.Zero)
            throw new ArgumentOutOfRangeException(nameof(maxSilence), "MaxSilence must be positive.");
        _maxSilence = maxSilence;
    }

    /// <summary>
    /// Fires when no value has been received within <see cref="MaxSilence"/>.
    /// Fires once per stale period — resets after <see cref="OnValueReceived"/> is called.
    /// </summary>
    public event Action? Stale;

    /// <summary>Gets the maximum silence interval after which the <see cref="Stale"/> event fires.</summary>
    public TimeSpan MaxSilence => _maxSilence;

    /// <summary>
    /// Test-only seam invoked by the timer callback after it has been entered but
    /// before it acquires the synchronization gate. Allows a test to deterministically
    /// interleave a <see cref="Stop"/> / <see cref="OnValueReceived"/> with an in-flight
    /// callback to exercise the stale-fire race. Never set in production.
    /// </summary>
    internal Action? CallbackEnteredHook { get; set; }

    /// <summary>
    /// Start monitoring. The timer begins counting from now.
    /// </summary>
    public void Start()
    {
        lock (_gate)
        {
            _generation++;
            _timer?.Dispose();
            _timer = new Timer(OnTimerElapsed, _generation, _maxSilence, Timeout.InfiniteTimeSpan);
        }
    }

    /// <summary>
    /// Signal that a value was received. Resets the stale timer.
    /// </summary>
    public void OnValueReceived()
    {
        lock (_gate)
        {
            // No active monitoring — nothing to reset.
            if (_timer is null)
                return;

            // Bump the generation: any timer callback for the previous period that
            // has already been entered will see a generation mismatch and decline to
            // raise Stale. The timer is recreated rather than re-armed with
            // Change(...) so the new callback carries the new generation token.
            _generation++;
            _timer.Dispose();
            _timer = new Timer(OnTimerElapsed, _generation, _maxSilence, Timeout.InfiniteTimeSpan);
        }
    }

    /// <summary>
    /// Stop monitoring and dispose the timer.
    /// </summary>
    public void Stop()
    {
        lock (_gate)
        {
            // Bumping the generation invalidates any in-flight callback so a stopped
            // monitor cannot deliver a Stale signal.
            _generation++;
            _timer?.Dispose();
            _timer = null;
        }
    }

    /// <summary>Stops monitoring and disposes the internal timer.</summary>
    public void Dispose()
    {
        Stop();
    }

    private void OnTimerElapsed(object? state)
    {
        var scheduledGeneration = (long)state!;

        CallbackEnteredHook?.Invoke();

        // Only fire if this callback still represents the current period. The check
        // and the generation bump happen under the gate, so a concurrent
        // OnValueReceived / Stop / Start either completes before this guard (its
        // generation bump makes this callback decline) or serializes after it.
        lock (_gate)
        {
            if (_generation != scheduledGeneration)
                return;

            // Consume this period so a duplicate callback for the same generation
            // cannot fire twice; the next Start/OnValueReceived issues a new token.
            _generation++;
        }

        Stale?.Invoke();
    }
}
