using ZB.MOM.WW.OtOpcUa.Commons.Messages.Redundancy;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

/// <summary>
///     Single decision point for the Primary data-plane gates (inbound device writes, native alarm acks,
///     fleet-wide alerts publishes). A KNOWN role wins outright; an UNKNOWN role (no redundancy snapshot
///     yet, or the snapshot never mentioned this node — the 03/S5 identity-mismatch shape) is resolved by
///     cluster membership: a single-driver cluster stays default-ALLOW (no peer exists — the
///     boot-window / single-node posture), a multi-driver cluster is default-DENY (a real Primary peer
///     exists; do not touch the shared field device until the redundancy snapshot proves this node is it).
///     Fixes archreview 03/S4 (primary gate defaulted to allow while role unknown → dual-primary window).
/// </summary>
public static class PrimaryGatePolicy
{
    /// <summary>Decide whether this node should service a Primary-only data-plane operation.</summary>
    /// <param name="localRole">This node's last-known redundancy role, or <c>null</c> when unknown
    /// (no snapshot received yet, or the snapshot omitted this node).</param>
    /// <param name="driverMemberCount">Count of Up cluster members carrying the <c>driver</c> role.</param>
    /// <returns><c>true</c> to service as Primary; <c>false</c> to deny.</returns>
    public static bool ShouldServiceAsPrimary(RedundancyRole? localRole, int driverMemberCount) =>
        localRole switch
        {
            RedundancyRole.Primary => true,
            RedundancyRole.Secondary or RedundancyRole.Detached => false,
            _ => driverMemberCount <= 1,   // unknown: allow only when no driver peer exists
        };

    /// <summary>
    ///     Decide whether this node should drain the replicated alarm store-and-forward queue.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b>Deliberately not <see cref="ShouldServiceAsPrimary"/>.</b> That gate protects a shared
    ///         field device, where two nodes acting at once is dangerous and irreversible, so an unknown
    ///         role with a peer present must deny. This gate protects an append-only historian, and the
    ///         delivery contract is already <i>at-least-once across a failover, by design</i> — identical
    ///         events even collapse to a single row, because ids hash the payload.
    ///     </para>
    ///     <para>
    ///         The two error costs are therefore asymmetric: a false allow costs a duplicate history row;
    ///         a false deny silently stops the alarm audit trail and eventually evicts it at the capacity
    ///         wall. So this decision keys on the role <b>alone</b> and opens when the role is unknown —
    ///         no membership tie-break, because cluster-wide driver count says nothing about whether
    ///         <i>this</i> node has a redundant partner.
    ///     </para>
    ///     <para>
    ///         Found by the LocalDb Phase 2 live gate: on a rig of four driver nodes in one cluster with
    ///         no <c>Redundancy</c> section, borrowing the write gate's verdict suspended the drain on
    ///         every node — including unpaired ones — so nothing drained anywhere.
    ///     </para>
    /// </remarks>
    /// <param name="localRole">This node's last-known redundancy role, or <c>null</c> when unknown.</param>
    /// <param name="queueSharingPeerIsPrimary">
    ///     Whether the node holding <see cref="RedundancyRole.Primary"/> in the same snapshot is a node
    ///     that <b>shares this node's queue</b> — i.e. its LocalDb replication partner.
    ///     <para>
    ///         Merely knowing that <i>a</i> Primary exists is not enough, because the redundancy role is
    ///         a CLUSTER-WIDE election while the alarm queue is PAIR-LOCAL. A fleet runs many pairs in
    ///         one cluster, so the elected driver Primary is usually in some other pair — and on the
    ///         docker-dev rig it is a central node, which carries the driver Akka role, replicates
    ///         nobody's LocalDb and does not even run the alarm historian. Deferring to it means this
    ///         node's events are delivered by no one, ever.
    ///     </para>
    /// </param>
    /// <returns><c>true</c> to drain; <c>false</c> only when a node holding these same rows is doing it.</returns>
    public static bool ShouldDrainAlarmHistory(RedundancyRole? localRole, bool queueSharingPeerIsPrimary) =>
        localRole switch
        {
            RedundancyRole.Primary => true,
            RedundancyRole.Secondary or RedundancyRole.Detached => !queueSharingPeerIsPrimary,
            _ => true,   // unknown role ⇒ drain; a duplicate row beats a silent gap
        };
}
