using System.Threading.Channels;
using NATS.Server.JetStream.Storage;

namespace NATS.Server.JetStream.MirrorSource;

// Go reference: server/stream.go:2788-2854 (processMirrorMsgs), 3125-3400 (setupMirrorConsumer)
// Go reference: server/stream.go:2863-3014 (processInboundMirrorMsg)

/// <summary>
/// Coordinates continuous synchronization from an origin stream to a local mirror.
/// Runs a background pull loop that fetches batches of messages from the origin,
/// applies them to the local store, and tracks origin-to-current sequence alignment
/// for catchup after restarts. Includes exponential backoff retry on failures
/// and health reporting via lag calculation.
/// </summary>
public sealed class MirrorCoordinator : IAsyncDisposable
{
    // Go: sourceHealthCheckInterval = 10 * time.Second
    private static readonly TimeSpan HealthCheckInterval = TimeSpan.FromSeconds(10);

    // Go: sourceHealthHB = 1 * time.Second
    private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(1);

    private static readonly TimeSpan InitialRetryDelay = TimeSpan.FromMilliseconds(250);
    private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromSeconds(30);
    private const int DefaultBatchSize = 256;

    private readonly IStreamStore _targetStore;
    private readonly Channel<StoredMessage> _inbound;
    private readonly Lock _gate = new();
    private CancellationTokenSource? _cts;
    private Task? _syncLoop;
    private int _consecutiveFailures;

    /// <summary>Last sequence number successfully applied from the origin stream.</summary>
    public ulong LastOriginSequence { get; private set; }

    /// <summary>UTC timestamp of the last successful sync operation.</summary>
    public DateTime LastSyncUtc { get; private set; }

    /// <summary>Number of consecutive sync failures (resets on success).</summary>
    public int ConsecutiveFailures
    {
        get { lock (_gate) return _consecutiveFailures; }
    }

    /// <summary>
    /// Whether the background sync loop is actively running.
    /// </summary>
    public bool IsRunning
    {
        get { lock (_gate) return _syncLoop is not null && !_syncLoop.IsCompleted; }
    }

    /// <summary>
    /// Current lag: origin last sequence minus local last sequence.
    /// Returns 0 when fully caught up or when origin sequence is unknown.
    /// </summary>
    public ulong Lag { get; private set; }

    // -------------------------------------------------------------------------
    // Gap detection properties
    // Go reference: server/stream.go:2863-3014 (processInboundMirrorMsg gap handling)
    // -------------------------------------------------------------------------

    /// <summary>Whether a sequence gap has been detected in the origin stream.</summary>
    public bool HasGap => _hasGap;

    /// <summary>First missing sequence number when a gap is detected.</summary>
    public ulong GapStart => _gapStart;

    /// <summary>Last missing sequence number when a gap is detected.</summary>
    public ulong GapEnd => _gapEnd;

    // -------------------------------------------------------------------------
    // Error state properties
    // -------------------------------------------------------------------------

    /// <summary>Whether the coordinator is in an error state.</summary>
    public bool HasError => _errorMessage is not null;

    /// <summary>Current error message, or null if no error.</summary>
    public string? ErrorMessage => _errorMessage;

    // Go: mirror.sseq — stream sequence tracking for gap detection
    private ulong _expectedOriginSeq;

    // Go: mirror.dseq — delivery sequence tracking
    private ulong _deliverySeq;

    // Gap detection state
    private ulong _gapStart;
    private ulong _gapEnd;
    private bool _hasGap;

    // Error state tracking
    private string? _errorMessage;

    public MirrorCoordinator(IStreamStore targetStore)
    {
        _targetStore = targetStore;
        _inbound = Channel.CreateUnbounded<StoredMessage>(new UnboundedChannelOptions
        {
            SingleReader = true,
            SingleWriter = false,
        });
    }

