using System.Diagnostics.Metrics;
using ZB.MOM.WW.Overview.Polling;

namespace ZB.MOM.WW.Overview.Observability;

/// <summary>
/// The dashboard's own Meter: what it currently believes about every registered instance, and how
/// long its sweeps take.
/// </summary>
/// <remarks>
/// <para>
/// The instance gauge is <em>observable</em> — it is read from the published snapshot at scrape
/// time rather than written on every poll. The snapshot is already the single authority for what
/// the dashboard believes, so deriving the gauge from it makes a disagreement between the page and
/// the metric impossible by construction.
/// </para>
/// <para>
/// The instrument names follow the family convention in
/// <c>components/observability/spec/METRIC-CONVENTIONS.md</c> §2 — lower-case, dot-separated
/// <c>&lt;app&gt;.&lt;subsystem&gt;.&lt;event&gt;</c>, durations in seconds — so the Prometheus
/// exporter renders them as <c>overview_instance_status</c> and
/// <c>overview_poll_duration_seconds</c>.
/// </para>
/// </remarks>
public sealed class OverviewMetrics : IDisposable
{
    /// <summary>The app's single Meter name, per the family one-meter-per-app convention.</summary>
    public const string MeterName = "ZB.MOM.WW.Overview";

    /// <summary>Per-instance status gauge; the value is the <see cref="InstanceStatus"/> ordinal.</summary>
    public const string InstanceStatusInstrument = "overview.instance.status";

    /// <summary>Sweep-duration histogram, in seconds.</summary>
    public const string PollDurationInstrument = "overview.poll.duration";

    private readonly Meter _meter;
    private readonly Histogram<double> _pollDuration;
    private readonly OverviewSnapshotStore _store;

    /// <summary>Initializes the meter and its instruments.</summary>
    /// <param name="meterFactory">Factory that scopes the meter's lifetime to the container.</param>
    /// <param name="store">The snapshot the observable gauge reads at scrape time.</param>
    public OverviewMetrics(IMeterFactory meterFactory, OverviewSnapshotStore store)
    {
        ArgumentNullException.ThrowIfNull(meterFactory);

        _store = store ?? throw new ArgumentNullException(nameof(store));
        _meter = meterFactory.Create(MeterName);

        _meter.CreateObservableGauge(
            InstanceStatusInstrument,
            ObserveInstanceStatus,
            unit: "1",
            description: "Derived status of each registered instance (0=Up, 1=Degraded, 2=Down, 3=Unreachable).");

        _pollDuration = _meter.CreateHistogram<double>(
            PollDurationInstrument,
            unit: "s",
            description: "Wall-clock duration of one poll sweep across every due instance.");
    }

    /// <summary>Records the duration of one completed sweep.</summary>
    /// <param name="duration">Wall-clock time the sweep took.</param>
    public void RecordSweep(TimeSpan duration) => _pollDuration.Record(duration.TotalSeconds);

    /// <inheritdoc/>
    public void Dispose() => _meter.Dispose();

    private IEnumerable<Measurement<int>> ObserveInstanceStatus()
    {
        foreach (var instance in _store.Current.AllInstances)
        {
            // Instances that have never been probed are skipped rather than reported as anything.
            // Emitting a value for them would publish a status the dashboard does not yet hold —
            // and the enum's zero ordinal is Up, so the fabrication would be the reassuring one.
            if (instance.Status is not { } status)
                continue;

            yield return new Measurement<int>(
                (int)status,
                new KeyValuePair<string, object?>("application", instance.Application),

                // Deliberately NOT named "instance": Prometheus attaches its own `instance` label to
                // every scraped series, and a colliding metric label is silently renamed to
                // `exported_instance`, which no dashboard query would be written against.
                new KeyValuePair<string, object?>("node", instance.Instance),
                new KeyValuePair<string, object?>("group", instance.Group ?? string.Empty));
        }
    }
}
