using Akka.Actor;
using Akka.Cluster.Tools.PublishSubscribe;
using Akka.Event;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Replication;
using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Protocol;

namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet;

/// <summary>
/// One per node. Owns this node's participation in secret replication: broadcasts local writes,
/// periodically announces its manifest, and services peers' pulls.
/// </summary>
/// <remarks>
/// <para>
/// The protocol is deliberately small — three messages, no leader, no singleton:
/// </para>
/// <list type="number">
/// <item><description>
/// <b>Live push.</b> A local write publishes a <see cref="SecretRowsMessage"/> to the topic. Peers
/// apply it last-writer-wins. This is the fast path — propagation in milliseconds — and it is
/// allowed to fail.
/// </description></item>
/// <item><description>
/// <b>Anti-entropy.</b> Every node periodically publishes a <see cref="SecretManifestAnnounce"/>.
/// A peer receiving it computes <em>both</em> directions in one pass: it pushes back rows it holds
/// newer, and asks (<see cref="SecretPullRequest"/>) for rows it is missing or behind on. This is
/// the correctness path — it repairs anything the fast path dropped, in either direction.
/// </description></item>
/// <item><description>
/// <b>Pull service.</b> A <see cref="SecretPullRequest"/> is answered with the requested rows.
/// </description></item>
/// </list>
/// <para>
/// There is no coordinator because there does not need to be one: every exchange is idempotent and
/// commutative under last-writer-wins, so nodes converge regardless of message order, duplication,
/// or which subset of peers is currently reachable. A node that was partitioned keeps serving its
/// local store the whole time and re-converges on the first announce after the partition heals —
/// which is the entire reason to choose this transport over the shared SQL-Server store.
/// </para>
/// </remarks>
public sealed class SecretReplicationActor : ReceiveActor, IWithTimers
{
    /// <summary>The distributed pub/sub topic secret replication traffic rides on.</summary>
    public const string Topic = "zb-mom-ww-secrets";

    private readonly ISecretStore _store;
    private readonly SecretReplicationReconciler _reconciler;
    private readonly TimeSpan _announceInterval;
    private readonly IActorRef _mediator;
    private readonly ILoggingAdapter _log = Context.GetLogger();

    /// <inheritdoc />
    public ITimerScheduler Timers { get; set; } = null!;

    /// <summary>Creates the replication actor.</summary>
    /// <param name="store">The node's local secret store.</param>
    /// <param name="cacheInvalidator">Resolver-cache seam, evicted for every applied row.</param>
    /// <param name="announceInterval">How often this node announces its manifest.</param>
    public SecretReplicationActor(
        ISecretStore store,
        ISecretCacheInvalidator? cacheInvalidator,
        TimeSpan announceInterval)
    {
        _store = store ?? throw new ArgumentNullException(nameof(store));
        _reconciler = new SecretReplicationReconciler(
            _store,
            cacheInvalidator,
            onRowFailed: (name, ex) => _log.Warning(
                ex, "Discarding replicated secret row {0} that could not be applied locally.", name.Value));
        _announceInterval = announceInterval;
        _mediator = DistributedPubSub.Get(Context.System).Mediator;

        _mediator.Tell(new Subscribe(Topic, Self));

        Receive<PublishLocalWrite>(HandlePublishLocalWrite);
        Receive<SecretRowsMessage>(HandleRows);
        Receive<SecretManifestAnnounce>(HandleManifestAnnounce);
        Receive<SecretPullRequest>(HandlePullRequest);
        Receive<AnnounceTick>(_ => HandleAnnounceTick());
        Receive<SubscribeAck>(_ => _log.Debug("Subscribed to secret replication topic {0}.", Topic));
        Receive<ExchangeFailed>(f =>
            _log.Warning(f.Cause, "Secret anti-entropy exchange with a peer failed; retrying next announce."));
        Receive<ExchangeDone>(_ => { /* successful exchange, piped back to self — nothing to do. */ });
    }