    /// <summary>
    /// Processes a single inbound message from the origin stream.
    /// This is the direct-call path used when the origin and mirror are in the same process.
    /// Go reference: server/stream.go:2863-3014 (processInboundMirrorMsg)
    /// </summary>
    public async Task OnOriginAppendAsync(StoredMessage message, CancellationToken ct)
    {
        // Go: sseq == mset.mirror.sseq+1 — normal in-order delivery
        if (_expectedOriginSeq > 0 && message.Sequence <= _expectedOriginSeq)
        {
            // Ignore older/duplicate messages (Go: sseq <= mset.mirror.sseq)
            return;
        }

        // Go: sseq > mset.mirror.sseq+1 and dseq == mset.mirror.dseq+1 — gap in origin (deleted/expired)
        // For in-process mirrors we skip gap handling since the origin store handles its own deletions.

        await _targetStore.AppendAsync(message.Subject, message.Payload, ct);
        _expectedOriginSeq = message.Sequence;
        _deliverySeq++;
        LastOriginSequence = message.Sequence;
        LastSyncUtc = DateTime.UtcNow;
        Lag = 0; // In-process mirror receives messages synchronously, so lag is always zero here.
    }

    /// <summary>
    /// Enqueues a message for processing by the background sync loop.
    /// Used when messages arrive asynchronously (e.g., from a pull consumer on the origin).
    /// </summary>
    public bool TryEnqueue(StoredMessage message)
    {
        return _inbound.Writer.TryWrite(message);
    }

    /// <summary>
    /// Starts the background sync loop that drains the inbound channel and applies
    /// messages to the local store. This models Go's processMirrorMsgs goroutine.
    /// Go reference: server/stream.go:2788-2854 (processMirrorMsgs)
    /// </summary>
    public void StartSyncLoop()
    {
        lock (_gate)
        {
            if (_syncLoop is not null && !_syncLoop.IsCompleted)
                return;

            _cts = new CancellationTokenSource();
            _syncLoop = RunSyncLoopAsync(_cts.Token);
        }
    }

    /// <summary>
    /// Starts the background sync loop with a pull-based fetch from the origin store.
    /// This models Go's setupMirrorConsumer + processMirrorMsgs pattern where the mirror
    /// actively pulls batches from the origin.
    /// Go reference: server/stream.go:3125-3400 (setupMirrorConsumer)
    /// </summary>
    public void StartPullSyncLoop(IStreamStore originStore, int batchSize = DefaultBatchSize)
    {
        lock (_gate)
        {
            if (_syncLoop is not null && !_syncLoop.IsCompleted)
                return;

            _cts = new CancellationTokenSource();
            _syncLoop = RunPullSyncLoopAsync(originStore, batchSize, _cts.Token);
        }
    }

    /// <summary>
    /// Stops the background sync loop and waits for it to complete.
    /// Go reference: server/stream.go:3027-3032 (cancelMirrorConsumer)
    /// </summary>
    public async Task StopAsync()
    {
        CancellationTokenSource? cts;
        Task? loop;
        lock (_gate)
        {
            cts = _cts;
            loop = _syncLoop;
        }

        if (cts is not null)
        {
            await cts.CancelAsync();
            if (loop is not null)
            {
                try { await loop; }
                catch (OperationCanceledException) { }
            }
        }

        lock (_gate)
        {
            _cts?.Dispose();
            _cts = null;
            _syncLoop = null;
        }
    }

    // -------------------------------------------------------------------------
    // Public retry / failure tracking API
    // Go reference: server/stream.go:3478-3505 (calculateRetryBackoff),
    //               server/stream.go:3125-3400 (setupMirrorConsumer retry logic)
    // -------------------------------------------------------------------------

    /// <summary>
    /// Increments the consecutive failure counter.
    /// Call this when a sync attempt fails.
    /// </summary>
    public void RecordFailure()
    {
        lock (_gate) _consecutiveFailures++;
    }

    /// <summary>
    /// Resets the consecutive failure counter to zero.
    /// Call this when a sync attempt succeeds.
    /// </summary>
    public void RecordSuccess()
    {
        lock (_gate) _consecutiveFailures = 0;
    }

    /// <summary>
    /// Returns the exponential backoff delay for the current failure count,
    /// without jitter so that tests get deterministic results.
    /// Go reference: server/stream.go:3478-3505 (calculateRetryBackoff)
    /// </summary>
    public TimeSpan GetRetryDelay()
    {
        var failures = ConsecutiveFailures;
        if (failures == 0) return InitialRetryDelay;
        var baseDelay = InitialRetryDelay.TotalMilliseconds * Math.Pow(2, Math.Min(failures - 1, 10));
        var capped = Math.Min(baseDelay, MaxRetryDelay.TotalMilliseconds);
        return TimeSpan.FromMilliseconds(capped);
    }

