namespace ZB.MOM.WW.Overview.Polling;

/// <summary>
/// Everything the dashboard knows about one instance, as of the last sweep that included it.
/// </summary>
/// <remarks>
/// Immutable by construction: the poller builds a whole new object graph each sweep and swaps it
/// into <see cref="OverviewSnapshotStore"/> atomically, so a render can never observe a half-updated
/// registry.
/// </remarks>
public sealed record InstanceSnapshot
{
    /// <summary>Owning application's display name.</summary>
    public required string Application { get; init; }

    /// <summary>This instance's display name, unique within its application.</summary>
    public required string Instance { get; init; }

    /// <summary>Cluster group, or null for a standalone instance.</summary>
    public string? Group { get; init; }

    /// <summary>The probed base URL, shown on the card for diagnosis.</summary>
    public required string BaseUrl { get; init; }

    /// <summary>Management UI link, when the registry supplies one.</summary>
    public string? ManagementUrl { get; init; }

    /// <summary>Label for <see cref="ManagementUrl"/>, from the application entry.</summary>
    public string ManagementLabel { get; init; } = "UI";

    /// <summary>Whether this instance is half of an active/standby pair.</summary>
    public bool HasActiveRole { get; init; }

    /// <summary>
    /// The damped, displayable status — null until the first sweep has actually probed this
    /// instance.
    /// </summary>
    /// <remarks>
    /// Null is a real, distinct state and not a stand-in for "Up": the registry is published to the
    /// store before the first probe returns so the page renders its full layout immediately, and
    /// showing an unmeasured instance as healthy for that half-second would be a fabricated reading.
    /// </remarks>
    public InstanceStatus? Status { get; init; }

    /// <summary>Active/standby state from the active tier.</summary>
    public ActiveState Active { get; init; } = ActiveState.NotApplicable;

    /// <summary>Consecutive failing observations, carried across sweeps to drive flap damping.</summary>
    public int ConsecutiveFailures { get; init; }

    /// <summary>
    /// When the sweep that produced this snapshot started; null before the first probe. Staleness
    /// is measured from here, so a stuck or paused poller ages every card even though no probe
    /// failed.
    /// </summary>
    public DateTimeOffset? LastPolledUtc { get; init; }

    /// <summary>When this instance last answered a probe at all, regardless of what it said.</summary>
    public DateTimeOffset? LastReachableUtc { get; init; }

    /// <summary>Wall-clock duration of the last ready probe.</summary>
    public TimeSpan Latency { get; init; }

    /// <summary>Why the last probe failed, when it did.</summary>
    public string? Error { get; init; }

    /// <summary>The last parsed ready-tier body, which drives the per-check detail list.</summary>
    public ZbHealthReport? Report { get; init; }

    /// <summary>This instance's effective staleness window.</summary>
    public TimeSpan StaleAfter { get; init; }

    /// <summary>Whether this snapshot is too old to be trusted as current.</summary>
    /// <param name="now">The current time.</param>
    /// <returns>True when the snapshot has aged past its staleness window.</returns>
    /// <remarks>
    /// Evaluated at render rather than stored, because staleness is a function of the clock and not
    /// of anything the poller observed — a snapshot that was fresh when written goes stale on its
    /// own while nothing at all happens.
    /// </remarks>
    public bool IsStale(DateTimeOffset now) =>
        Status is not null && LastPolledUtc is { } polled && now - polled > StaleAfter;
}

/// <summary>Leader state for one cluster group, derived from what its members report.</summary>
/// <param name="Name">The group name from the registry.</param>
/// <param name="LeaderInstance">
/// Registry name of the instance the reported leader address resolves to, or null when no member
/// reported a leader or the address matches no registered instance.
/// </param>
/// <param name="LeaderAddress">The reported Akka leader address, shown raw when unresolved.</param>
/// <param name="Disagreement">
/// True when reachable members reported different leaders — the split-brain tell.
/// </param>
/// <param name="ReportedLeaders">Every distinct address reported, for the disagreement detail.</param>
public sealed record GroupSnapshot(
    string Name,
    string? LeaderInstance,
    string? LeaderAddress,
    bool Disagreement,
    IReadOnlyList<string> ReportedLeaders);

/// <summary>One application section of the dashboard.</summary>
/// <param name="Name">The application's display name.</param>
/// <param name="Instances">Its instances, in registry order.</param>
/// <param name="Groups">Leader state per cluster group, in registry order.</param>
public sealed record ApplicationSnapshot(
    string Name,
    IReadOnlyList<InstanceSnapshot> Instances,
    IReadOnlyList<GroupSnapshot> Groups)
{
    /// <summary>Finds the group state for <paramref name="group"/>.</summary>
    /// <param name="group">The group name, or null for standalone instances.</param>
    /// <returns>The group snapshot, or null when there is none.</returns>
    public GroupSnapshot? GroupFor(string? group) =>
        group is null ? null : Groups.FirstOrDefault(g => string.Equals(g.Name, group, StringComparison.Ordinal));

    /// <summary>
    /// The most severe status across this application's probed instances, for the rollup pill;
    /// null while nothing has been probed yet.
    /// </summary>
    public InstanceStatus? Worst
    {
        get
        {
            InstanceStatus? worst = null;
            foreach (var instance in Instances)
            {
                if (instance.Status is not { } status)
                    continue;

                if (worst is null || StatusSeverity.Rank(status) > StatusSeverity.Rank(worst.Value))
                    worst = status;
            }

            return worst;
        }
    }
}

/// <summary>A complete, atomically-published view of the whole registry.</summary>
/// <param name="GeneratedUtc">When the sweep that produced this snapshot started.</param>
/// <param name="Applications">The applications, in registry order.</param>
public sealed record OverviewSnapshot(DateTimeOffset GeneratedUtc, IReadOnlyList<ApplicationSnapshot> Applications)
{
    /// <summary>The pre-registry snapshot served before the poller has published anything.</summary>
    public static OverviewSnapshot Empty { get; } = new(default, []);

    /// <summary>Every instance across every application, flattened.</summary>
    public IEnumerable<InstanceSnapshot> AllInstances => Applications.SelectMany(a => a.Instances);
}

/// <summary>Severity ordering for rollups.</summary>
public static class StatusSeverity
{
    /// <summary>Ranks a status; higher is worse.</summary>
    /// <param name="status">The status to rank.</param>
    /// <returns>The severity rank.</returns>
    /// <remarks>
    /// <see cref="InstanceStatus.Unreachable"/> outranks <see cref="InstanceStatus.Down"/> because
    /// it carries strictly less information: a Down instance told us what is wrong, an Unreachable
    /// one did not answer at all and could be anything up to and including gone.
    /// </remarks>
    public static int Rank(InstanceStatus status) => status switch
    {
        InstanceStatus.Up => 0,
        InstanceStatus.Degraded => 1,
        InstanceStatus.Down => 2,
        InstanceStatus.Unreachable => 3,
        _ => 3,
    };
}
