using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;

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

/// <summary>
/// Site Health <see cref="IKpiSampleSource"/> for the "KPI History &amp;
/// Trends" backbone. Unlike the other sample sources — which each query a
/// central repository — this one reads the in-memory
/// <see cref="ICentralHealthAggregator"/> state, snapshotting the latest
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport"/>
/// each tracked site has reported into per-site (<see cref="KpiScopes.Site"/>)
/// <see cref="KpiSample"/> rows.
/// </summary>
/// <remarks>
/// <para>
/// Each sampling pass the central recorder enumerates this source and calls
/// <see cref="CollectAsync"/>, which iterates
/// <see cref="ICentralHealthAggregator.GetAllSiteStates"/> and, for every site
/// whose <see cref="SiteHealthState.LatestReport"/> is non-null, emits the
/// fixed metric catalog below. Sites known only via heartbeats (null
/// <see cref="SiteHealthState.LatestReport"/>) contribute nothing.
/// </para>
/// <para>
/// The aggregator is registered as a singleton (its state is shared, immutable,
/// and safe to hand straight to callers); this source is nonetheless registered
/// DI-scoped for consistency with the recorder's per-tick scope and the other
/// sample sources. A scoped consumer resolving a singleton dependency is the
/// normal DI lifetime relationship and introduces no captive-dependency issue.
/// </para>
/// </remarks>
public sealed class SiteHealthKpiSampleSource : IKpiSampleSource
{
    // ── Metric catalog (the agreed metric-name strings for this source) ──
    // Declaration order matches the emission order in AddSiteSnapshot. 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 MetricConnectionsUp = "connectionsUp";
    private const string MetricConnectionsDown = KpiMetrics.SiteHealth.ConnectionsDown;
    private const string MetricScriptErrors = KpiMetrics.SiteHealth.ScriptErrors;
    private const string MetricAlarmEvalErrors = KpiMetrics.SiteHealth.AlarmEvalErrors;
    private const string MetricSfBufferDepth = KpiMetrics.SiteHealth.SfBufferDepth;
    private const string MetricDeadLetters = KpiMetrics.SiteHealth.DeadLetters;
    private const string MetricParkedMessages = "parkedMessages";
    private const string MetricDeployedInstances = "deployedInstances";
    private const string MetricEnabledInstances = "enabledInstances";
    private const string MetricDisabledInstances = "disabledInstances";
    private const string MetricAuditBacklogPending = "auditBacklogPending";
    private const string MetricEventLogWriteFailures = KpiMetrics.SiteHealth.EventLogWriteFailures;

    private readonly ICentralHealthAggregator _aggregator;

    /// <summary>
    /// Creates the sample source over the central in-memory health aggregator.
    /// </summary>
    /// <param name="aggregator">The central health aggregator holding per-site state.</param>
    public SiteHealthKpiSampleSource(ICentralHealthAggregator aggregator)
    {
        ArgumentNullException.ThrowIfNull(aggregator);
        _aggregator = aggregator;
    }

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

    /// <inheritdoc />
    public Task<IReadOnlyList<KpiSample>> CollectAsync(
        DateTime capturedAtUtc, CancellationToken cancellationToken = default)
    {
        var samples = new List<KpiSample>();

        foreach (var (siteId, state) in _aggregator.GetAllSiteStates())
        {
            // Sites known only via heartbeats (no report yet) contribute nothing.
            if (state.LatestReport is not { } report)
            {
                continue;
            }

            AddSiteSnapshot(samples, capturedAtUtc, siteId, report);
        }

        return Task.FromResult<IReadOnlyList<KpiSample>>(samples);
    }

    /// <summary>
    /// Appends the per-site metric catalog for one site's latest report,
    /// mapping each metric to the corresponding
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport"/>
    /// field.
    /// </summary>
    private static void AddSiteSnapshot(
        List<KpiSample> samples,
        DateTime capturedAtUtc,
        string siteId,
        ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport report)
    {
        // Connection statuses: Connected counts as up, everything else
        // (Disconnected / Connecting / Error) counts as down.
        var connectionsUp = 0;
        var connectionsDown = 0;
        foreach (var status in report.DataConnectionStatuses.Values)
        {
            if (status == ConnectionHealth.Connected)
            {
                connectionsUp++;
            }
            else
            {
                connectionsDown++;
            }
        }

        // Sum of the store-and-forward buffer depths across all buffers.
        long sfBufferDepth = 0;
        foreach (var depth in report.StoreAndForwardBufferDepths.Values)
        {
            sfBufferDepth += depth;
        }

        samples.Add(Sample(capturedAtUtc, MetricConnectionsUp, siteId, connectionsUp));
        samples.Add(Sample(capturedAtUtc, MetricConnectionsDown, siteId, connectionsDown));
        samples.Add(Sample(capturedAtUtc, MetricScriptErrors, siteId, report.ScriptErrorCount));
        samples.Add(Sample(capturedAtUtc, MetricAlarmEvalErrors, siteId, report.AlarmEvaluationErrorCount));
        samples.Add(Sample(capturedAtUtc, MetricSfBufferDepth, siteId, sfBufferDepth));
        samples.Add(Sample(capturedAtUtc, MetricDeadLetters, siteId, report.DeadLetterCount));
        samples.Add(Sample(capturedAtUtc, MetricParkedMessages, siteId, report.ParkedMessageCount));
        samples.Add(Sample(capturedAtUtc, MetricDeployedInstances, siteId, report.DeployedInstanceCount));
        samples.Add(Sample(capturedAtUtc, MetricEnabledInstances, siteId, report.EnabledInstanceCount));
        samples.Add(Sample(capturedAtUtc, MetricDisabledInstances, siteId, report.DisabledInstanceCount));
        // Nested audit-backlog object may be null (no data yet) — treat as 0.
        samples.Add(Sample(
            capturedAtUtc, MetricAuditBacklogPending, siteId,
            report.SiteAuditBacklog?.PendingCount ?? 0));
        samples.Add(Sample(
            capturedAtUtc, MetricEventLogWriteFailures, siteId, report.SiteEventLogWriteFailures));
    }

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