    // -------------------------------------------------------------------------
    // Sequence gap detection
    // Go reference: server/stream.go:2863-3014 (processInboundMirrorMsg)
    // -------------------------------------------------------------------------

    /// <summary>
    /// Records the next received sequence number from the origin stream.
    /// Sets gap state when a gap (skipped sequences) is detected.
    /// </summary>
    public void RecordSourceSeq(ulong seq)
    {
        if (_expectedOriginSeq > 0 && seq > _expectedOriginSeq + 1)
        {
            _hasGap = true;
            _gapStart = _expectedOriginSeq + 1;
            _gapEnd = seq - 1;
        }
        _expectedOriginSeq = seq;
    }

    // -------------------------------------------------------------------------
    // Error state management
    // -------------------------------------------------------------------------

    /// <summary>Sets the coordinator into an error state with the given message.</summary>
    public void SetError(string message) => _errorMessage = message;

    /// <summary>Clears the error state.</summary>
    public void ClearError() => _errorMessage = null;

    /// <summary>
    /// Reports current health state for monitoring.
    /// Go reference: server/stream.go:2739-2743 (mirrorInfo), 2698-2736 (sourceInfo)
    /// </summary>
    public MirrorHealthReport GetHealthReport(ulong? originLastSeq = null)
    {
        var lag = originLastSeq.HasValue && originLastSeq.Value > LastOriginSequence
            ? originLastSeq.Value - LastOriginSequence
            : Lag;

        return new MirrorHealthReport
        {
            LastOriginSequence = LastOriginSequence,
            LastSyncUtc = LastSyncUtc,
            Lag = lag,
            ConsecutiveFailures = ConsecutiveFailures,
            IsRunning = IsRunning,
            IsStalled = LastSyncUtc != default
                        && DateTime.UtcNow - LastSyncUtc > HealthCheckInterval,
        };
    }

    /// <summary>
    /// Returns a structured monitoring response for this mirror.
    /// Go reference: server/stream.go:2739-2743 (mirrorInfo building StreamSourceInfo)
    /// </summary>
    public MirrorInfoResponse GetMirrorInfo(string streamName)
    {
        var report = GetHealthReport();
        return new MirrorInfoResponse
        {
            Name = streamName,
            Lag = report.Lag,
            Active = report.LastSyncUtc != default ? (long)(DateTime.UtcNow - report.LastSyncUtc).TotalMilliseconds : -1,
            Error = ErrorMessage,
        };
    }

    public async ValueTask DisposeAsync()
    {
        await StopAsync();
        _inbound.Writer.TryComplete();
    }

    // -------------------------------------------------------------------------
    // Background sync loop: channel-based (inbound messages pushed to us)
    // Go reference: server/stream.go:2788-2854 (processMirrorMsgs main loop)
    // -------------------------------------------------------------------------
    private async Task RunSyncLoopAsync(CancellationToken ct)
    {
        // Go: t := time.NewTicker(sourceHealthCheckInterval)
        using var healthTimer = new PeriodicTimer(HealthCheckInterval);
        var reader = _inbound.Reader;

        while (!ct.IsCancellationRequested)
        {
            try
            {
                // Go: select { case <-msgs.ch: ... case <-t.C: ... }
                // We process all available messages, then wait for more or health check.
                while (reader.TryRead(out var msg))
                {
                    await ProcessInboundMessageAsync(msg, ct);
                }

                // Wait for either a new message or health check tick
                var readTask = reader.WaitToReadAsync(ct).AsTask();
                var healthTask = healthTimer.WaitForNextTickAsync(ct).AsTask();
                await Task.WhenAny(readTask, healthTask);

                if (ct.IsCancellationRequested)
                    break;

                // Drain any messages that arrived
                while (reader.TryRead(out var msg2))
                {
                    await ProcessInboundMessageAsync(msg2, ct);
                }
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                break;
            }
            catch (Exception)
            {
                // Go: mset.retryMirrorConsumer() on errors
                lock (_gate)
                {
                    _consecutiveFailures++;
                }

                var delay = CalculateBackoff(_consecutiveFailures);
                try { await Task.Delay(delay, ct); }
                catch (OperationCanceledException) { break; }
            }
        }
    }

