namespace ZB.MOM.WW.OtOpcUa.Cluster;
///
/// Selects and configures the transport carrying central→node commands and node→central
/// deployment acks (per-cluster mesh Phase 2).
///
///
///
/// selects between a pair-local transport and a cross-mesh
/// one. is the default and cannot cross an application-Cluster
/// boundary; is the only mode that can, and
/// requires it on any node carrying a
/// cluster-{ClusterId} role.
///
///
/// The DistributedPubSub branch is retained, not scheduled for deletion. An earlier
/// comment here promised Phase 6 would delete it along with the single mesh; that was
/// deliberately reversed — see docs/Configuration.md § "The compiled transport-mode
/// defaults were deliberately NOT flipped". DPS remains the correct and default transport for
/// a single-mesh topology, which every integration test and every profile that leaves this
/// key unset still runs.
///
///
/// Discovery is deliberately asymmetric, matching the sister project. Central
/// discovers nodes from the database — enabled, non-maintenance ClusterNode
/// rows, rebuilt every . Nodes discover central from
/// this section (): static, restart to change. The
/// asymmetry follows the rate of change — operators add and retire nodes, but central's own
/// address is part of the deployment.
///
///
public sealed class MeshTransportOptions
{
/// Configuration section name.
public const string SectionName = "MeshTransport";
///
/// Every command stays on DistributedPubSub — pair-local, and the default. Cannot cross an
/// application-Cluster boundary.
///
public const string ModeDps = "Dps";
///
/// Commands cross the mesh boundary as unary gRPC calls on the mesh_transport.v1
/// contract — central dials each node's NodeCommandService, a node posts its
/// deployment ack to central's CentralCommandService. The command plane is
/// authenticated per application Cluster ().
///
public const string ModeGrpc = "Grpc";
///
/// (default) or . Any other value fails the host
/// at startup rather than silently falling back — a transport that is neither delivers
/// nothing, and the symptom would be commands that vanish. That includes the retired
/// ClusterClient value: a node still carrying it is refused at boot rather than
/// quietly demoted to DPS, which on a split-topology node would deliver nothing at all.
///
public string Mode { get; set; } = ModeDps;
///
/// How often central re-reads ClusterNode rows to rebuild its dispatch set.
/// Refreshing on a timer (rather than only on admin change) means a row edited by any
/// process — migration, direct SQL, a second admin node — is picked up without a restart.
///
public int ContactRefreshSeconds { get; set; } = 60;
///
/// MeshTransport:CentralGrpcEndpoints — node-side only, and only under
/// : the gRPC base addresses of the central pair's
/// CentralCommandService, e.g. http://central-1:4055. Full URLs
/// (scheme://host:port), not bare host:port — http:// is h2c.
///
///
/// Tried in order, stopping at the first success, which is what keeps the ack
/// one-per-node-per-dispatch: either central node routes it to the same
/// ConfigPublishCoordinator singleton proxy, so which one answers does not matter,
/// but two answering would double-count.
///
public string[] CentralGrpcEndpoints { get; set; } = [];
///
/// MeshTransport:GrpcPsk — this node's pre-shared key for the command plane, one per
/// application Cluster. Under it is both the bearer this node
/// presents when posting an ack to central and the bearer it requires on inbound
/// NodeCommandService calls.
///
///
///
/// Production form is a secret reference — ${secret:OT-MESH-PSK-<clusterId>},
/// e.g. ${secret:OT-MESH-PSK-SITE-A} — resolved pre-host by the existing
/// SecretReferenceExpander, so it works on a driver-only node with no ConfigDb.
/// Supply a literal only on a rig, via the environment
/// (MeshTransport__GrpcPsk); never commit one.
///
///
/// Per-Cluster, not fleet-wide, on purpose. The three existing shared-bearer gRPC
/// channels are read/telemetry planes; this one carries RestartDriver,
/// AlarmCommand and deployment dispatch, so a single fleet key would let one
/// compromised site node command every other site and forge acks fleet-wide.
///
///
/// Nullable because unset must be distinguishable from blank at the boundary that
/// fails closed: the serve-side interceptor rejects every call when no key is
/// configured rather than defaulting to an empty comparison that any empty bearer would
/// satisfy.
///
///
public string? GrpcPsk { get; set; }
///
/// MeshTransport:DispatchTimeoutSeconds — central-side deadline, in seconds, for one
/// NodeCommandService.Execute call to one node. Must be greater than 0 under
/// .
///
///
/// Bounded per node because a fan-out dispatch dials every enabled node in the cluster: an
/// unbounded call to one unreachable node would stall the commands queued behind it. Expiry
/// is logged and dropped — the transport is fire-and-forget and layers no retry, so the
/// deploy path's 2-minute apply deadline stays the only recovery mechanism.
/// Validation of this key is scoped to driver-role nodes; that covers the central pair only
/// because central nodes are always fused (admin,driver,cluster-*). A pure-admin
/// central node would leave this unvalidated on the node that uses it.
///
public int DispatchTimeoutSeconds { get; set; } = 10;
///
/// MeshTransport:AckTimeoutSeconds — node-side deadline, in seconds, for one
/// CentralCommandService.ReportApply call to one central endpoint. Must be greater
/// than 0 under .
///
///
/// Applied per endpoint, not per ack: the node walks in
/// order, so a dead first endpoint costs this deadline before the second is tried.
///
public int AckTimeoutSeconds { get; set; } = 10;
///
/// MeshTransport:ClusterPsks:{clusterId} — central-side override map of per-cluster
/// PSKs, consulted before the Secrets store when central builds a channel to (or
/// verifies an ack from) a given Cluster. Keyed case-insensitively.
///
///
/// Production leaves this empty and the runtime provider resolves
/// OT-MESH-PSK-{clusterId} from the Secrets store instead; the map exists so the
/// docker-dev rig and integration tests can supply literals through the environment
/// (MeshTransport__ClusterPsks__SITE-A) without standing up a KEK. Central needs a map
/// rather than a single key because its cluster set is DB-dynamic — one key per site pair it
/// talks to — whereas a node holds exactly its own ().
///
public Dictionary ClusterPsks { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}