using Microsoft.Extensions.Logging;

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

/// <summary>
///     Periodic re-evaluation scheduler for calc tags carrying a <c>timerIntervalMs</c>. A deliberate
///     mirror of the VirtualTag <c>TimerTriggerScheduler</c> (<c>Core.VirtualTags</c>): one
///     <see cref="System.Threading.Timer"/> per distinct interval group (so the wire count stays low
///     regardless of tag count) with a per-group in-flight flag that skips a tick when the prior tick for
///     the same group is still running. Revived <b>inside the driver</b> because the VT scheduler is welded
///     to <c>VirtualTagEngine.EvaluateOneAsync</c>; here each tick invokes the driver's own evaluate
///     callback per tag in the group.
/// </summary>
internal sealed class CalculationTimerScheduler : IDisposable
{
    private readonly Action<string> _evaluate;
    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;

    /// <summary>Initializes a new instance of the <see cref="CalculationTimerScheduler"/> class.</summary>
    /// <param name="evaluate">Callback invoked with a calc tag's RawPath when its timer group ticks.</param>
    /// <param name="logger">Logger for diagnostics.</param>
    public CalculationTimerScheduler(Action<string> evaluate, ILogger logger)
    {
        _evaluate = evaluate ?? throw new ArgumentNullException(nameof(evaluate));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

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

    /// <summary>Stand up one <see cref="Timer"/> per distinct interval. All tags sharing an interval share
    /// a timer; each tick re-evaluates every tag in the group.</summary>
    /// <param name="tags">The calc tags to schedule (only those with a positive <see cref="CalculationTagDefinition.TimerInterval"/> are scheduled).</param>
    public void Start(IEnumerable<CalculationTagDefinition> tags)
    {
        ArgumentNullException.ThrowIfNull(tags);
        if (_disposed) throw new ObjectDisposedException(nameof(CalculationTimerScheduler));

        var byInterval = tags
            .Where(t => t.TimerInterval is { } iv && iv > TimeSpan.Zero)
            .GroupBy(t => t.TimerInterval!.Value);

        foreach (var group in byInterval)
        {
            var paths = group.Select(t => t.RawPath).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.LogInformation("CalculationTimerScheduler: {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 — bounds the outstanding
        // work to one tick per group regardless of how long an evaluation takes.
        if (Interlocked.CompareExchange(ref ctx.InFlight, 1, 0) != 0)
        {
            Interlocked.Increment(ref _skippedTickCount);
            return;
        }

        try
        {
            foreach (var path in ctx.Paths)
            {
                if (_cts.IsCancellationRequested) return;
                try { _evaluate(path); }
                catch (Exception ex) { _logger.LogError(ex, "CalculationTimerScheduler evaluate failed for {Path}", path); }
            }
        }
        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 { /* best-effort teardown */ }
        }
        _timers.Clear();
        _groups.Clear();
        _cts.Dispose();
    }

    private sealed class TickGroup(IReadOnlyList<string> paths)
    {
        // 0 = idle, 1 = a tick is currently running for this group.
        public int InFlight;
        public IReadOnlyList<string> Paths { get; } = paths;
    }
}