    /// <summary>Props for the replication actor.</summary>
    /// <param name="store">The node's local secret store.</param>
    /// <param name="cacheInvalidator">Resolver-cache seam.</param>
    /// <param name="announceInterval">How often this node announces its manifest.</param>
    /// <returns>Props creating the actor.</returns>
    public static Props Props(
        ISecretStore store, ISecretCacheInvalidator? cacheInvalidator, TimeSpan announceInterval) =>
        Akka.Actor.Props.Create(() =>
            new SecretReplicationActor(store, cacheInvalidator, announceInterval));

    /// <inheritdoc />
    protected override void PreStart() =>
        Timers.StartPeriodicTimer(AnnounceTick.Instance, AnnounceTick.Instance, _announceInterval, _announceInterval);

    // A local write, handed over by AkkaSecretReplicator. Published from inside the actor so the
    // message carries Self as sender — that is what lets a peer reply directly to this node instead
    // of broadcasting its answer to everyone.
    private void HandlePublishLocalWrite(PublishLocalWrite message)
    {
        _mediator.Tell(new Publish(
            Topic,
            new SecretRowsMessage([SecretRowDto.FromStoredSecret(message.Row)])));
    }

    private void HandleRows(SecretRowsMessage message)
    {
        if (IsFromSelf())
        {
            return;
        }

        IReadOnlyList<StoredSecret> rows = Materialize(message.Rows);

        if (rows.Count == 0)
        {
            return;
        }

        // PipeTo nothing: applying is fire-and-forget from the mailbox's point of view. Failures are
        // logged, and anti-entropy re-delivers anything that did not land.
        _reconciler.ApplyAsync(rows, CancellationToken.None)
            .ContinueWith(
                task => _log.Warning(
                    task.Exception,
                    "Failed to apply {0} replicated secret row(s) from a peer; anti-entropy will retry.",
                    rows.Count),
                CancellationToken.None,
                TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default);
    }

    private void HandleManifestAnnounce(SecretManifestAnnounce message)
    {
        if (IsFromSelf())
        {
            return;
        }

        IActorRef peer = Sender;
        // Captured here, on the actor thread, because the exchange awaits and cannot read Self after.
        IActorRef self = Self;
        IReadOnlyList<SecretManifestEntry> remote = [.. message.Entries.Select(e => e.ToEntry())];

        ReconcileWithPeerAsync(peer, self, remote).PipeTo(self, failure: ex => new ExchangeFailed(ex));
    }

    private void HandlePullRequest(SecretPullRequest message)
    {
        if (IsFromSelf())
        {
            return;
        }

        IActorRef peer = Sender;
        // Captured before the continuation: Self is context-dependent and there is no guarantee the
        // actor context is still current when the continuation runs (Akka analyzer AK1005).
        IActorRef self = Self;
        IReadOnlyList<SecretName> names = ParseNames(message.Names);

        _reconciler.ReadLocalAsync(names, CancellationToken.None)
            .ContinueWith(
                task =>
                {
                    if (task.IsFaulted)
                    {
                        // Logged rather than swallowed: the peer is waiting for a reply it will never
                        // get, and without this the only symptom is a secret that quietly never
                        // converges. The peer re-asks on its next announce.
                        _log.Warning(
                            task.Exception,
                            "Failed to read {0} local secret row(s) for a peer's pull request.",
                            names.Count);
                        return;
                    }

                    if (task.IsCompletedSuccessfully && task.Result.Count > 0)
                    {
                        peer.Tell(
                            new SecretRowsMessage([.. task.Result.Select(SecretRowDto.FromStoredSecret)]),
                            self);
                    }
                },
                CancellationToken.None,
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default);
    }

    private void HandleAnnounceTick()
    {
        _store.GetManifestAsync(CancellationToken.None)
            .ContinueWith(
                task =>
                {
                    if (task.IsCompletedSuccessfully)
                    {
                        _mediator.Tell(new Publish(
                            Topic,
                            new SecretManifestAnnounce(
                                [.. task.Result.Select(SecretManifestEntryDto.FromEntry)])));
                    }
                },
                CancellationToken.None,
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default);
    }

