using Serilog;

namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags;

/// <summary>
///     Periodic re-evaluation scheduler for tags with a non-null
///     <see cref="VirtualTagDefinition.TimerInterval"/>. Independent of the
///     change-trigger path — a tag can be timer-only, change-only, or both. One
///     <see cref="System.Threading.Timer"/> per interval-group keeps the wire count
///     low regardless of tag count.
/// </summary>
/// <remarks>
///     <para>
///         Each timer group carries a per-group in-flight flag (see
///         <c>TickGroup.InFlight</c>). When the timer fires while a tick for the same
///         group is still running, the new callback skips the work and increments
///         <see cref="SkippedTickCount"/> rather than blocking a thread-pool thread on
///         the engine's evaluation gate. This bounds the work outstanding at one tick
///         per group, regardless of how long an individual evaluation takes.
///     </para>
/// </remarks>
public sealed class TimerTriggerScheduler : IDisposable
{
    private readonly VirtualTagEngine _engine;
    private readonly ILogger _logger;
    private readonly List<Timer> _timers = [];
    private readonly List<TickGroup> _groups = [];
    private readonly CancellationTokenSource _cts = new();
    private long _skippedTickCount;
    private bool _disposed;
    private bool _started;

    /// <summary>Initializes a new instance of the <see cref="TimerTriggerScheduler"/> class.</summary>
    /// <param name="engine">The virtual tag engine to trigger evaluations on.</param>
    /// <param name="logger">A logger for diagnostic output.</param>
    public TimerTriggerScheduler(VirtualTagEngine engine, ILogger logger)
    {
        _engine = engine ?? throw new ArgumentNullException(nameof(engine));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>
    ///     Diagnostic counter: number of timer callbacks that skipped their work because
    ///     the prior tick for the same group was still running. Exposed for tests +
    ///     operational metrics. Monotonic; never resets.
    /// </summary>
    public long SkippedTickCount => Interlocked.Read(ref _skippedTickCount);

    /// <summary>
    ///     Stand up one <see cref="Timer"/> per unique interval. All tags with
    ///     matching interval share a timer; each tick triggers re-evaluation of the
    ///     group in topological order so cascades are consistent with change-triggered
    ///     behavior.
    /// </summary>
    /// <param name="definitions">The virtual tag definitions to schedule timers for.</param>
    /// <exception cref="InvalidOperationException">Thrown when <c>Start</c> is called more than once on the same instance.</exception>
    public void Start(IReadOnlyList<VirtualTagDefinition> definitions)
    {
        if (_disposed) throw new ObjectDisposedException(nameof(TimerTriggerScheduler));
        if (_started) throw new InvalidOperationException(
            "TimerTriggerScheduler.Start has already been called. Create a new instance for each Load cycle.");
        _started = true;

        var byInterval = definitions
            .Where(d => d.TimerInterval.HasValue && d.TimerInterval.Value > TimeSpan.Zero)
            .GroupBy(d => d.TimerInterval!.Value);

        foreach (var group in byInterval)
        {
            var paths = group.Select(d => d.Path).ToArray();
            var interval = group.Key;
            var ctx = new TickGroup(paths);
            _groups.Add(ctx);
            var timer = new Timer(_ => OnTimer(ctx), null, interval, interval);
            _timers.Add(timer);
            _logger.Information("TimerTriggerScheduler: {TagCount} tag(s) on {Interval} cadence",
                paths.Length, interval);
        }
    }

    private void OnTimer(TickGroup ctx)
    {
        if (_cts.IsCancellationRequested) return;

        // Skip the tick when the prior one for this group is still running. Without
        // this guard a slow evaluation (or one waiting on the engine's _evalGate) would
        // cause subsequent timer callbacks to each pin a thread-pool thread on the
        // gate, compounding under high tick rates.
        if (Interlocked.CompareExchange(ref ctx.InFlight, 1, 0) != 0)
        {
            Interlocked.Increment(ref _skippedTickCount);
            return;
        }

        // Run async without blocking the timer's pool-thread callback. The task is
        // fire-and-forget — failures are logged inside RunTickAsync; the InFlight flag
        // is reset in the finally block so the next tick can proceed.
        _ = RunTickAsync(ctx);
    }

    private async Task RunTickAsync(TickGroup ctx)
    {
        try
        {
            foreach (var p in ctx.Paths)
            {
                if (_cts.IsCancellationRequested) return;
                try
                {
                    await _engine.EvaluateOneAsync(p, _cts.Token).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                catch (Exception ex)
                {
                    _logger.Error(ex, "TimerTriggerScheduler evaluate failed for {Path}", p);
                }
            }
        }
        finally
        {
            Interlocked.Exchange(ref ctx.InFlight, 0);
        }
    }

    /// <summary>Releases all timers and disposes the scheduler's resources.</summary>
    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        _cts.Cancel();
        foreach (var t in _timers)
        {
            try { t.Dispose(); } catch { }
        }
        _timers.Clear();
        _groups.Clear();
        _cts.Dispose();
    }

    /// <summary>Groups tags that share the same timer interval.</summary>
    private sealed class TickGroup
    {
        // 0 = idle, 1 = a tick is currently running (or queued) for this group. Use
        // Interlocked.CompareExchange so a timer callback observes a consistent "is the
        // prior tick still running" answer without taking a lock.
        /// <summary>Flag indicating whether a tick is currently in flight for this group.</summary>
        public int InFlight;

        /// <summary>Gets the list of tag paths in this group.</summary>
        public IReadOnlyList<string> Paths { get; }

        /// <summary>Initializes a new instance of the <see cref="TickGroup"/> class.</summary>
        /// <param name="paths">The tag paths in this timer group.</param>
        public TickGroup(IReadOnlyList<string> paths)
        {
            Paths = paths;
        }
    }
}
