namespace ZB.MOM.WW.ScadaBridge.LoadHarness.Metrics;

/// <summary>
/// Lock-free logarithmic latency histogram sized for tens of thousands of samples
/// per second across many threads.
///
/// <para>
/// Buckets are 16-per-octave over microseconds, i.e. bucket <c>i</c> covers
/// <c>[2^(i/16), 2^((i+1)/16))</c> µs. That bounds relative bucket width at
/// <c>2^(1/16) - 1 ≈ 4.4%</c>, so a reported percentile is within ~4.4% of the true
/// value — ample for the millisecond-scale thresholds this harness asserts, and far
/// cheaper than retaining 45 million raw samples.
/// </para>
/// <para>
/// Recording is a <see cref="Math.Log2(double)"/> plus one
/// <see cref="Interlocked.Increment(ref long)"/>; there is no allocation on the hot path.
/// </para>
/// </summary>
public sealed class LatencyHistogram
{
    private const int SubBucketsPerOctave = 16;
    private const int BucketCount = 64 * SubBucketsPerOctave;

    private readonly long[] _buckets = new long[BucketCount];
    private long _count;
    private long _totalMicroseconds;
    private long _maxMicroseconds;

    /// <summary>Number of samples recorded.</summary>
    public long Count => Interlocked.Read(ref _count);

    /// <summary>Largest sample seen, in microseconds (exact — not bucketed).</summary>
    public double MaxMs => Interlocked.Read(ref _maxMicroseconds) / 1000.0;

    /// <summary>Arithmetic mean in milliseconds (exact — accumulated, not bucketed).</summary>
    public double MeanMs
    {
        get
        {
            var count = Interlocked.Read(ref _count);
            return count == 0 ? 0 : Interlocked.Read(ref _totalMicroseconds) / 1000.0 / count;
        }
    }

    /// <summary>
    /// Records one sample. Negative durations (clock skew across the emit/receive
    /// boundary) are clamped to zero rather than discarded, so the sample count stays
    /// an honest denominator.
    /// </summary>
    /// <param name="elapsed">The measured latency.</param>
    public void Record(TimeSpan elapsed)
    {
        var micros = (long)(elapsed.TotalMilliseconds * 1000.0);
        if (micros < 0) micros = 0;

        Interlocked.Increment(ref _count);
        Interlocked.Add(ref _totalMicroseconds, micros);

        long observedMax;
        while (micros > (observedMax = Interlocked.Read(ref _maxMicroseconds)))
        {
            if (Interlocked.CompareExchange(ref _maxMicroseconds, micros, observedMax) == observedMax)
                break;
        }

        Interlocked.Increment(ref _buckets[BucketIndex(micros)]);
    }

    private static int BucketIndex(long micros)
    {
        if (micros <= 0) return 0;
        var index = (int)(Math.Log2(micros) * SubBucketsPerOctave);
        if (index < 0) return 0;
        return index >= BucketCount ? BucketCount - 1 : index;
    }

    /// <summary>Bucket midpoint in milliseconds, used when reconstructing a percentile.</summary>
    private static double BucketMidpointMs(int index)
    {
        var low = Math.Pow(2, (double)index / SubBucketsPerOctave);
        var high = Math.Pow(2, (double)(index + 1) / SubBucketsPerOctave);
        return (low + high) / 2.0 / 1000.0;
    }

    /// <summary>
    /// Returns the requested percentile in milliseconds, or 0 when no samples were recorded.
    /// </summary>
    /// <param name="percentile">Percentile in the range 0..100 (e.g. 99 for P99).</param>
    /// <returns>The percentile value in milliseconds.</returns>
    public double PercentileMs(double percentile)
    {
        var total = Interlocked.Read(ref _count);
        if (total == 0) return 0;

        var target = (long)Math.Ceiling(total * percentile / 100.0);
        if (target < 1) target = 1;

        long cumulative = 0;
        for (var i = 0; i < BucketCount; i++)
        {
            cumulative += Interlocked.Read(ref _buckets[i]);
            if (cumulative >= target)
                return BucketMidpointMs(i);
        }

        return MaxMs;
    }

    /// <summary>Materializes the standard percentile set plus mean/max/count for reporting.</summary>
    /// <returns>A snapshot record of this histogram.</returns>
    public LatencySnapshot Snapshot() => new(
        Count,
        MeanMs,
        PercentileMs(50),
        PercentileMs(95),
        PercentileMs(99),
        PercentileMs(99.9),
        MaxMs);
}

/// <summary>Point-in-time summary of a <see cref="LatencyHistogram"/>. All times in milliseconds.</summary>
/// <param name="Count">Samples recorded.</param>
/// <param name="MeanMs">Arithmetic mean.</param>
/// <param name="P50Ms">Median.</param>
/// <param name="P95Ms">95th percentile.</param>
/// <param name="P99Ms">99th percentile.</param>
/// <param name="P999Ms">99.9th percentile.</param>
/// <param name="MaxMs">Largest observed sample.</param>
public sealed record LatencySnapshot(
    long Count,
    double MeanMs,
    double P50Ms,
    double P95Ms,
    double P99Ms,
    double P999Ms,
    double MaxMs);
