using Akka.Actor;
using Akka.Cluster;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace ZB.MOM.WW.Health.Akka;

/// <summary>
/// Health check that maps the local node's Akka cluster membership status to a
/// <see cref="HealthStatus"/> through a configurable <see cref="AkkaClusterStatusPolicy"/>.
/// Register to the <see cref="ZbHealthTags.Ready"/> tag only — cluster membership is a readiness
/// concern; the <see cref="ZbHealthTags.Active"/> tier is reserved for the leader / active-node probe.
/// </summary>
/// <remarks>
/// The <see cref="ActorSystem"/> is resolved lazily from the service provider. If it is not yet
/// available — e.g. during startup before Akka is initialised — the check returns
/// <see cref="HealthStatus.Degraded"/> rather than throwing, so it is safe to register before Akka
/// is fully up.
/// </remarks>
public sealed class AkkaClusterHealthCheck : IHealthCheck
{
    private readonly IServiceProvider _serviceProvider;
    private readonly AkkaClusterStatusPolicy _policy;

    /// <summary>Initializes a new <see cref="AkkaClusterHealthCheck"/>.</summary>
    /// <param name="serviceProvider">
    /// The application service provider. The <see cref="ActorSystem"/> is resolved lazily so the
    /// check is startup-safe: if no <see cref="ActorSystem"/> is registered yet the result is Degraded.
    /// </param>
    /// <param name="policy">The status-to-health mapping policy to apply.</param>
    public AkkaClusterHealthCheck(IServiceProvider serviceProvider, AkkaClusterStatusPolicy policy)
    {
        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        _policy = policy ?? throw new ArgumentNullException(nameof(policy));
    }

    /// <inheritdoc />
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var system = _serviceProvider.GetService<ActorSystem>();
        if (system is null)
            return Task.FromResult(HealthCheckResult.Degraded("ActorSystem not yet available."));

        MemberStatus status;
        IReadOnlyDictionary<string, object> data;
        try
        {
            // Cluster.Get(system).SelfMember can throw while the ActorSystem exists but the cluster
            // has not finished initialising (e.g. Akka.Cluster not yet configured →
            // ConfigurationException). The spec's startup-safety rule maps this to Degraded, not an
            // escaping exception (which the host would record as Unhealthy and pull the node from
            // rotation).
            var cluster = Cluster.Get(system);
            status = cluster.SelfMember.Status;
            data = BuildClusterData(cluster);
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            return Task.FromResult(HealthCheckResult.Degraded("Akka cluster state not yet accessible.", ex));
        }

        var health = _policy.Evaluate(status);
        var description = $"Akka cluster member status: {status}";
        var result = health switch
        {
            HealthStatus.Healthy   => HealthCheckResult.Healthy(description, data),
            HealthStatus.Degraded  => HealthCheckResult.Degraded(description, data: data),
            _                      => HealthCheckResult.Unhealthy(description, data: data),
        };
        return Task.FromResult(result);
    }

    /// <summary>
    /// Projects this node's view of the cluster into the check's <c>data</c> dictionary.
    /// </summary>
    /// <param name="cluster">The cluster extension for the local <see cref="ActorSystem"/>.</param>
    /// <returns>
    /// JSON-friendly values only (string / int / string[]), so the canonical health writer can emit
    /// them verbatim: <c>leader</c> (omitted while the leader is unknown, e.g. pre-join or during
    /// convergence), <c>selfAddress</c>, <c>selfRoles</c> (sorted for a stable payload),
    /// <c>memberCount</c>, <c>unreachableCount</c>.
    /// </returns>
    /// <remarks>
    /// This is deliberately each node's <em>own</em> view rather than an authoritative fleet answer —
    /// a consumer that reads it from several members of the same cluster can compare them, and a
    /// disagreement is a split-brain tell.
    /// </remarks>
    internal static IReadOnlyDictionary<string, object> BuildClusterData(Cluster cluster)
    {
        ArgumentNullException.ThrowIfNull(cluster);

        var state = cluster.State;
        var data = new Dictionary<string, object>(StringComparer.Ordinal)
        {
            ["selfAddress"] = cluster.SelfAddress.ToString(),
            ["selfRoles"] = cluster.SelfMember.Roles.OrderBy(static r => r, StringComparer.Ordinal).ToArray(),
            ["memberCount"] = state.Members.Count,
            ["unreachableCount"] = state.Unreachable.Count,
        };

        // Omitted rather than emitted as null: a missing key reads as "this node does not know yet",
        // which is exactly the pre-join / mid-convergence state.
        var leader = state.Leader;
        if (leader is not null)
            data["leader"] = leader.ToString();

        return data;
    }
}
