using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Replication;

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

/// <summary>
/// Keeps a node's local secret store converged with the shared SQL-Server hub, in hub-replication
/// mode. Runs a bidirectional anti-entropy sweep on an interval: pulls rows the hub holds newer,
/// pushes rows the node holds newer.
/// </summary>
/// <remarks>
/// <para>
/// This is the durability guarantee behind best-effort publishing. Live publishes make a write
/// visible in milliseconds; this sweep is what makes it <em>certain</em>. A node that was offline
/// while a peer wrote catches up on its next sweep, and a write made while the hub was unreachable
/// is pushed on the next sweep instead of being lost.
/// </para>
/// <para>
/// The sweep never throws out of <see cref="ExecuteAsync"/>: a hub outage must degrade the node to
/// "serving its last-known-good local secrets" rather than crash the host. Failures are logged and
/// retried on the next tick.
/// </para>
/// </remarks>
public sealed partial class SqlServerSecretSyncService : BackgroundService
{
    private readonly SecretReplicationReconciler _reconciler;
    private readonly ISecretStore _local;
    private readonly ISecretReplicationHub _hub;
    private readonly SqlServerSecretsOptions _options;
    private readonly ILogger<SqlServerSecretSyncService> _logger;
    private readonly TimeProvider _timeProvider;

    /// <summary>Creates the sync service.</summary>
    /// <param name="local">The node's local store.</param>
    /// <param name="hub">The shared hub store.</param>
    /// <param name="cacheInvalidator">Resolver-cache seam, evicted for every applied row.</param>
    /// <param name="options">SQL-Server replication options.</param>
    /// <param name="logger">Logger for sweep outcomes and failures.</param>
    /// <param name="timeProvider">Clock, injectable so tests need not wait a real interval.</param>
    public SqlServerSecretSyncService(
        ISecretStore local,
        ISecretReplicationHub hub,
        ISecretCacheInvalidator? cacheInvalidator,
        IOptions<SqlServerSecretsOptions> options,
        ILogger<SqlServerSecretSyncService> logger,
        TimeProvider? timeProvider = null)
    {
        ArgumentNullException.ThrowIfNull(options);

        _local = local ?? throw new ArgumentNullException(nameof(local));
        _hub = hub ?? throw new ArgumentNullException(nameof(hub));
        _options = options.Value;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _timeProvider = timeProvider ?? TimeProvider.System;
        _reconciler = new SecretReplicationReconciler(
            _local,
            cacheInvalidator,
            onRowFailed: (name, ex) => LogRowDiscarded(ex, name.Value));
    }

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (_options.SyncOnStartup)
        {
            await SweepSafelyAsync(stoppingToken).ConfigureAwait(false);
        }

        using PeriodicTimer timer = new(_options.SyncInterval, _timeProvider);

        while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
        {
            await SweepSafelyAsync(stoppingToken).ConfigureAwait(false);
        }
    }

    /// <summary>
    /// Runs one bidirectional reconcile against the hub.
    /// </summary>
    /// <param name="ct">A token to cancel the sweep.</param>
    /// <returns>How many rows were pulled from and pushed to the hub.</returns>
    internal async Task<(int Pulled, int Pushed)> SweepAsync(CancellationToken ct)
    {
        IReadOnlyList<SecretManifestEntry> hubManifest =
            await _hub.GetManifestAsync(ct).ConfigureAwait(false);

        // Pull first: adopting the hub's newer rows before computing the push set means a row the hub
        // already had newer is not then pushed straight back at it.
        int pulled = await _reconciler
            .ReconcileAsync(hubManifest, (names, token) => _hub.GetManyAsync(names, token), ct)
            .ConfigureAwait(false);

        IReadOnlyList<SecretManifestEntry> localManifest =
            await _local.GetManifestAsync(ct).ConfigureAwait(false);

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

        int pushed = 0;

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

            foreach (StoredSecret row in rows)
            {
                // Verbatim, like the replicator — the hub records the originating node's revision.
                await _hub.ApplyReplicatedAsync(row, ct).ConfigureAwait(false);
                pushed++;
            }
        }

        return (pulled, pushed);
    }

    private async Task SweepSafelyAsync(CancellationToken ct)
    {
        try
        {
            (int pulled, int pushed) = await SweepAsync(ct).ConfigureAwait(false);

            if (pulled > 0 || pushed > 0)
            {
                LogSweepConverged(pulled, pushed);
            }
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // Host shutdown — not a failure.
        }
        catch (Exception ex)
        {
            LogSweepFailed(ex);
        }
    }

    [LoggerMessage(
        EventId = 1,
        Level = LogLevel.Information,
        Message = "Secret hub sync converged: pulled {Pulled} row(s) from the hub, pushed {Pushed} row(s) to it.")]
    private partial void LogSweepConverged(int pulled, int pushed);

    [LoggerMessage(
        EventId = 2,
        Level = LogLevel.Warning,
        Message = "Secret hub sync failed; the node continues serving its local store and will retry on the next interval.")]
    private partial void LogSweepFailed(Exception exception);

    [LoggerMessage(
        EventId = 3,
        Level = LogLevel.Warning,
        Message = "Discarding replicated secret row {SecretName} that could not be applied locally; the rest of the batch continues.")]
    private partial void LogRowDiscarded(Exception exception, string secretName);
}
