namespace ZB.MOM.WW.Overview.Polling;

/// <summary>The derived state of one probed instance.</summary>
public enum InstanceStatus
{
    /// <summary>Ready tier answered 200 / <c>Healthy</c>.</summary>
    Up,

    /// <summary>Ready tier answered 200 / <c>Degraded</c> — serving, but a check is unhappy.</summary>
    Degraded,

    /// <summary>
    /// Ready tier answered 503 / <c>Unhealthy</c>. The app is up and talking; a dependency is not.
    /// </summary>
    Down,

    /// <summary>
    /// Nothing answered — timeout, connection refused, DNS failure, or a body that is not the
    /// canonical health JSON. Kept distinct from <see cref="Down"/> because it usually means
    /// network / VPN / registry typo rather than a sick application, and the two lead an operator
    /// to look in completely different places.
    /// </summary>
    Unreachable,
}

/// <summary>Whether a probed instance is the active half of its pair.</summary>
public enum ActiveState
{
    /// <summary>The instance has no active/standby concept (single-instance gateways).</summary>
    NotApplicable,

    /// <summary>The active probe did not answer, so activity is unknown.</summary>
    Unknown,

    /// <summary>Active tier answered 200 — this is the active node.</summary>
    Active,

    /// <summary>Active tier answered 503 — this is the standby.</summary>
    Standby,
}

/// <summary>
/// Pure status derivation: probe result → <see cref="InstanceStatus"/>, with two-strike flap
/// damping applied across polls. Static and dependency-free so every rule is table-testable.
/// </summary>
public static class StatusDerivation
{
    /// <summary>
    /// Consecutive failing observations required before a previously-good instance is shown as
    /// failing. A single blip — one dropped packet, one GC pause past the timeout — is not news;
    /// two in a row is.
    /// </summary>
    public const int FailureStrikes = 2;

    /// <summary>Classifies one ready-tier probe, before any damping.</summary>
    /// <param name="probe">The probe result.</param>
    /// <returns>The observed status.</returns>
    public static InstanceStatus Observe(HealthProbeResult probe)
    {
        ArgumentNullException.ThrowIfNull(probe);

        if (!probe.Reachable || probe.Report is null)
            return InstanceStatus.Unreachable;

        // The reported status wins over the HTTP code: they agree by construction (Healthy/Degraded
        // → 200, Unhealthy → 503), and the body is the more specific of the two — 200 alone cannot
        // tell Up from Degraded.
        return probe.Report.Status switch
        {
            "Healthy" => InstanceStatus.Up,
            "Degraded" => InstanceStatus.Degraded,
            "Unhealthy" => InstanceStatus.Down,

            // Parsed as JSON but carrying no status we recognise: the endpoint is answering with
            // something that is not this contract, which is the Unreachable case, not a sick app.
            _ => InstanceStatus.Unreachable,
        };
    }

    /// <summary>Classifies one active-tier probe.</summary>
    /// <param name="hasActiveRole">Whether this instance participates in an active/standby pair.</param>
    /// <param name="probe">The active-tier probe result, or null when not probed.</param>
    /// <returns>The derived active state.</returns>
    public static ActiveState ObserveActive(bool hasActiveRole, HealthProbeResult? probe)
    {
        if (!hasActiveRole)
            return ActiveState.NotApplicable;

        if (probe is null || !probe.Reachable || probe.StatusCode is null)
            return ActiveState.Unknown;

        return probe.StatusCode switch
        {
            200 => ActiveState.Active,
            503 => ActiveState.Standby,
            _ => ActiveState.Unknown,
        };
    }

    /// <summary>
    /// Applies two-strike damping: a failing observation only replaces a previously-good state
    /// after <see cref="FailureStrikes"/> consecutive failures, while any good observation is
    /// adopted immediately.
    /// </summary>
    /// <param name="previous">The last displayed status, or null on the first-ever poll.</param>
    /// <param name="previousConsecutiveFailures">Consecutive failures recorded before this poll.</param>
    /// <param name="observed">This poll's observed status.</param>
    /// <returns>The status to display and the updated consecutive-failure count.</returns>
    /// <remarks>
    /// Recovery is deliberately asymmetric — instant. Damping exists to suppress false alarms, and
    /// making recovery equally slow would just convert them into false alarms that linger.
    /// </remarks>
    public static (InstanceStatus Status, int ConsecutiveFailures) Damp(
        InstanceStatus? previous,
        int previousConsecutiveFailures,
        InstanceStatus observed)
    {
        var failing = observed is InstanceStatus.Down or InstanceStatus.Unreachable;

        if (!failing)
            return (observed, 0);

        var failures = previousConsecutiveFailures + 1;

        // With no previous state there is nothing to protect and nothing better to show: adopt the
        // observation. Claiming Up on a first poll that failed would be a fabrication.
        if (previous is null || failures >= FailureStrikes)
            return (observed, failures);

        return (previous.Value, failures);
    }
}
