namespace ZB.MOM.WW.LocalDb.Replication;
///
/// Configuration for the bidirectional 2-node replication engine. All durations are wall-clock UTC.
///
public sealed class ReplicationOptions
{
/// gRPC address of the peer to dial. Required for the initiator role (validated in the sync host).
public string PeerAddress { get; set; } = "";
///
/// Shared key that authenticates the pair partner. Set the SAME value on both nodes. The initiator
/// sends it as Authorization: Bearer <key>; since 0.3.6 the passive
/// also requires it (constant-time compare) and refuses any other
/// stream with Unauthenticated, before the handshake and before displacing the current
/// stream. Null, empty or whitespace disables key auth on both sides: the initiator sends no
/// header and the endpoint accepts any caller (a warning is logged at startup), so protect it some
/// other way — network isolation or a host interceptor.
///
public string? ApiKey { get; set; }
/// How often the change pump flushes pending oplog deltas to the peer.
public TimeSpan FlushInterval { get; set; } = TimeSpan.FromMilliseconds(250);
///
/// Secondary, row-count cap on an outbound batch (and the snapshot streamer's DB page size).
/// is the primary cap.
///
///
/// Historically this was the ONLY cap, which made it a proxy for a byte budget that consumers
/// had to tune by hand against their widest row — ScadaBridge pinned it to 16 because a 70 KB
/// config_json times the 500 default is ~35 MB against a 4 MB gRPC limit. With
/// doing that job directly, this is back to being what its name
/// says: a bound on how many rows one message may carry.
///
public int MaxBatchSize { get; set; } = 500;
///
/// Primary cap on an outbound batch: the summed serialized size of the entries in one
/// DeltaBatch / SnapshotBatch message, in bytes. Default 2 MB.
///
///
///
/// Sized at half the default 4 MB gRPC receive limit, so the message framing, the oneof wrapper
/// and any per-entry overhead the estimate does not model all fit inside the margin.
///
///
/// A single row larger than the budget is still sent, alone. Skipping it would stall the
/// stream permanently on one oversized row; sending it gives the transport the chance to accept
/// it (gRPC limits are configurable) and, if it refuses, a loud transport error naming the size
/// rather than silent non-convergence.
///
///
/// Wire compatibility: none required. Batch size is a sender-side choice the protocol
/// already leaves free — DeltaBatch/SnapshotBatch carry a repeated field with no
/// declared count — so a node on this version interoperates with a peer on any earlier one in
/// both directions, with no proto change and no lib_schema_version bump. (ScadaBridge
/// site pairs stop and start together regardless; this simply does not depend on it.)
///
///
public int MaxBatchBytes { get; set; } = 2 * 1024 * 1024;
/// Backlog ceiling: exceeding it flags a snapshot resync and prunes the oldest oplog rows.
public long MaxOplogRows { get; set; } = 1_000_000;
/// Age ceiling: an unpruned oplog entry older than this flags a snapshot resync.
public TimeSpan MaxOplogAge { get; set; } = TimeSpan.FromDays(7);
/// How long deleted-row tombstones are retained before pruning.
public TimeSpan TombstoneRetention { get; set; } = TimeSpan.FromDays(7);
/// Upper bound on the exponential reconnect backoff.
public TimeSpan ReconnectBackoffMax { get; set; } = TimeSpan.FromSeconds(60);
///
/// How long the initiator's HTTP/2 connection may sit without a keepalive PING before one is
/// sent. Default 60s.
///
///
///
/// A sync stream is long-lived and, on a converged pair, almost entirely silent — which is
/// exactly the shape a half-open TCP connection hides behind. Without pings, a hard crash or a
/// partition of one node leaves the other holding a stream that will never produce another byte
/// and never fail either: the initiator sits in a session it believes is healthy, and the
/// passive node's handler sits inside its single-stream guard, refusing the reconnect that would
/// heal the pair. Pings turn that into a detected failure on both ends.
///
///
/// Sent with KeepAlivePingPolicy.Always, so they flow on an idle connection too — an
/// active-streams-only policy would not fire in precisely the silent-pair case above. Safe over
/// h2c: a PING is an HTTP/2 frame, nothing to do with TLS.
///
///
public TimeSpan KeepAlivePingDelay { get; set; } = TimeSpan.FromSeconds(60);
///
/// How long the initiator waits for a keepalive PING ack before tearing the connection down.
/// Default 20s. Detection therefore lands within roughly
/// + this.
///
public TimeSpan KeepAlivePingTimeout { get; set; } = TimeSpan.FromSeconds(20);
/// How often the maintenance service enforces the oplog caps and prunes expired tombstones.
public TimeSpan MaintenanceInterval { get; set; } = TimeSpan.FromSeconds(60);
///
/// Ceiling on the snapshot rows a receiver holds in memory while their foreign-key parent has not
/// arrived yet. Default 100,000. Past it the OLDEST held rows are applied once more and, if their
/// parent is still missing, dead-lettered.
///
///
///
/// Since 0.3.6 a snapshot row that fails only its FK check is deferred and retried as the rest of
/// the snapshot lands, instead of being dead-lettered. A 0.3.6 sender streams parent tables first,
/// so in practice only self-referencing rows whose parent sorts later wait; but an OLDER sender
/// streams tables by name, and a child table that sorts before its parent then defers every one of
/// its rows. This bounds that memory.
///
///
/// A spilled row is lost to this snapshot, exactly like an orphan dead-lettered at
/// SnapshotComplete: the sender's debt clears on the ack, so it is not re-sent. It is
/// visible in __localdb_dead_letter and in localdb.sync.dead_lettered. Upgrade the
/// sender (parents first) or raise this cap if spills appear.
///
///
public int MaxSnapshotDeferredRows { get; set; } = 100_000;
/// Maximum tolerated HLC drift where a peer's clock runs ahead of ours.
public TimeSpan MaxHlcDriftAhead { get; set; } = TimeSpan.FromMinutes(5);
///
/// When true, entries whose HLC physical time exceeds are
/// dead-lettered individually (the rest of the batch still applies); when false, drifted
/// entries are applied and a single warning is logged per session.
///
public bool FailClosedOnDrift { get; set; }
}