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>
public sealed class TimerTriggerScheduler : IDisposable
{
    private readonly VirtualTagEngine _engine;
    private readonly ILogger _logger;
    private readonly List<Timer> _timers = [];
    private readonly CancellationTokenSource _cts = new();
    private bool _disposed;

    public TimerTriggerScheduler(VirtualTagEngine engine, ILogger logger)
    {
        _engine = engine ?? throw new ArgumentNullException(nameof(engine));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <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>
    public void Start(IReadOnlyList<VirtualTagDefinition> definitions)
    {
        if (_disposed) throw new ObjectDisposedException(nameof(TimerTriggerScheduler));

        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 timer = new Timer(_ => Tick(paths), null, interval, interval);
            _timers.Add(timer);
            _logger.Information("TimerTriggerScheduler: {TagCount} tag(s) on {Interval} cadence",
                paths.Length, interval);
        }
    }

    private void Tick(IReadOnlyList<string> paths)
    {
        if (_cts.IsCancellationRequested) return;
        foreach (var p in paths)
        {
            try
            {
                _engine.EvaluateOneAsync(p, _cts.Token).GetAwaiter().GetResult();
            }
            catch (OperationCanceledException)
            {
                return;
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "TimerTriggerScheduler evaluate failed for {Path}", p);
            }
        }
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        _cts.Cancel();
        foreach (var t in _timers)
        {
            try { t.Dispose(); } catch { }
        }
        _timers.Clear();
        _cts.Dispose();
    }
}
