using Microsoft.Extensions.Configuration;
namespace ZB.MOM.WW.OtOpcUa.Runtime;
///
/// The definitions of "this node's LocalDb is replicated to a peer" (#603) — one place, two deliberately
/// different predicates for the two consumers: the Host's table-registration step
/// (LocalDbSetup.ReplicationIsConfigured → : which nodes capture
/// changes) and the alarm-historian drain gate (AddAlarmHistorian → :
/// which nodes stand down in favour of a peer). Lives in Runtime because Host references Runtime and not
/// the reverse.
///
///
///
/// Why two predicates (review R1 m3). The two answers cost differently when wrong. Registering a
/// node that has no working sync path only costs capture triggers. Gating the drain of a node that
/// shares its queue with nobody parks its alarm events behind a Primary that does not hold them —
/// undelivered until the node is promoted, and drop-oldest at Capacity meanwhile. So
/// registration stays permissive and the gate is strict. Every node the gate treats as sharing also
/// captures, so the two never disagree in the harmful direction.
///
///
/// Registration: LocalDb:Replication:PeerAddress OR LocalDb:Replication:ApiKey.
/// The OR is required: only the dialling half of a pair sets PeerAddress, while the passive
/// half carries the shared ApiKey (plus a listen port) and nothing else. Keying on
/// PeerAddress alone would strip capture from every passive node.
///
///
/// Drain gate: ApiKey AND (PeerAddress OR LocalDb:SyncListenPort > 0). A
/// peer holds this node's rows only over a working sync path: a dial (PeerAddress) or a
/// listener (SyncListenPort), each authenticated by the key. Both auth directions fail closed,
/// so a path without the key shares nothing, and a key with neither path (e.g. a fleet-wide
/// secret/env ApiKey on a node with no peer) shares nothing either. Every docker-dev shape
/// gives the same answer under both predicates: site-a-1 (PeerAddress + ApiKey + port) and site-a-2
/// (ApiKey + port) are gated; central and site-b (neither) are not.
///
///
public static class LocalDbReplicationPolicy
{
/// Configuration section holding the LocalDb replication options.
public const string ReplicationSectionPath = "LocalDb:Replication";
/// The dedicated passive-sync listener port key (same key as LocalDbRegistration.SyncListenPortKey).
public const string SyncListenPortKey = "LocalDb:SyncListenPort";
/// Whether this node registers its replicated tables for change capture (table registration).
/// The application configuration.
/// true when PeerAddress or ApiKey is set under the replication section.
public static bool CapturesChanges(IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
var section = configuration.GetSection(ReplicationSectionPath);
return !string.IsNullOrWhiteSpace(section["PeerAddress"])
|| !string.IsNullOrWhiteSpace(section["ApiKey"]);
}
/// Whether a pair peer actually holds this node's replicated rows (the alarm drain gate).
/// The application configuration.
/// true when ApiKey is set AND either PeerAddress is set or
/// LocalDb:SyncListenPort is a positive port.
public static bool SharesRowsWithPeer(IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
var section = configuration.GetSection(ReplicationSectionPath);
if (string.IsNullOrWhiteSpace(section["ApiKey"])) return false;
var dials = !string.IsNullOrWhiteSpace(section["PeerAddress"]);
var listens = int.TryParse(configuration[SyncListenPortKey], out var port) && port > 0;
return dials || listens;
}
}