using System.Diagnostics;
using System.Net.Sockets;
using Akka.Actor;
using Akka.Cluster;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ZB.MOM.WW.OtOpcUa.Cluster;

/// <summary>
/// Runs the simultaneous-cold-start split-brain guard: when <c>Cluster:BootstrapGuard:Enabled</c>, the
/// node starts with NO config seed nodes (see <c>BuildClusterOptions</c>), and this coordinator picks
/// the join order and issues the single <see cref="Akka.Cluster.Cluster.JoinSeedNodes"/> once the
/// ActorSystem is up. See <see cref="ClusterBootstrapGuard"/> for the decision logic and the
/// split-brain rationale.
/// </summary>
/// <remarks>
///     <para>
///         The founder (lower canonical address) joins its self-first order immediately. The higher
///         node probes its partner's Akka endpoint (a plain TCP connect — the partner has bound its
///         port well before it forms) up to <see cref="ClusterBootstrapGuardOptions.PartnerProbeSeconds"/>:
///         reachable ⇒ peer-first (join the founder); unreachable ⇒ self-first (partner is dead, form
///         alone). The join runs on a background task so it never blocks host startup, and it is issued
///         exactly once — the coordinator never re-forms a node mid-handshake, the failure mode of the
///         retired <c>SelfFormAfter</c> watchdog.
///     </para>
/// </remarks>
public sealed class ClusterBootstrapCoordinator : IHostedService
{
    private readonly Func<ActorSystem> _system;
    private readonly AkkaClusterOptions _options;
    private readonly ILogger<ClusterBootstrapCoordinator> _log;
    private readonly CancellationTokenSource _cts = new();
    private Task? _joinTask;

    /// <summary>Creates the coordinator.</summary>
    /// <param name="system">Lazy accessor for the node's ActorSystem (resolved after Akka's hosted service starts it).</param>
    /// <param name="options">The bound cluster options (seed list + guard settings).</param>
    /// <param name="log">Logger for the bootstrap decision.</param>
    public ClusterBootstrapCoordinator(
        Func<ActorSystem> system, IOptions<AkkaClusterOptions> options, ILogger<ClusterBootstrapCoordinator> log)
    {
        _system = system ?? throw new ArgumentNullException(nameof(system));
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
        _log = log ?? throw new ArgumentNullException(nameof(log));
    }

    /// <inheritdoc/>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        if (!_options.BootstrapGuard.Enabled)
            return Task.CompletedTask; // dark switch off — Akka auto-joined from config seeds already.

