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;

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

/// <summary>
/// "KPI History &amp; Trends" — the
/// <see cref="IKpiSampleSource"/> for the <see cref="KpiSources.AuditLog"/> source.
/// The central recorder singleton enumerates this provider once per sampling pass
/// and persists its samples into the central <c>KpiSample</c> history table.
/// </summary>
/// <remarks>
/// <para>
/// Snapshots the Audit Log volume + error-rate counts the live Health-dashboard
/// "Audit" tiles already read (<see cref="IAuditLogRepository.GetKpiSnapshotAsync"/>)
/// over the same trailing <see cref="Window"/>, plus the global pending backlog the
/// snapshot carries. All three metrics are <see cref="KpiScopes.Global"/> only — the
/// snapshot is a system-wide aggregate with no per-site / per-node breakdown, so
/// <see cref="KpiSample.ScopeKey"/> is always <c>null</c>.
/// </para>
/// <para>
/// The trailing window is anchored on <c>capturedAtUtc</c> (passed as the snapshot's
/// <c>nowUtc</c>) so the sample reflects the same instant the recorder stamps every
/// other source with — deterministic across the whole sampling pass rather than the
/// repository's server-side <c>UtcNow</c> at call time.
/// </para>
/// </remarks>
public sealed class AuditLogKpiSampleSource : IKpiSampleSource
{
    /// <summary>
    /// Trailing window for the volume + error-rate aggregate. Fixed at 1 hour to
    /// match the live Audit KPI tiles (<c>AuditLogQueryService.KpiWindow</c>) and
    /// the <c>…LastHour</c> metric names below.
    /// </summary>
    private static readonly TimeSpan Window = TimeSpan.FromHours(1);

    // Metric catalog — the exact strings persisted in KpiSample.Metric. All three are
    // charted, so they share the public Commons catalog: source + UI trend page key off
    // one symbol. Stable identifiers; do not rename (values are persisted).
    private const string TotalEventsLastHourMetric = KpiMetrics.AuditLog.TotalEventsLastHour;
    private const string ErrorEventsLastHourMetric = KpiMetrics.AuditLog.ErrorEventsLastHour;
    private const string BacklogTotalMetric = KpiMetrics.AuditLog.BacklogTotal;

    private readonly IAuditLogRepository _repository;
    private readonly IAuditBacklogProvider? _backlogProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="AuditLogKpiSampleSource"/> class.
    /// </summary>
    /// <param name="repository">
    /// Append-only Audit Log repository — its <see cref="IAuditLogRepository.GetKpiSnapshotAsync"/>
    /// supplies the volume and error counts snapshotted here (its <c>BacklogTotal</c>
    /// is always zero — see <paramref name="backlogProvider"/>).
    /// </param>
    /// <param name="backlogProvider">
    /// Optional cross-site backlog aggregate. When registered (central node), the
    /// <c>backlogTotal</c> sample records its value; when absent (sites / unit tests,
    /// where MS.DI resolves the default) the source falls back to the snapshot's own
    /// <c>BacklogTotal</c> — i.e. zero, the previous default behavior. Injecting the seam
    /// keeps the append-only repo out of the in-memory health aggregator.
    /// </param>
    public AuditLogKpiSampleSource(
        IAuditLogRepository repository,
        IAuditBacklogProvider? backlogProvider = null)
    {
        ArgumentNullException.ThrowIfNull(repository);
        _repository = repository;
        _backlogProvider = backlogProvider;
    }

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

    /// <inheritdoc/>
    public async Task<IReadOnlyList<KpiSample>> CollectAsync(
        DateTime capturedAtUtc,
        CancellationToken cancellationToken = default)
    {
        var snapshot = await _repository
            .GetKpiSnapshotAsync(Window, nowUtc: capturedAtUtc, cancellationToken)
            .ConfigureAwait(false);

        // Defensive: a null snapshot would mean the repo broke its non-null
        // contract, but the source must degrade to "nothing to report" rather
        // than NRE the whole sampling pass.
        if (snapshot is null)
        {
            return [];
        }

        // Backlog: the provider's cross-site aggregate when registered (central),
        // otherwise the snapshot's own value (zero) as the site/test fallback.
        var backlogTotal = _backlogProvider?.GetPendingBacklogTotal() ?? snapshot.BacklogTotal;

        return
        [
            Sample(TotalEventsLastHourMetric, snapshot.TotalEventsLastHour, capturedAtUtc),
            Sample(ErrorEventsLastHourMetric, snapshot.ErrorEventsLastHour, capturedAtUtc),
            Sample(BacklogTotalMetric, backlogTotal, capturedAtUtc),
        ];
    }

    private static KpiSample Sample(string metric, double value, DateTime capturedAtUtc) => new()
    {
        Source = KpiSources.AuditLog,
        Metric = metric,
        Scope = KpiScopes.Global,
        ScopeKey = null,
        Value = value,
        CapturedAtUtc = capturedAtUtc,
    };
}
