using System.Diagnostics;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.LoadHarness;

/// <summary>
/// Drives simulated tag value changes into a site's connections at a fixed nominal
/// rate, standing in for an OPC UA server's notification thread.
///
/// <para>
/// One driver task per data connection, because that mirrors production: each
/// <c>DataConnectionActor</c> has exactly one adapter feeding it, and every update
/// for that connection funnels through that actor's single mailbox. Sharding the
/// emitters differently would hide the per-connection-actor serialization point,
/// which is one of the things WP-4 needs to characterize.
/// </para>
/// <para>
/// The emitter walks the connection's tag list in slices sized so that one full pass
/// takes <see cref="HarnessConfig.TagUpdatePeriod"/>, then sleeps out the remainder
/// of each slice's budget. If a slice overruns its budget the driver does NOT try to
/// catch up — it records the shortfall in <see cref="EmitLagSeconds"/> so the results
/// can say honestly whether the offered load was actually delivered.
/// </para>
/// </summary>
public sealed class TagUpdateDriver : IAsyncDisposable
{
    private const int SlicesPerPeriod = 20;

    private readonly List<Task> _tasks = new();
    private readonly CancellationTokenSource _cts = new();
    private long _emitted;
    private long _skippedNoCallback;
    private long _lagTicks;

    /// <summary>Total tag value changes handed to adapter callbacks.</summary>
    public long EmittedCount => Interlocked.Read(ref _emitted);

    /// <summary>
    /// Emissions skipped because no Instance Actor had subscribed to that connection
    /// yet (the adapter callback is captured at subscribe time). Non-zero only during
    /// the ramp; a non-zero value in the steady window would mean lost offered load.
    /// </summary>
    public long SkippedNoCallback => Interlocked.Read(ref _skippedNoCallback);

    /// <summary>
    /// Cumulative seconds by which emit slices overran their time budget, summed
    /// across driver tasks. Large values mean the harness itself could not offer the
    /// nominal rate and the measured throughput is driver-bound, not system-bound.
    /// </summary>
    public double EmitLagSeconds => Interlocked.Read(ref _lagTicks) / (double)Stopwatch.Frequency;

    /// <summary>
    /// Starts one emitter task per connection across every site.
    /// </summary>
    /// <param name="sites">The sites to drive.</param>
    /// <param name="config">Harness configuration supplying the update period.</param>
    /// <returns>The running driver.</returns>
    public static TagUpdateDriver Start(IReadOnlyList<SiteRuntimeFixture> sites, HarnessConfig config)
    {
        var driver = new TagUpdateDriver();
        foreach (var site in sites)
        {
            for (var c = 0; c < site.TagPathsByConnection.Count; c++)
            {
                var connectionIndex = c;
                var tagPaths = site.TagPathsByConnection[connectionIndex].ToArray();
                driver._tasks.Add(Task.Run(() => driver.EmitLoopAsync(
                    site, connectionIndex, tagPaths, config.TagUpdatePeriod, driver._cts.Token)));
            }
        }

        return driver;
    }

    private async Task EmitLoopAsync(
        SiteRuntimeFixture site,
        int connectionIndex,
        string[] tagPaths,
        TimeSpan period,
        CancellationToken cancellationToken)
    {
        if (tagPaths.Length == 0)
            return;

        var sliceBudget = period / SlicesPerPeriod;
        var sliceSize = Math.Max(1, (int)Math.Ceiling(tagPaths.Length / (double)SlicesPerPeriod));
        var cursor = 0;
        var sequence = 0d;

        while (!cancellationToken.IsCancellationRequested)
        {
            var sliceStart = Stopwatch.GetTimestamp();
            var callback = ResolveCallback(site, connectionIndex);

            if (callback == null)
            {
                Interlocked.Add(ref _skippedNoCallback, sliceSize);
            }
            else
            {
                var end = Math.Min(cursor + sliceSize, tagPaths.Length);
                for (var i = cursor; i < end; i++)
                {
                    // Stamped HERE: this instant rides TagValueUpdate.Timestamp all the
                    // way to the subscriber, so the probe's subtraction is a genuine
                    // end-to-end latency and not a re-stamped approximation.
                    callback(tagPaths[i], new TagValue(sequence, QualityCode.Good, DateTimeOffset.UtcNow));
                }

                Interlocked.Add(ref _emitted, end - cursor);
                cursor = end;
            }

            if (cursor >= tagPaths.Length)
            {
                cursor = 0;
                sequence += 1d;
            }

            var elapsed = Stopwatch.GetTimestamp() - sliceStart;
            var budgetTicks = (long)(sliceBudget.TotalSeconds * Stopwatch.Frequency);
            if (elapsed < budgetTicks)
            {
                var remaining = TimeSpan.FromSeconds((budgetTicks - elapsed) / (double)Stopwatch.Frequency);
                try
                {
                    await Task.Delay(remaining, cancellationToken);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
            }
            else
            {
                Interlocked.Add(ref _lagTicks, elapsed - budgetTicks);
            }
        }
    }

    /// <summary>
    /// Resolves the live adapter callback for a connection, by NAME. The DCL factory
    /// appends adapters as its manager actor processes CreateConnectionCommands, so
    /// list position does not track connection index.
    /// </summary>
    private static SubscriptionCallback? ResolveCallback(SiteRuntimeFixture site, int connectionIndex)
    {
        var name = SiteRuntimeFixture.ConnectionName(connectionIndex);
        // Last match wins: a reconnect would create a fresh adapter for the same name,
        // and only the newest one holds the live subscription callback.
        return site.Connections.LastOrDefault(c => c.ConnectionName == name)?.ValueCallback;
    }

    private int _disposed;

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        if (Interlocked.Exchange(ref _disposed, 1) != 0)
            return;

        await _cts.CancelAsync();
        try
        {
            await Task.WhenAll(_tasks);
        }
        catch (OperationCanceledException)
        {
            // Expected on shutdown.
        }

        _cts.Dispose();
    }
}
