using Akka.Cluster;

namespace ZB.MOM.WW.Health.Akka;

/// <summary>
/// THE family-wide definition of "the active node": the <b>oldest Up member</b> of the cluster,
/// optionally scoped to a role — the member <c>ClusterSingletonManager</c> places singletons on.
/// </summary>
/// <remarks>
///     <para>
///         Cluster <b>leadership</b> (<c>ClusterState.Leader</c> / <c>RoleLeader</c>) is the wrong
///         answer and must never be used for a product-level active/standby decision. Leadership is
///         <i>address</i>-ordered (host, then port) and has no relationship to time; singleton
///         placement is <i>age</i>-ordered. On a freshly-formed cluster the two agree, which is why
///         the discrepancy hides in unit tests and in a rig that has never been restarted. They
///         diverge permanently after any restart: the restarted node rejoins as the youngest but
///         keeps its address, so if it holds the lower address it becomes leader while the
///         singletons — and all the work they own — stay on the other node.
///     </para>
///     <para>
///         Both Akka consumers in the family independently hit this and each wrote its own
///         replacement rather than use the shared leader-based check
///         (ScadaBridge <c>OldestNodeActiveHealthCheck</c>, OtOpcUa <c>ClusterPrimaryHealthCheck</c>).
///         This type is those two implementations promoted to one, so the health tier, the inbound
///         API gate, the store-and-forward delivery gate and OtOpcUa's OPC UA <c>ServiceLevel</c>
///         cannot drift apart about which node is in charge.
///     </para>
///     <para>
///         Only <see cref="MemberStatus.Up"/> members are eligible, matching singleton placement: a
///         <c>Leaving</c> node is handing its singletons over and must not be named active.
///     </para>
/// </remarks>
public static class ClusterActiveNode
{
    /// <summary>
    /// The oldest <see cref="MemberStatus.Up"/> member, optionally scoped to <paramref name="role"/>.
    /// </summary>
    /// <param name="members">The cluster members, in any order.</param>
    /// <param name="role">
    /// The role to scope the selection to, or <c>null</c> to consider every Up member.
    /// </param>
    /// <returns>The oldest eligible member, or <c>null</c> when none is Up.</returns>
    public static Member? OldestUpMember(IEnumerable<Member> members, string? role = null)
    {
        ArgumentNullException.ThrowIfNull(members);

        Member? oldest = null;
        foreach (var member in members)
        {
            if (member.Status != MemberStatus.Up) continue;
            if (role is not null && !member.HasRole(role)) continue;
            if (oldest is null || member.IsOlderThan(oldest)) oldest = member;
        }

        return oldest;
    }

    /// <summary>
    /// The oldest <see cref="MemberStatus.Up"/> member of <paramref name="cluster"/>, optionally
    /// scoped to <paramref name="role"/>.
    /// </summary>
    /// <param name="cluster">The Akka cluster to evaluate.</param>
    /// <param name="role">
    /// The role to scope the selection to, or <c>null</c> to consider every Up member.
    /// </param>
    /// <returns>The oldest eligible member, or <c>null</c> when none is Up.</returns>
    public static Member? OldestUpMember(Cluster cluster, string? role = null)
    {
        ArgumentNullException.ThrowIfNull(cluster);
        return OldestUpMember(cluster.State.Members, role);
    }

    /// <summary>
    /// Whether the local node is the active node — Up, and the oldest Up member in the role scope.
    /// </summary>
    /// <param name="cluster">The Akka cluster to evaluate.</param>
    /// <param name="role">
    /// The role to scope the selection to, or <c>null</c> to consider every Up member.
    /// </param>
    /// <returns>
    /// <c>true</c> when the local member is Up, carries <paramref name="role"/> when one is given,
    /// and no other eligible member is older.
    /// </returns>
    /// <remarks>
    /// Identity is compared by <see cref="Member.UniqueAddress"/>, not <c>Address</c>: a node that
    /// restarted on the same host and port is a different member, and during the overlap the cluster
    /// briefly holds both incarnations.
    /// </remarks>
    public static bool SelfIsActive(Cluster cluster, string? role = null)
    {
        ArgumentNullException.ThrowIfNull(cluster);

        var self = cluster.SelfMember;
        if (self.Status != MemberStatus.Up) return false;
        if (role is not null && !self.HasRole(role)) return false;

        var oldest = OldestUpMember(cluster.State.Members, role);
        return oldest is not null && oldest.UniqueAddress.Equals(self.UniqueAddress);
    }

    /// <summary>
    /// The first role in <paramref name="rolePreference"/> that <paramref name="selfRoles"/> carries
    /// — the role whose oldest Up member owns this node's active work.
    /// </summary>
    /// <param name="selfRoles">The local member's cluster roles.</param>
    /// <param name="rolePreference">
    /// Candidate roles in descending precedence, e.g. <c>["admin", "driver"]</c> for a node that
    /// answers for <c>admin</c> when it is fused and for <c>driver</c> when it is a driver-only node.
    /// </param>
    /// <returns>
    /// The first carried role, or <c>null</c> when the node carries none of them.
    /// </returns>
    /// <remarks>
    /// Precedence matters on a fused node: it must answer for the role its singletons — and the
    /// traffic being routed to it — are pinned to. Answering for a lower-precedence role could name a
    /// different node the moment a single-role member joins the mesh.
    /// </remarks>
    public static string? ResolveActiveRole(IEnumerable<string> selfRoles, IReadOnlyList<string> rolePreference)
    {
        ArgumentNullException.ThrowIfNull(selfRoles);
        ArgumentNullException.ThrowIfNull(rolePreference);

        if (rolePreference.Count == 0) return null;

        var roles = selfRoles as IReadOnlyCollection<string> ?? selfRoles.ToList();
        foreach (var candidate in rolePreference)
        {
            if (roles.Contains(candidate)) return candidate;
        }

        return null;
    }
}
