using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications;

namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox.Kpi;

/// <summary>
/// <see cref="IKpiSampleSource"/> for the Notification Outbox: snapshots the same
/// point-in-time delivery KPIs the live Health-dashboard tiles surface — queue depth, stuck
/// count, parked count, delivered-last-interval, and oldest-pending age — at every sampling
/// pass of the central KPI-history recorder ("KPI History &amp; Trends").
/// </summary>
/// <remarks>
/// <para>
/// Computes the same cutoffs the live KPI handlers in
/// <see cref="NotificationOutboxActor"/> use, anchored on the recorder's shared
/// <c>capturedAtUtc</c> rather than wall-clock <c>now</c>: the stuck cutoff is
/// <c>capturedAtUtc - <see cref="NotificationOutboxOptions.StuckAgeThreshold"/></c> and the
/// delivered window is <c>capturedAtUtc - <see cref="NotificationOutboxOptions.DeliveredKpiWindow"/></c>.
/// The COUNT metrics (queueDepth, stuckCount, parkedCount, deliveredLastInterval) equal the
/// live tile at the same instant; the <c>oldestPendingAgeSeconds</c> metric is computed against
/// the repository's internal clock and may differ from the live tile by the query-execution latency.
/// </para>
/// <para>
/// Emits Global (<c>ScopeKey == null</c>), per-Site (<c>ScopeKey == SourceSiteId</c>), and
/// per-Node (<c>ScopeKey == SourceNode</c>) samples, mirroring the repository's three KPI
/// computation methods. The oldest-pending age (a <see cref="TimeSpan"/>?) maps to
/// <c>oldestPendingAgeSeconds</c> via <see cref="TimeSpan.TotalSeconds"/>; that one metric is
/// omitted when the age is <c>null</c>.
/// </para>
/// </remarks>
public sealed class NotificationOutboxKpiSampleSource : IKpiSampleSource
{
    // Charted metrics share the public Commons catalog so source + UI trend page key
    // off one symbol. The uncharted internal metrics stay private here.
    private const string MetricQueueDepth = KpiMetrics.NotificationOutbox.QueueDepth;
    private const string MetricStuckCount = KpiMetrics.NotificationOutbox.StuckCount;
    private const string MetricParkedCount = KpiMetrics.NotificationOutbox.ParkedCount;
    private const string MetricDeliveredLastInterval = KpiMetrics.NotificationOutbox.DeliveredLastInterval;
    private const string MetricOldestPendingAgeSeconds = KpiMetrics.NotificationOutbox.OldestPendingAgeSeconds;

    private readonly INotificationOutboxRepository _repository;
    private readonly NotificationOutboxOptions _options;

    /// <summary>
    /// Creates the sample source.
    /// </summary>
    /// <param name="repository">Outbox repository providing the KPI computation methods.</param>
    /// <param name="options">Outbox options carrying the stuck-age and delivered-window cutoffs.</param>
    public NotificationOutboxKpiSampleSource(
        INotificationOutboxRepository repository,
        IOptions<NotificationOutboxOptions> options)
    {
        ArgumentNullException.ThrowIfNull(repository);
        ArgumentNullException.ThrowIfNull(options);

        _repository = repository;
        _options = options.Value;
    }

    /// <inheritdoc />
    public string Source => KpiSources.NotificationOutbox;

    /// <inheritdoc />
    public async Task<IReadOnlyList<KpiSample>> CollectAsync(
        DateTime capturedAtUtc, CancellationToken cancellationToken = default)
    {
        // Anchor the live KPI cutoffs on the recorder's shared capture instant, so a sample
        // captured at the same moment equals the live Health-dashboard tile.
        var capturedAt = new DateTimeOffset(capturedAtUtc, TimeSpan.Zero);
        var stuckCutoff = capturedAt - _options.StuckAgeThreshold;
        var deliveredSince = capturedAt - _options.DeliveredKpiWindow;

        var samples = new List<KpiSample>();

        var global = await _repository.ComputeKpisAsync(stuckCutoff, deliveredSince, cancellationToken)
            .ConfigureAwait(false);
        AddSnapshot(
            samples, capturedAtUtc, KpiScopes.Global, scopeKey: null,
            global.QueueDepth, global.StuckCount, global.ParkedCount,
            global.DeliveredLastInterval, global.OldestPendingAge);

        var perSite = await _repository.ComputePerSiteKpisAsync(stuckCutoff, deliveredSince, cancellationToken)
            .ConfigureAwait(false);
        foreach (var site in perSite)
        {
            AddSnapshot(
                samples, capturedAtUtc, KpiScopes.Site, site.SourceSiteId,
                site.QueueDepth, site.StuckCount, site.ParkedCount,
                site.DeliveredLastInterval, site.OldestPendingAge);
        }

        var perNode = await _repository.ComputePerNodeKpisAsync(stuckCutoff, deliveredSince, cancellationToken)
            .ConfigureAwait(false);
        foreach (var node in perNode)
        {
            AddSnapshot(
                samples, capturedAtUtc, KpiScopes.Node, node.SourceNode,
                node.QueueDepth, node.StuckCount, node.ParkedCount,
                node.DeliveredLastInterval, node.OldestPendingAge);
        }

        return samples;
    }

    /// <summary>
    /// Appends the five outbox metrics for one snapshot at the given scope, omitting
    /// <c>oldestPendingAgeSeconds</c> when <paramref name="oldestPendingAge"/> is <c>null</c>.
    /// </summary>
    private void AddSnapshot(
        List<KpiSample> samples,
        DateTime capturedAtUtc,
        string scope,
        string? scopeKey,
        int queueDepth,
        int stuckCount,
        int parkedCount,
        int deliveredLastInterval,
        TimeSpan? oldestPendingAge)
    {
        samples.Add(Sample(capturedAtUtc, MetricQueueDepth, scope, scopeKey, queueDepth));
        samples.Add(Sample(capturedAtUtc, MetricStuckCount, scope, scopeKey, stuckCount));
        samples.Add(Sample(capturedAtUtc, MetricParkedCount, scope, scopeKey, parkedCount));
        samples.Add(Sample(capturedAtUtc, MetricDeliveredLastInterval, scope, scopeKey, deliveredLastInterval));

        if (oldestPendingAge is { } age)
        {
            samples.Add(Sample(capturedAtUtc, MetricOldestPendingAgeSeconds, scope, scopeKey, age.TotalSeconds));
        }
    }

    private KpiSample Sample(
        DateTime capturedAtUtc, string metric, string scope, string? scopeKey, double value) =>
        new()
        {
            Source = KpiSources.NotificationOutbox,
            Metric = metric,
            Scope = scope,
            ScopeKey = scopeKey,
            Value = value,
            CapturedAtUtc = capturedAtUtc,
        };
}