    // Both halves of an anti-entropy exchange, computed from one manifest: push what the peer is
    // behind on, ask for what we are behind on.
    //
    // `self` is a PARAMETER, captured by the caller while the actor context is still current. It
    // must not be read as the `Self` property below: `Self` resolves through Context, which is
    // [ThreadStatic], and every `await` here can resume on a thread-pool thread where that context
    // is gone — reading it there throws NotSupportedException and faults the whole exchange.
    //
    // This was a live bug. It hid because a local SQLite store usually completes `await`
    // synchronously, keeping the continuation on the mailbox thread and the context intact, so the
    // entire cluster suite passed while anti-entropy would have broken under any slower or more
    // contended store. Anti_entropy_still_works_when_the_store_is_genuinely_asynchronous forces the
    // async path and fails if this regresses.
    private async Task<ExchangeDone> ReconcileWithPeerAsync(
        IActorRef peer, IActorRef self, IReadOnlyList<SecretManifestEntry> remote)
    {
        IReadOnlyList<SecretManifestEntry> localManifest =
            await _store.GetManifestAsync(CancellationToken.None).ConfigureAwait(false);

        IReadOnlyList<SecretName> push =
            SecretReplicationReconciler.ComputePushSet(localManifest, remote);

        if (push.Count > 0)
        {
            IReadOnlyList<StoredSecret> rows =
                await _reconciler.ReadLocalAsync(push, CancellationToken.None).ConfigureAwait(false);

            if (rows.Count > 0)
            {
                peer.Tell(new SecretRowsMessage([.. rows.Select(SecretRowDto.FromStoredSecret)]), self);
            }
        }

        IReadOnlyList<SecretName> pull =
            SecretReplicationReconciler.ComputePullSet(localManifest, remote);

        if (pull.Count > 0)
        {
            peer.Tell(new SecretPullRequest([.. pull.Select(n => n.Value)]), self);
        }

        return ExchangeDone.Instance;
    }

    // DistributedPubSub delivers a node's own publications back to it. Dropping them here is what
    // stops a broadcast from being re-applied locally and, worse, echoed onward.
    private bool IsFromSelf() => Sender.Equals(Self);

    // A peer's rows are untrusted input: a malformed name or content type must drop that row, not
    // fault the actor and stall replication for every other secret.
    private IReadOnlyList<StoredSecret> Materialize(IReadOnlyList<SecretRowDto> dtos)
    {
        var rows = new List<StoredSecret>(dtos.Count);

        foreach (SecretRowDto dto in dtos)
        {
            try
            {
                rows.Add(dto.ToStoredSecret());
            }
            catch (Exception ex) when (ex is ArgumentException or FormatException)
            {
                _log.Warning(ex, "Discarding a malformed replicated secret row from a peer.");
            }
        }

        return rows;
    }

    private IReadOnlyList<SecretName> ParseNames(IReadOnlyList<string> names)
    {
        var parsed = new List<SecretName>(names.Count);

        foreach (string name in names)
        {
            try
            {
                parsed.Add(new SecretName(name));
            }
            catch (ArgumentException ex)
            {
                _log.Warning(ex, "Discarding a malformed secret name in a peer's pull request.");
            }
        }

        return parsed;
    }

    /// <summary>Instruction from <see cref="AkkaSecretReplicator"/> to broadcast a local write.</summary>
    /// <param name="Row">The encrypted row to broadcast.</param>
    public sealed record PublishLocalWrite(StoredSecret Row);

    private sealed record AnnounceTick
    {
        public static readonly AnnounceTick Instance = new();
    }

    private sealed record ExchangeFailed(Exception Cause);

    private sealed record ExchangeDone
    {
        public static readonly ExchangeDone Instance = new();
    }
}
