using Grpc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.LocalDb.Contracts;
using ZB.MOM.WW.LocalDb.Internal;
using ZB.MOM.WW.LocalDb.Replication.Internal;

namespace ZB.MOM.WW.LocalDb.Replication;

/// <summary>
/// The passive (server) side of replication: a gRPC service that accepts an inbound bidirectional
/// <c>Sync</c> stream and runs the symmetric <see cref="SyncSession"/> over it. Registered as a
/// singleton so its single-stream guard spans concurrent calls (a node syncs with exactly one peer).
/// </summary>
public sealed class LocalDbSyncService : LocalDbSync.LocalDbSyncBase
{
    private readonly SqliteLocalDb _db;
    private readonly ReplicationOptions _options;
    private readonly ILoggerFactory _loggerFactory;
    private readonly ILogger<LocalDbSyncService> _logger;
    private readonly LocalDbMetrics? _metrics;
    private readonly SyncStatus? _status;

    // 0 = idle, 1 = a stream is active. Interlocked-guarded across concurrent calls (singleton service).
    private int _active;

    /// <summary>
    /// Creates the passive sync service. <paramref name="status"/> is taken as the public
    /// <see cref="ISyncStatus"/> (SyncStatus is internal and cannot appear in a public signature);
    /// DI always supplies the concrete SyncStatus the ctor recovers by cast.
    /// </summary>
    public LocalDbSyncService(
        ILocalDb db, IOptions<ReplicationOptions> options, ILoggerFactory loggerFactory,
        LocalDbMetrics? metrics = null, ISyncStatus? status = null)
    {
        _db = SyncSessionFactory.RequireSqlite(db, nameof(LocalDbSyncService));
        _options = options.Value;
        _loggerFactory = loggerFactory;
        _logger = loggerFactory.CreateLogger<LocalDbSyncService>();
        _metrics = metrics;
        _status = status as SyncStatus;
    }

    public override async Task Sync(
        IAsyncStreamReader<SyncMessage> requestStream,
        IServerStreamWriter<SyncMessage> responseStream,
        ServerCallContext context)
    {
        if (Interlocked.CompareExchange(ref _active, 1, 0) != 0)
            throw new RpcException(new Status(StatusCode.AlreadyExists, "a sync stream is already active"));

        try
        {
            // The linked source exists so the reader pump is deterministically torn down on EVERY
            // exit path — including a session fault for a local (non-transport) reason, where the
            // call token never fires and the pump would otherwise keep reading a stream whose
            // handler has exited.
            using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);
            var (duplex, readerTask) = GrpcSyncDuplex.Create(requestStream, responseStream, linkedCts.Token);
            var session = SyncSessionFactory.Create(_db, _options, _loggerFactory, _metrics, _status);
            _status?.SessionStarted();
            try
            {
                await session.RunAsync(duplex, linkedCts.Token);
            }
            finally
            {
                _status?.SessionEnded();
                linkedCts.Cancel();
                try
                {
                    await readerTask;
                }
                catch (Exception ex)
                {
                    _logger.LogDebug(ex, "Sync reader pump ended during teardown.");
                }
            }
        }
        catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
        {
            // Client disconnect / server shutdown: the normal end of a long-lived streaming RPC.
        }
        catch (LocalDbSchemaMismatchException ex)
        {
            throw new RpcException(new Status(StatusCode.FailedPrecondition, ex.Message));
        }
        finally
        {
            Interlocked.Exchange(ref _active, 0);
        }
    }
}
