namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// Physical OPC UA server node within a .
public sealed class ClusterNode
{
/// Stable per-machine logical ID, e.g. "LINE3-OPCUA-A".
public required string NodeId { get; set; }
/// The unique identifier of the cluster this node belongs to.
public required string ClusterId { get; set; }
/// Machine hostname / IP.
public required string Host { get; set; }
/// The OPC UA server port (default 4840).
public int OpcUaPort { get; set; } = 4840;
/// The dashboard HTTP port (default 8081).
public int DashboardPort { get; set; } = 8081;
///
/// Akka remoting port (default 4053). This is central's dial target, not the node's own
/// binding configuration — the node binds from Cluster:Port in its own appsettings,
/// and this column duplicates that value for a reader that cannot see the node's config.
/// Per-cluster mesh Phase 2 builds its ClusterClient contact points from
/// + this port, at which point central and the site node no longer share a
/// gossip ring and central has no other way to learn it.
///
///
/// The duplication is real and unenforced by the schema. ClusterNodeAddressReconciler
/// on the admin node compares this row against observed cluster membership and logs an Error
/// on mismatch — a node that binds 4054 while its row says 4053 is unreachable from central in
/// Phase 2, and the symptom there is a silent absence of acks rather than an error.
///
public int AkkaPort { get; set; } = 4053;
///
/// gRPC port central dials for the Phase 5 telemetry stream, or when the
/// node exposes none. Also central's dial target rather than the node's binding config.
///
///
/// Nullable by intent: nothing listens on this port yet, and a non-null default would assert a
/// port that does not exist. Phase 5 populates it; until then is the
/// honest value.
///
public int? GrpcPort { get; set; }
///
/// OPC UA ApplicationUri — MUST be unique per node per OPC UA spec. Clients pin trust here.
/// Fleet-wide unique index enforces no two nodes share a value.
/// Stored explicitly, NOT derived from at runtime — silent rewrite on
/// hostname change would break all client trust.
///
public required string ApplicationUri { get; set; }
/// Primary = 200, Secondary = 150 by default.
public byte ServiceLevelBase { get; set; } = 200;
///
/// Per-node override JSON keyed by DriverInstanceId, merged onto cluster-level DriverConfig
/// at apply time. Minimal by intent. Nullable when no overrides exist.
///
public string? DriverConfigOverridesJson { get; set; }
/// Gets or sets a value indicating whether this node is enabled.
///
/// Part of the declared topology. DraftValidator.ValidateClusterTopology requires
/// the enabled-node count to equal , so disabling one node
/// of a Warm/Hot pair is a deploy-blocking validation error by design — it would boot the
/// runtime into the InvalidTopology band. To take a node out of service temporarily, use
/// instead.
///
public bool Enabled { get; set; } = true;
///
/// Node is temporarily out of service: still part of the cluster's declared topology, but not
/// expected to participate. A deployment does not wait for its ack, and the address reconciler
/// does not warn that it is absent.
///
///
///
/// Separate from because the two answer different questions.
/// means "this node is part of the declared topology" and is checked
/// against ; this flag means "do not expect it right
/// now". Per-cluster mesh Phase 1 made an enabled row's ack mandatory, so without this a
/// node down for maintenance would block every deployment to its cluster — and clearing
/// to escape that is itself rejected by the topology gate on any
/// 2-node pair, which is every cluster in the target deployment.
///
///
/// Found by the Phase 1 live gate: the two rules were independently reasonable and
/// contradictory together.
///
///
public bool MaintenanceMode { get; set; }
/// Gets or sets the timestamp when this node was last seen.
public DateTime? LastSeenAt { get; set; }
/// Gets or sets the timestamp when this node was created.
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// Gets or sets the username of who created this node.
public required string CreatedBy { get; set; }
// Navigation
/// Gets or sets the cluster this node belongs to.
public ServerCluster? Cluster { get; set; }
/// Gets or sets the credentials associated with this node.
public ICollection Credentials { get; set; } = [];
}