    // -------------------------------------------------------------------------
    // Background sync loop: pull-based (we fetch from origin)
    // Go reference: server/stream.go:3125-3400 (setupMirrorConsumer creates
    //               ephemeral pull consumer; processMirrorMsgs drains it)
    // -------------------------------------------------------------------------
    private async Task RunPullSyncLoopAsync(IStreamStore originStore, int batchSize, CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                var messages = await originStore.ListAsync(ct);
                var applied = 0;

                foreach (var msg in messages)
                {
                    if (ct.IsCancellationRequested) break;

                    // Skip messages we've already synced
                    if (msg.Sequence <= LastOriginSequence)
                        continue;

                    await ProcessInboundMessageAsync(msg, ct);
                    applied++;

                    if (applied >= batchSize)
                        break;
                }

                // Update lag based on origin state
                if (messages.Count > 0)
                {
                    var originLast = messages[^1].Sequence;
                    Lag = originLast > LastOriginSequence ? originLast - LastOriginSequence : 0;
                }

                lock (_gate) _consecutiveFailures = 0;

                // Go: If caught up, wait briefly before next poll
                if (applied == 0)
                {
                    try { await Task.Delay(HeartbeatInterval, ct); }
                    catch (OperationCanceledException) { break; }
                }
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                break;
            }
            catch (Exception)
            {
                lock (_gate) _consecutiveFailures++;
                var delay = CalculateBackoff(_consecutiveFailures);
                try { await Task.Delay(delay, ct); }
                catch (OperationCanceledException) { break; }
            }
        }
    }

    // Go reference: server/stream.go:2863-3014 (processInboundMirrorMsg)
    private async Task ProcessInboundMessageAsync(StoredMessage message, CancellationToken ct)
    {
        // Go: sseq <= mset.mirror.sseq — ignore older messages
        if (_expectedOriginSeq > 0 && message.Sequence <= _expectedOriginSeq)
            return;

        // Go: dc > 1 — skip redelivered messages
        if (message.Redelivered)
            return;

        // Go: sseq == mset.mirror.sseq+1 — normal sequential delivery
        // Go: else — gap handling (skip sequences if deliver seq matches)
        await _targetStore.AppendAsync(message.Subject, message.Payload, ct);
        _expectedOriginSeq = message.Sequence;
        _deliverySeq++;
        LastOriginSequence = message.Sequence;
        LastSyncUtc = DateTime.UtcNow;

        lock (_gate) _consecutiveFailures = 0;
    }

    // Go reference: server/stream.go:3478-3505 (calculateRetryBackoff in setupSourceConsumer)
    // Exponential backoff with jitter, capped at MaxRetryDelay.
    private static TimeSpan CalculateBackoff(int failures)
    {
        var baseDelay = InitialRetryDelay.TotalMilliseconds * Math.Pow(2, Math.Min(failures - 1, 10));
        var capped = Math.Min(baseDelay, MaxRetryDelay.TotalMilliseconds);
        var jitter = Random.Shared.NextDouble() * 0.2 * capped; // +-20% jitter
        return TimeSpan.FromMilliseconds(capped + jitter);
    }
}

/// <summary>
/// Health report for a mirror coordinator, used by monitoring endpoints.
/// Go reference: server/stream.go:2698-2736 (sourceInfo/StreamSourceInfo)
/// </summary>
public sealed record MirrorHealthReport
{
    public ulong LastOriginSequence { get; init; }
    public DateTime LastSyncUtc { get; init; }
    public ulong Lag { get; init; }
    public int ConsecutiveFailures { get; init; }
    public bool IsRunning { get; init; }
    public bool IsStalled { get; init; }
}

/// <summary>
/// Structured monitoring response for a mirror, as returned by the JetStream monitoring API.
/// Go reference: server/stream.go:2739-2743 (mirrorInfo / StreamSourceInfo)
/// </summary>
public sealed record MirrorInfoResponse
{
    /// <summary>Name of the stream being mirrored.</summary>
    public string Name { get; init; } = string.Empty;

    /// <summary>Number of messages behind the origin stream.</summary>
    public ulong Lag { get; init; }

    /// <summary>
    /// Milliseconds since the last successful sync.
    /// -1 when the mirror has never synced.
    /// </summary>
    public long Active { get; init; } = -1;

    /// <summary>Current error message, or null if no error.</summary>
    public string? Error { get; init; }
}
