using System.Collections.Immutable;
using Akka.Actor;
using Akka.Cluster.Tools.Client;

namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Communication;

/// <summary>
///     Creates the <c>ClusterClient</c> central uses to reach driver nodes. Exists as a seam so
///     <c>CentralCommunicationActor</c> can be tested against a <c>TestProbe</c> without standing up
///     a second cluster.
/// </summary>
public interface IMeshClusterClientFactory
{
    /// <summary>Creates a client for the given receptionist contact points.</summary>
    /// <param name="system">The actor system to spawn the client in.</param>
    /// <param name="clusterId">
    ///     The application <c>Cluster</c> these contacts belong to. Central now creates one client per
    ///     cluster (Phase 6), so the id is woven into the client's actor name to keep the per-cluster
    ///     clients distinct and diagnosable.
    /// </param>
    /// <param name="contacts">Receptionist actor paths, one per reachable node.</param>
    /// <returns>The new client's <see cref="IActorRef"/>.</returns>
    IActorRef Create(ActorSystem system, string clusterId, ImmutableHashSet<ActorPath> contacts);
}

/// <inheritdoc />
public sealed class DefaultMeshClusterClientFactory : IMeshClusterClientFactory
{
    /// <summary>
    ///     Per-incarnation generation counter, appended to the actor name.
    /// </summary>
    /// <remarks>
    ///     <c>Context.Stop</c> of the previous client is <b>asynchronous</b> — its actor name stays
    ///     reserved until termination completes — so recreating the same name while handling a single
    ///     message throws <see cref="InvalidActorNameException"/>. That is exactly what a contact-set
    ///     change does: stop the old client, create the replacement, both in one handler. A
    ///     generation suffix makes every incarnation's name unique by construction. Ported from the
    ///     sister project, where it was a shipped bug fix rather than foresight.
    /// </remarks>
    private long _generation;

    /// <inheritdoc />
    public IActorRef Create(ActorSystem system, string clusterId, ImmutableHashSet<ActorPath> contacts)
    {
        ArgumentNullException.ThrowIfNull(system);
        ArgumentException.ThrowIfNullOrEmpty(clusterId);
        ArgumentNullException.ThrowIfNull(contacts);

        var settings = ClusterClientSettings.Create(system).WithInitialContacts(contacts);
        var name = $"mesh-client-{SanitizeForActorName(clusterId)}-{Interlocked.Increment(ref _generation)}";
        return system.ActorOf(ClusterClient.Props(settings), name);
    }

    /// <summary>
    ///     Reduces a cluster id to characters safe in an Akka actor name. The id is operator-authored
    ///     (e.g. a GUID or a slug), so it may carry characters Akka rejects; anything outside
    ///     <c>[A-Za-z0-9-]</c> becomes <c>-</c>. The trailing generation counter still guarantees
    ///     uniqueness, so a lossy sanitisation cannot cause a name collision.
    /// </summary>
    /// <param name="clusterId">The raw cluster id.</param>
    /// <returns>An actor-name-safe rendering of the id.</returns>
    private static string SanitizeForActorName(string clusterId)
    {
        var chars = clusterId.Select(c => char.IsAsciiLetterOrDigit(c) || c == '-' ? c : '-');
        return new string(chars.ToArray());
    }
}
