namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;

/// <summary>Physical OPC UA server node within a <see cref="ServerCluster"/>.</summary>
public sealed class ClusterNode
{
    /// <summary>Stable per-machine logical ID, e.g. "LINE3-OPCUA-A".</summary>
    public required string NodeId { get; set; }

    /// <summary>The unique identifier of the cluster this node belongs to.</summary>
    public required string ClusterId { get; set; }

    /// <summary>Machine hostname / IP.</summary>
    public required string Host { get; set; }

    /// <summary>The OPC UA server port (default 4840).</summary>
    public int OpcUaPort { get; set; } = 4840;

    /// <summary>The dashboard HTTP port (default 8081).</summary>
    public int DashboardPort { get; set; } = 8081;

    /// <summary>
    ///     Akka remoting port (default 4053). <b>This is central's dial target, not the node's own
    ///     binding configuration</b> — the node binds from <c>Cluster:Port</c> 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
    ///     <see cref="Host"/> + 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.
    /// </summary>
    /// <remarks>
    ///     The duplication is real and unenforced by the schema. <c>ClusterNodeAddressReconciler</c>
    ///     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.
    /// </remarks>
    public int AkkaPort { get; set; } = 4053;

    /// <summary>
    ///     gRPC port central dials for the Phase 5 telemetry stream, or <see langword="null"/> when the
    ///     node exposes none. Also central's dial target rather than the node's binding config.
    /// </summary>
    /// <remarks>
    ///     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 <see langword="null"/> is the
    ///     honest value.
    /// </remarks>
    public int? GrpcPort { get; set; }

    /// <summary>
    ///     OPC UA <c>ApplicationUri</c> — 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 <see cref="Host"/> at runtime — silent rewrite on
    ///     hostname change would break all client trust.
    /// </summary>
    public required string ApplicationUri { get; set; }

    /// <summary>Primary = 200, Secondary = 150 by default.</summary>
    public byte ServiceLevelBase { get; set; } = 200;

    /// <summary>
    ///     Per-node override JSON keyed by DriverInstanceId, merged onto cluster-level DriverConfig
    ///     at apply time. Minimal by intent. Nullable when no overrides exist.
    /// </summary>
    public string? DriverConfigOverridesJson { get; set; }

    /// <summary>Gets or sets a value indicating whether this node is enabled.</summary>
    /// <remarks>
    ///     <b>Part of the declared topology.</b> <c>DraftValidator.ValidateClusterTopology</c> requires
    ///     the enabled-node count to equal <see cref="ServerCluster.NodeCount"/>, 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
    ///     <see cref="MaintenanceMode"/> instead.
    /// </remarks>
    public bool Enabled { get; set; } = true;

    /// <summary>
    ///     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.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         Separate from <see cref="Enabled"/> because the two answer different questions.
    ///         <see cref="Enabled"/> means "this node is part of the declared topology" and is checked
    ///         against <see cref="ServerCluster.NodeCount"/>; 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
    ///         <see cref="Enabled"/> to escape that is itself rejected by the topology gate on any
    ///         2-node pair, which is every cluster in the target deployment.
    ///     </para>
    ///     <para>
    ///         Found by the Phase 1 live gate: the two rules were independently reasonable and
    ///         contradictory together.
    ///     </para>
    /// </remarks>
    public bool MaintenanceMode { get; set; }

    /// <summary>Gets or sets the timestamp when this node was last seen.</summary>
    public DateTime? LastSeenAt { get; set; }

    /// <summary>Gets or sets the timestamp when this node was created.</summary>
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    /// <summary>Gets or sets the username of who created this node.</summary>
    public required string CreatedBy { get; set; }

    // Navigation
    /// <summary>Gets or sets the cluster this node belongs to.</summary>
    public ServerCluster? Cluster { get; set; }
    /// <summary>Gets or sets the credentials associated with this node.</summary>
    public ICollection<ClusterNodeCredential> Credentials { get; set; } = [];
}
