namespace ZB.MOM.WW.OtOpcUa.Cluster;

public sealed class AkkaClusterOptions
{
    public const string SectionName = "Cluster";

    /// <summary>Gets or sets the Akka system name.</summary>
    public string SystemName { get; set; } = "otopcua";

    /// <summary>Gets or sets the hostname to bind to (default 0.0.0.0).</summary>
    public string Hostname { get; set; } = "0.0.0.0";

    /// <summary>Gets or sets the port to listen on (default 4053).</summary>
    public int Port { get; set; } = 4053;

    /// <summary>
    /// Hostname advertised in cluster gossip. Must be reachable by other nodes.
    /// In docker-compose this is the container DNS name; in bare metal it's the
    /// host's stable LAN address.
    /// </summary>
    public string PublicHostname { get; set; } = "127.0.0.1";

    /// <summary>
    /// Gets or sets the seed nodes for cluster bootstrapping.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b>ORDER IS LOAD-BEARING (decision 2026-07-22): a node that is one of its own seeds
    ///         must list ITSELF first.</b> Akka runs <c>FirstSeedNodeProcess</c> — the only bootstrap
    ///         path that can form a NEW cluster when no peer answers <c>InitJoin</c> — exclusively
    ///         when <c>seed-nodes[0]</c> is this node's own address; any other node runs
    ///         <c>JoinSeedNodeProcess</c>, which retries <c>InitJoin</c> forever and can never form a
    ///         cluster. So listing both peers does NOT mean either can cold-start alone: a node that
    ///         lists its partner first never comes Up while that partner is down. Self-first ordering
    ///         closes that gap inside Akka's own handshake — unlike the retired
    ///         <c>Cluster:SelfFormAfter</c> watchdog, which sat outside it and could not tell "no
    ///         seed answered" from "a seed answered and the join is in flight".
    ///     </para>
    ///     <para>
    ///         The rule is <b>conditional</b>: it binds only when this node's own address appears in
    ///         this list. A node seeded exclusively by someone else — today's docker-dev site nodes,
    ///         which list only <c>central-1</c> — is legitimately not a seed and is exempt. Enforced
    ///         at boot by <see cref="AkkaClusterOptionsValidator"/>; identity is
    ///         <see cref="PublicHostname"/> + <see cref="Port"/> (what Akka puts in
    ///         <c>SelfAddress</c>), never the <see cref="Hostname"/> bind address.
    ///     </para>
    /// </remarks>
    public string[] SeedNodes { get; set; } = Array.Empty<string>();

    /// <summary>
    /// Cluster roles for this node. When empty the role list comes from
    /// <c>OTOPCUA_ROLES</c> via <see cref="RoleParser"/>. Allowed values:
    /// <c>admin</c>, <c>driver</c>, <c>dev</c>.
    /// </summary>
    public string[] Roles { get; set; } = Array.Empty<string>();

    /// <summary>
    /// How the cluster decides to down a node it can no longer reach. One of <c>auto-down</c>
    /// (default) or <c>keep-oldest</c>; any other value fails the host at startup.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b><c>auto-down</c> — availability.</b> The leader among the <i>reachable</i> members
    ///         downs the unreachable peer after
    ///         <see cref="ServiceCollectionExtensions.DowningStableAfter"/>. A hard crash of either
    ///         node — including the oldest — fails over to the survivor with no operator action.
    ///         The trade is that a genuine network partition (both nodes alive, link cut) leaves
    ///         both sides running active until an operator restarts one.
    ///     </para>
    ///     <para>
    ///         <b><c>keep-oldest</c> — partition-safety.</b> The SBR resolver sacrifices the younger
    ///         side of a split, so a partition can never run dual-active. <b>The cost is severe for a
    ///         two-node pair:</b> it cannot survive a crash of the oldest node at all. Akka.NET's
    ///         <c>KeepOldest.OldestDecision</c> only lets <c>down-if-alone</c> rescue a side holding
    ///         &gt;= 2 members, so the 1-vs-1 survivor downs <i>itself</i> and shuts down — the
    ///         redundancy pair turns a single-node crash into a total outage. Choose this only for
    ///         clusters of three or more nodes, or where dual-active is genuinely worse than an
    ///         outage.
    ///     </para>
    /// </remarks>
    public string SplitBrainResolverStrategy { get; set; } = "auto-down";

    /// <summary>
    /// The simultaneous-cold-start split-brain guard (<c>Cluster:BootstrapGuard</c>). Default OFF — a
    /// dark switch, so existing deployments and tests keep Akka's config-driven self-first auto-join
    /// unchanged. See <see cref="ClusterBootstrapGuard"/> for the decision logic and
    /// <c>ClusterBootstrapCoordinator</c> for the runtime.
    /// </summary>
    public ClusterBootstrapGuardOptions BootstrapGuard { get; set; } = new();
}

/// <summary>
/// Configuration for the simultaneous-cold-start split-brain guard. When
/// <see cref="Enabled"/>, the node does NOT auto-join from its config seeds; a coordinator picks the
/// join order (founder self-first / joiner peer-first) after a reachability probe. See
/// <see cref="ClusterBootstrapGuard"/>.
/// </summary>
public sealed class ClusterBootstrapGuardOptions
{
    /// <summary>
    /// Gets or sets whether the guard is active. Default <see langword="false"/> — Akka auto-joins from
    /// the config seed list exactly as before. Only meaningful on a node that is one of its own two
    /// pair seeds; inert everywhere else.
    /// </summary>
    public bool Enabled { get; set; }

    /// <summary>
    /// Gets or sets how long the higher-address node probes its partner's Akka endpoint before
    /// concluding the partner is dead and forming alone. Must comfortably exceed the partner's
    /// worst-case process-start-to-Akka-bind time so a slow-but-alive partner is never mistaken for a
    /// dead one (which would re-open the split). Default 25 s.
    /// </summary>
    public int PartnerProbeSeconds { get; set; } = 25;

    /// <summary>
    /// Gets or sets the interval between partner reachability probes, in milliseconds. Default 500 ms.
    /// </summary>
    public int PartnerProbeIntervalMs { get; set; } = 500;

    /// <summary>
    /// Gets or sets the per-probe TCP connect timeout, in milliseconds. Default 1000 ms.
    /// </summary>
    public int ProbeConnectTimeoutMs { get; set; } = 1000;
}
