using System.Text.Json;
using System.Text.Json.Serialization;

namespace ZB.MOM.WW.Overview.Polling;

/// <summary>
/// The canonical <c>ZbHealthWriter</c> body every family app serves from <c>MapZbHealth</c>:
/// <c>{ status, totalDurationMs, entries: { name: { status, description, durationMs, data? } } }</c>.
/// </summary>
/// <remarks>
/// This mirrors a contract owned by <c>ZB.MOM.WW.Health</c>. It lives in the app for v1 by
/// deliberate decision (design §8.2): promoting it to a <c>ZB.MOM.WW.Health.Client</c> package is
/// worth a publish only once a second consumer exists.
/// </remarks>
public sealed class ZbHealthReport
{
    /// <summary>Aggregate tier status: <c>Healthy</c>, <c>Degraded</c> or <c>Unhealthy</c>.</summary>
    [JsonPropertyName("status")]
    public string? Status { get; init; }

    /// <summary>Total time the tier's checks took, in milliseconds.</summary>
    [JsonPropertyName("totalDurationMs")]
    public double TotalDurationMs { get; init; }

    /// <summary>Per-check results, keyed by check name (verbatim, e.g. <c>akka-cluster</c>).</summary>
    [JsonPropertyName("entries")]
    public Dictionary<string, ZbHealthEntry> Entries { get; init; } = new(StringComparer.Ordinal);
}

/// <summary>One check's result inside a <see cref="ZbHealthReport"/>.</summary>
public sealed class ZbHealthEntry
{
    /// <summary>This check's status: <c>Healthy</c>, <c>Degraded</c> or <c>Unhealthy</c>.</summary>
    [JsonPropertyName("status")]
    public string? Status { get; init; }

    /// <summary>Human-readable detail. Always present in the canonical shape; may be JSON null.</summary>
    [JsonPropertyName("description")]
    public string? Description { get; init; }

    /// <summary>How long this check took, in milliseconds.</summary>
    [JsonPropertyName("durationMs")]
    public double DurationMs { get; init; }

    /// <summary>
    /// Optional structured data the check published (ZB.MOM.WW.Health 0.2.0+). Absent entirely on
    /// checks that publish none and on every pre-0.2.0 node — hence nullable, and hence the
    /// dashboard must render fully (minus leader) without it.
    /// </summary>
    /// <remarks>
    /// Values stay as <see cref="JsonElement"/>: this is an open contract carrying arbitrary JSON,
    /// and binding it to concrete types here would break the moment a check adds a field.
    /// </remarks>
    [JsonPropertyName("data")]
    public Dictionary<string, JsonElement>? Data { get; init; }

    /// <summary>
    /// Reads <paramref name="key"/> from <see cref="Data"/> as a string, or null when the key is
    /// absent or is not a JSON string.
    /// </summary>
    /// <param name="key">The data key, matched verbatim.</param>
    /// <returns>The string value, or null.</returns>
    public string? DataString(string key)
    {
        if (Data is null || !Data.TryGetValue(key, out var value))
            return null;

        return value.ValueKind == JsonValueKind.String ? value.GetString() : null;
    }

    /// <summary>
    /// Reads <paramref name="key"/> from <see cref="Data"/> as an integer, or null when the key is
    /// absent or is not a JSON number.
    /// </summary>
    /// <param name="key">The data key, matched verbatim.</param>
    /// <returns>The integer value, or null.</returns>
    public int? DataInt(string key)
    {
        if (Data is null || !Data.TryGetValue(key, out var value))
            return null;

        return value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var parsed) ? parsed : null;
    }
}
