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.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;

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

/// <summary>
/// Site Call Audit <see cref="IKpiSampleSource"/> for the "KPI History
/// &amp; Trends" backbone. Each sampling pass the central recorder enumerates this
/// source and calls <see cref="CollectAsync"/>, which snapshots the same
/// point-in-time <c>SiteCalls</c> KPIs the live Health-dashboard tiles show —
/// global, per-source-site, and per-originating-node — into flat
/// <see cref="KpiSample"/> rows.
/// </summary>
/// <remarks>
/// <para>
/// The cutoffs are derived from <see cref="SiteCallAuditOptions"/> exactly as the
/// live <c>SiteCallAuditActor</c> KPI handlers derive them
/// (<c>stuckCutoff = capturedAtUtc - StuckAgeThreshold</c>,
/// <c>intervalSince = capturedAtUtc - KpiInterval</c>). The COUNT metrics
/// (buffered, parked, failedLastInterval, deliveredLastInterval, stuck) 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. The recorder's <c>capturedAtUtc</c> is
/// the single anchor for both cutoffs.
/// </para>
/// <para>
/// Registered DI-scoped (next to the rest of the Site Call Audit composition) so
/// each sampling pass resolves a fresh repository scope, mirroring the actor's
/// scope-per-message repository access.
/// </para>
/// </remarks>
public sealed class SiteCallAuditKpiSampleSource : IKpiSampleSource
{
    // ── Metric catalog (the agreed metric-name strings for this source) ──
    // Declaration order matches the emission order in AddSnapshot. 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 MetricBuffered = KpiMetrics.SiteCallAudit.Buffered;
    private const string MetricParked = KpiMetrics.SiteCallAudit.Parked;
    private const string MetricFailedLastInterval = KpiMetrics.SiteCallAudit.FailedLastInterval;
    private const string MetricDeliveredLastInterval = KpiMetrics.SiteCallAudit.DeliveredLastInterval;
    private const string MetricStuck = "stuck";
    private const string MetricOldestPendingAgeSeconds = "oldestPendingAgeSeconds";

    private readonly ISiteCallAuditRepository _repository;
    private readonly SiteCallAuditOptions _options;

    /// <summary>
    /// Creates the sample source over the central <c>SiteCalls</c> repository and
    /// the Site Call Audit options that define the stuck-age + KPI-interval
    /// windows.
    /// </summary>
    /// <param name="repository">The central <c>SiteCalls</c> operational-state repository.</param>
    /// <param name="options">Site Call Audit windowing options (stuck-age + KPI interval).</param>
    public SiteCallAuditKpiSampleSource(
        ISiteCallAuditRepository repository,
        IOptions<SiteCallAuditOptions> options)
    {
        ArgumentNullException.ThrowIfNull(repository);
        ArgumentNullException.ThrowIfNull(options);

        _repository = repository;
        _options = options.Value;
    }

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

    /// <inheritdoc />
    public async Task<IReadOnlyList<KpiSample>> CollectAsync(
        DateTime capturedAtUtc, CancellationToken cancellationToken = default)
    {
        // Match the live SiteCallAuditActor KPI handlers: stuck cutoff and
        // interval window are both anchored on the single capture instant.
        var stuckCutoff = capturedAtUtc - _options.StuckAgeThreshold;
        var intervalSince = capturedAtUtc - _options.KpiInterval;

        var global = await _repository
            .ComputeKpisAsync(stuckCutoff, intervalSince, cancellationToken)
            .ConfigureAwait(false);
        var perSite = await _repository
            .ComputePerSiteKpisAsync(stuckCutoff, intervalSince, cancellationToken)
            .ConfigureAwait(false);
        var perNode = await _repository
            .ComputePerNodeKpisAsync(stuckCutoff, intervalSince, cancellationToken)
            .ConfigureAwait(false);

        var samples = new List<KpiSample>();

        // Global scope (null ScopeKey).
        AddSnapshot(
            samples, capturedAtUtc, KpiScopes.Global, scopeKey: null,
            global.BufferedCount, global.ParkedCount,
            global.FailedLastInterval, global.DeliveredLastInterval,
            global.OldestPendingAge, global.StuckCount);

        // Per-site scope (ScopeKey = source site id).
        foreach (var site in perSite)
        {
            AddSnapshot(
                samples, capturedAtUtc, KpiScopes.Site, scopeKey: site.SourceSite,
                site.BufferedCount, site.ParkedCount,
                site.FailedLastInterval, site.DeliveredLastInterval,
                site.OldestPendingAge, site.StuckCount);
        }

        // Per-node scope (ScopeKey = node name).
        foreach (var node in perNode)
        {
            AddSnapshot(
                samples, capturedAtUtc, KpiScopes.Node, scopeKey: node.SourceNode,
                node.BufferedCount, node.ParkedCount,
                node.FailedLastInterval, node.DeliveredLastInterval,
                node.OldestPendingAge, node.StuckCount);
        }

        return samples;
    }

    /// <summary>
    /// Appends the six-metric catalog for one snapshot at the given scope. The
    /// oldest-pending-age metric is omitted when the snapshot's age is
    /// <c>null</c> (no non-terminal rows) rather than written as zero.
    /// </summary>
    private void AddSnapshot(
        List<KpiSample> samples,
        DateTime capturedAtUtc,
        string scope,
        string? scopeKey,
        int buffered,
        int parked,
        int failedLastInterval,
        int deliveredLastInterval,
        TimeSpan? oldestPendingAge,
        int stuck)
    {
        samples.Add(Sample(capturedAtUtc, MetricBuffered, scope, scopeKey, buffered));
        samples.Add(Sample(capturedAtUtc, MetricParked, scope, scopeKey, parked));
        samples.Add(Sample(capturedAtUtc, MetricFailedLastInterval, scope, scopeKey, failedLastInterval));
        samples.Add(Sample(capturedAtUtc, MetricDeliveredLastInterval, scope, scopeKey, deliveredLastInterval));
        samples.Add(Sample(capturedAtUtc, MetricStuck, scope, scopeKey, stuck));

        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.SiteCallAudit,
            Metric = metric,
            Scope = scope,
            ScopeKey = scopeKey,
            Value = value,
            CapturedAtUtc = capturedAtUtc,
        };
}
