using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Overview.Registry;

namespace ZB.MOM.WW.Overview.Observability;

/// <summary>
/// Fails when the poller has stopped sweeping — the one failure mode that would otherwise leave the
/// dashboard looking perfectly healthy while every card it serves quietly went stale.
/// </summary>
/// <remarks>
/// This is what makes the dashboard's own <c>/health/ready</c> honest. Without it the process
/// answers 200 for as long as Kestrel is up, which says nothing about whether the data behind the
/// page is still being refreshed.
/// </remarks>
public sealed class PollCycleHealthCheck : IHealthCheck
{
    private readonly PollCycleHeartbeat _heartbeat;
    private readonly TimeProvider _timeProvider;
    private readonly TimeSpan _grace;

    /// <summary>Initializes a new <see cref="PollCycleHealthCheck"/>.</summary>
    /// <param name="heartbeat">The poller's completed-sweep heartbeat.</param>
    /// <param name="options">The registry, used to derive the tolerated gap.</param>
    /// <param name="timeProvider">Clock.</param>
    public PollCycleHealthCheck(
        PollCycleHeartbeat heartbeat,
        IOptions<OverviewOptions> options,
        TimeProvider timeProvider)
    {
        ArgumentNullException.ThrowIfNull(options);

        _heartbeat = heartbeat ?? throw new ArgumentNullException(nameof(heartbeat));
        _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
        _grace = GraceFor(options.Value);
    }

    /// <summary>
    /// The longest gap between completed sweeps that is still considered healthy.
    /// </summary>
    /// <param name="options">The registry.</param>
    /// <returns>The tolerated gap.</returns>
    /// <remarks>
    /// A sweep completes on every timer tick, and the timer ticks at the fastest configured
    /// interval — so the expected gap is one interval plus however long the probes took, which is
    /// bounded by the slowest configured timeout. Twice that is comfortably past "slow" and
    /// unambiguously "stopped".
    /// </remarks>
    internal static TimeSpan GraceFor(OverviewOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);

        var timings = options.Applications
            .SelectMany(app => app.Instances.Select(instance =>
                EffectiveTimings.Resolve(options, app, instance)))
            .ToList();

        // An empty registry cannot poll at all; RegistryLoadedHealthCheck is the check that reports
        // that, so fall back to the global defaults rather than dividing by an empty set here.
        var interval = timings.Count > 0
            ? timings.Min(t => t.PollInterval)
            : TimeSpan.FromSeconds(options.PollIntervalSeconds);
        var timeout = timings.Count > 0
            ? timings.Max(t => t.Timeout)
            : TimeSpan.FromSeconds(options.TimeoutSeconds);

        return 2 * (interval + timeout);
    }

    /// <inheritdoc/>
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var now = _timeProvider.GetUtcNow();
        var last = _heartbeat.LastCompletedUtc;

        var data = new Dictionary<string, object>(StringComparer.Ordinal)
        {
            ["graceSeconds"] = Math.Round(_grace.TotalSeconds, 3),
        };

        if (last is null)
        {
            // Nothing has swept yet. Within the grace window that is ordinary startup, so report
            // Degraded (still a 200) rather than failing readiness for the first few seconds of
            // every deployment. Past it, the poller never started and that IS a failure.
            var waiting = now - _heartbeat.StartedUtc;
            data["waitingSeconds"] = Math.Round(waiting.TotalSeconds, 3);

            return Task.FromResult(waiting <= _grace
                ? HealthCheckResult.Degraded("No poll cycle has completed yet; the poller is still starting.", data: data)
                : HealthCheckResult.Unhealthy(
                    $"No poll cycle has completed in {waiting.TotalSeconds:0.#}s since startup.",
                    exception: null,
                    data));
        }

        var age = now - last.Value;
        data["lastCycleUtc"] = last.Value.ToString("O", System.Globalization.CultureInfo.InvariantCulture);
        data["ageSeconds"] = Math.Round(age.TotalSeconds, 3);

        return Task.FromResult(age <= _grace
            ? HealthCheckResult.Healthy($"Last poll cycle completed {age.TotalSeconds:0.#}s ago.", data)
            : HealthCheckResult.Unhealthy(
                $"No poll cycle has completed in {age.TotalSeconds:0.#}s; the displayed data is stale.",
                exception: null,
                data));
    }
}