        // Fire-and-forget so a higher node with a dead partner (which waits the full probe window)
        // never blocks host startup. Exceptions are logged; a failed join leaves the node unjoined,
        // which is visible and recoverable, never a silent split.
        _joinTask = Task.Run(() => RunAsync(_cts.Token), _cts.Token);
        return Task.CompletedTask;
    }

    /// <inheritdoc/>
    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await _cts.CancelAsync().ConfigureAwait(false);
        if (_joinTask is not null)
        {
            try { await _joinTask.ConfigureAwait(false); }
            catch (OperationCanceledException) { /* expected on shutdown */ }
        }
    }

    private async Task RunAsync(CancellationToken ct)
    {
        try
        {
            var seeds = _options.SeedNodes ?? Array.Empty<string>();
            var role = ClusterBootstrapGuard.Analyze(seeds, _options.PublicHostname, _options.Port);

            string[] order;
            var committedPeerFirst = false;
            if (!role.Applies)
            {
                // Not a pair seed (single-seed legacy site node, self absent, malformed, or 3+ seeds):
                // join the configured seeds unchanged — the guard arbitrates only the 2-node pair race.
                order = seeds;
                _log.LogInformation(
                    "Bootstrap guard: not a pair seed ({SeedCount} seed(s)); joining configured seeds unchanged.",
                    seeds.Length);
            }
            else if (role.IsFounder)
            {
                order = role.SelfFirstOrder;
                _log.LogInformation(
                    "Bootstrap guard: this node is the preferred founder (lower address); joining self-first, forming immediately if no peer answers.");
            }
            else
            {
                var reachable = await ProbePartnerAsync(role.PartnerHost!, role.PartnerPort, ct).ConfigureAwait(false);
                order = ClusterBootstrapGuard.HigherNodeOrder(role, reachable);
                committedPeerFirst = reachable;
                _log.LogInformation(
                    "Bootstrap guard: this node is the higher address; partner {PartnerHost}:{PartnerPort} was {Reachability} within the probe window; joining {Order}.",
                    role.PartnerHost, role.PartnerPort, reachable ? "REACHABLE" : "UNREACHABLE",
                    reachable ? "peer-first (join the founder)" : "self-first (partner down, forming alone)");
            }

            if (order.Length == 0)
            {
                _log.LogWarning("Bootstrap guard: no seed nodes to join; node stays unjoined until a seed is configured.");
                return;
            }

            var cluster = Akka.Cluster.Cluster.Get(_system());
            var addresses = order.Select(Address.Parse).ToArray();
            cluster.JoinSeedNodes(addresses);

            // Peer-first is a one-way commitment: JoinSeedNodeProcess never self-forms. If the founder
            // died in the probe→join window, this node hangs unjoined forever. We do NOT re-form here
            // (the retired SelfFormAfter mid-handshake failure mode) — we make the hang operator-visible
            // so a restart, which re-runs the guard against the now-dead founder and self-forms, recovers
            // it. Nothing is done on the founder / self-first paths, which always self-form on their own.
            if (committedPeerFirst)
                await WarnIfNotUpAsync(cluster, ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            // Host shutting down before the join completed — nothing to do.
        }
        catch (Exception ex)
        {
            _log.LogError(ex, "Bootstrap guard: failed to issue JoinSeedNodes; node remains unjoined.");
        }
    }

    /// <summary>
    /// After committing peer-first, waits a bounded time for this node to reach <c>Up</c> and logs a
    /// clear warning if it does not — the founder must have died in the probe→join window, leaving this
    /// node hung in <c>JoinSeedNodeProcess</c>. Read-only: it never re-forms or re-joins (that is the
    /// retired mid-handshake failure mode); it only surfaces the condition so an operator restarts the node.
    /// </summary>
    private async Task WarnIfNotUpAsync(Akka.Cluster.Cluster cluster, CancellationToken ct)
    {
        // Give the join a generous grace: the founder's own self-form (seed-node-timeout) plus this
        // node's join round-trip. Two probe windows is comfortably beyond both.
        var grace = TimeSpan.FromSeconds(Math.Max(10, _options.BootstrapGuard.PartnerProbeSeconds * 2));
        var sw = Stopwatch.StartNew();
        while (!ct.IsCancellationRequested && sw.Elapsed < grace)
        {
            if (cluster.SelfMember.Status == MemberStatus.Up) return; // joined — all good
            try { await Task.Delay(TimeSpan.FromSeconds(1), ct).ConfigureAwait(false); }
            catch (OperationCanceledException) { return; }
        }

        if (!ct.IsCancellationRequested && cluster.SelfMember.Status != MemberStatus.Up)
            _log.LogWarning(
                "Bootstrap guard: committed peer-first but this node is still {Status} after {Grace}s — its founder likely died in the probe→join window. This node will NOT self-form on its own (peer-first never does). RESTART it to recover: the guard will re-run, find the founder down, and form alone.",
                cluster.SelfMember.Status, (int)grace.TotalSeconds);
    }

    /// <summary>
    /// Polls the partner's Akka endpoint with a plain TCP connect until it is reachable or the probe
    /// window elapses. Reachable-at-TCP is a sound proxy for "the partner is coming up": a node binds
    /// its Akka port near the start of startup, well before it forms or joins a cluster.
    /// </summary>
    private async Task<bool> ProbePartnerAsync(string host, int port, CancellationToken ct)
    {
        var window = TimeSpan.FromSeconds(Math.Max(0, _options.BootstrapGuard.PartnerProbeSeconds));
        var interval = TimeSpan.FromMilliseconds(Math.Max(50, _options.BootstrapGuard.PartnerProbeIntervalMs));
        var sw = Stopwatch.StartNew();

        while (!ct.IsCancellationRequested)
        {
            if (await TryConnectAsync(host, port, ct).ConfigureAwait(false)) return true;
            if (sw.Elapsed >= window) return false;
            try { await Task.Delay(interval, ct).ConfigureAwait(false); }
            catch (OperationCanceledException) { return false; }
        }
        return false;
    }

    private async Task<bool> TryConnectAsync(string host, int port, CancellationToken ct)
    {
        try
        {
            using var client = new TcpClient();
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            cts.CancelAfter(Math.Max(100, _options.BootstrapGuard.ProbeConnectTimeoutMs));
            await client.ConnectAsync(host, port, cts.Token).ConfigureAwait(false);
            return client.Connected;
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            throw; // host shutdown — propagate
        }
        catch
        {
            return false; // connect refused / timed out / DNS not resolvable yet — partner not up
        }
    }
}
