using NATS.Server.JetStream.Models;
using StorageType = NATS.Server.JetStream.Models.StorageType;

namespace NATS.Server.JetStream.Storage;

// Go: server/filestore.go:11630  consumerFileStore struct
// Go: ConsumerStore interface implements: UpdateDelivered, UpdateAcks, State, Stop, Delete
/// <summary>
/// File-backed consumer state store. Persists consumer delivery progress, ack floor,
/// pending messages, and redelivery counts to a binary state file on disk.
///
/// The state is encoded/decoded using <see cref="ConsumerStateCodec"/> which matches
/// the Go wire format, enabling interoperability.
///
/// Reference: golang/nats-server/server/filestore.go:11630 (consumerFileStore)
/// </summary>
public sealed class ConsumerFileStore : IConsumerStore
{
    private readonly string _stateFile;
    private readonly ConsumerConfig _cfg;
    private ConsumerState _state = new();
    private bool _dirty;
    private bool _closed;
    private bool _inFlusher;
    private readonly CancellationTokenSource _cts = new();
    private readonly Task _flusherTask;
    private readonly Lock _mu = new();

    /// <summary>
    /// True if the background flusher goroutine is running.
    /// Reference: golang/nats-server/server/filestore.go:11640 (qch / flusher pattern)
    /// </summary>
    public bool InFlusher
    {
        get { lock (_mu) return _inFlusher; }
    }

    // Go: ErrNoAckPolicy — returned when UpdateAcks is called on a consumer without explicit ack policy.
    // Reference: golang/nats-server/server/errors.go
    public static readonly Exception ErrNoAckPolicy = new InvalidOperationException("ErrNoAckPolicy");

    public ConsumerFileStore(string stateFile, ConsumerConfig cfg)
    {
        _stateFile = stateFile;
        _cfg = cfg;

        // Load existing state from disk if present.
        if (File.Exists(_stateFile))
        {
            try
            {
                var data = File.ReadAllBytes(_stateFile);
                _state = ConsumerStateCodec.Decode(data);
            }
            catch
            {
                _state = new ConsumerState();
            }
        }

        // Start background flusher task (equivalent to Go goroutine for periodic state flush).
        _flusherTask = RunFlusherAsync(_cts.Token);
    }

    // Go: consumerFileStore.SetStarting — filestore.go:11660
    public void SetStarting(ulong sseq)
    {
        lock (_mu)
        {
            _state.AckFloor = new SequencePair(0, sseq > 0 ? sseq - 1 : 0);
        }
    }

    // Go: consumerFileStore.UpdateStarting — filestore.go:11665
    public void UpdateStarting(ulong sseq)
    {
        lock (_mu)
        {
            _state.AckFloor = new SequencePair(0, sseq > 0 ? sseq - 1 : 0);
        }
    }

    // Go: consumerFileStore.Reset — filestore.go:11670
    public void Reset(ulong sseq)
    {
        lock (_mu)
        {
            _state = new ConsumerState
            {
                AckFloor = new SequencePair(0, sseq > 0 ? sseq - 1 : 0),
            };
            _dirty = true;
        }
    }

    // Go: consumerFileStore.HasState — filestore.go
    public bool HasState()
    {
        lock (_mu)
            return _state.Delivered.Consumer > 0 || _state.AckFloor.Consumer > 0;
    }

    // Go: consumerFileStore.UpdateDelivered — filestore.go:11700
    // dseq=consumer delivery seq, sseq=stream seq, dc=delivery count, ts=Unix nanosec timestamp
    public void UpdateDelivered(ulong dseq, ulong sseq, ulong dc, long ts)
    {
        lock (_mu)
        {
            if (_closed) return;

            // Go: For AckNone, delivery count > 1 is not allowed.
            if (dc != 1 && _cfg.AckPolicy == AckPolicy.None)
                throw (InvalidOperationException)ErrNoAckPolicy;

            _state.Delivered = new SequencePair(dseq, sseq);

            if (_cfg.AckPolicy != AckPolicy.None)
            {
                // Track pending for explicit/all ack policies.
                _state.Pending ??= new Dictionary<ulong, Pending>();
                _state.Pending[sseq] = new Pending(dseq, ts);

                // Track redelivery if dc > 1.
                if (dc > 1)
                {
                    _state.Redelivered ??= new Dictionary<ulong, ulong>();
                    _state.Redelivered[sseq] = dc;
                }
            }
            else
            {
                // AckNone: ack floor advances with delivery.
                _state.AckFloor = new SequencePair(dseq, sseq);
            }

            _dirty = true;
        }
    }

    // Go: consumerFileStore.UpdateAcks — filestore.go:11760
    public void UpdateAcks(ulong dseq, ulong sseq)
    {
        lock (_mu)
        {
            if (_closed) return;

            // Go: AckNone consumers cannot ack.
            if (_cfg.AckPolicy == AckPolicy.None)
                throw (InvalidOperationException)ErrNoAckPolicy;

            // Must exist in pending.
            if (_state.Pending == null || !_state.Pending.ContainsKey(sseq))
                throw new InvalidOperationException($"Sequence {sseq} not found in pending.");

            _state.Pending.Remove(sseq);
            if (_state.Pending.Count == 0)
                _state.Pending = null;

            // Remove from redelivered if present.
            _state.Redelivered?.Remove(sseq);
            if (_state.Redelivered?.Count == 0)
                _state.Redelivered = null;

            // Advance ack floor: find the highest contiguous ack floor.
            // Go: consumerFileStore.UpdateAcks advances AckFloor to the
            //     highest consumer seq / stream seq that are fully acked.
            AdvanceAckFloor(dseq, sseq);

            _dirty = true;
        }
    }

    // Go: consumerFileStore.Update — filestore.go
    public void Update(ConsumerState state)
    {
        lock (_mu)
        {
            _state = state;
            _dirty = true;
        }
    }

    // Go: consumerFileStore.State — filestore.go:12103
    public ConsumerState State()
    {
        lock (_mu)
        {
            // Return a deep copy.
            var copy = new ConsumerState
            {
                Delivered = _state.Delivered,
                AckFloor = _state.AckFloor,
            };

            if (_state.Pending != null)
            {
                copy.Pending = new Dictionary<ulong, Pending>(_state.Pending);
            }

            if (_state.Redelivered != null)
            {
                copy.Redelivered = new Dictionary<ulong, ulong>(_state.Redelivered);
            }

            return copy;
        }
    }

    // Go: consumerFileStore.BorrowState — filestore.go:12109
    public ConsumerState BorrowState()
    {
        lock (_mu) return _state;
    }

    // Go: consumerFileStore.EncodedState — filestore.go
    public byte[] EncodedState()
    {
        lock (_mu)
            return ConsumerStateCodec.Encode(_state);
    }

    // Go: consumerFileStore.Type — filestore.go:12099
    public StorageType Type() => StorageType.File;

    // Go: consumerFileStore.Stop — filestore.go:12327
    public void Stop()
    {
        lock (_mu)
        {
            if (_closed) return;
            _closed = true;
        }

        // Signal flusher to stop and wait.
        _cts.Cancel();
        try { _flusherTask.Wait(TimeSpan.FromMilliseconds(500)); } catch { /* best effort */ }

        // Flush final state.
        FlushState();
    }

    // Go: consumerFileStore.Delete — filestore.go:12382
    public void Delete()
    {
        lock (_mu)
        {
            if (_closed) return;
            _closed = true;
        }

        _cts.Cancel();
        try { _flusherTask.Wait(TimeSpan.FromMilliseconds(200)); } catch { /* best effort */ }

        if (File.Exists(_stateFile))
        {
            try { File.Delete(_stateFile); } catch { /* best effort */ }
        }
    }

    // Go: consumerFileStore.StreamDelete — filestore.go:12387
    public void StreamDelete()
    {
        Delete();
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    /// <summary>
    /// Advances the ack floor to the highest contiguous acknowledged sequence.
    /// Go: consumerFileStore.UpdateAcks — floor advances when no gaps remain in pending.
    /// Reference: golang/nats-server/server/filestore.go:11817
    /// </summary>
    private void AdvanceAckFloor(ulong dseq, ulong sseq)
    {
        // If there are no pending entries, ack floor = delivered.
        if (_state.Pending == null || _state.Pending.Count == 0)
        {
            _state.AckFloor = _state.Delivered;
            return;
        }

        // Go: advance floor by looking at the lowest pending entry.
        // The ack floor is the highest dseq/sseq pair such that all earlier seqs are acked.
        // Simple approach: find the minimum pending stream seq — floor is one below that.
        var minPendingSSeq = ulong.MaxValue;
        ulong minPendingDSeq = 0;
        foreach (var kv in _state.Pending)
        {
            if (kv.Key < minPendingSSeq)
            {
                minPendingSSeq = kv.Key;
                minPendingDSeq = kv.Value.Sequence;
            }
        }

        if (minPendingSSeq == ulong.MaxValue || minPendingDSeq == 0)
        {
            _state.AckFloor = _state.Delivered;
            return;
        }

        // Floor = one below the lowest pending.
        // Go uses AckFloor.Stream as "last fully acked stream seq".
        // We need to find what was acked just before the minimum pending.
        // Use the current dseq/sseq being acked for floor advancement check.
        if (dseq > _state.AckFloor.Consumer && sseq > _state.AckFloor.Stream)
        {
            // Check if dseq is the one just above the current floor.
            if (dseq == _state.AckFloor.Consumer + 1)
            {
                _state.AckFloor = new SequencePair(dseq, sseq);

                // Walk forward while consecutive pending entries are acked.
                // (This is a simplified version; full Go impl tracks ordering explicitly.)
            }
        }
    }

    /// <summary>
    /// Persists the current state to disk.
    /// Reference: golang/nats-server/server/filestore.go:11630 (flusher pattern)
    /// </summary>
    private void FlushState()
    {
        byte[] data;
        lock (_mu)
        {
            if (!_dirty) return;
            data = ConsumerStateCodec.Encode(_state);
            _dirty = false;
        }

        try
        {
            var dir = Path.GetDirectoryName(_stateFile);
            if (!string.IsNullOrEmpty(dir))
                Directory.CreateDirectory(dir);
            File.WriteAllBytes(_stateFile, data);
        }
        catch
        {
            // Best effort; mark dirty again so we retry.
            lock (_mu) _dirty = true;
        }
    }

    /// <summary>
    /// Background flusher that periodically persists dirty state.
    /// Reference: golang/nats-server/server/filestore.go — consumerFileStore flusher goroutine.
    /// </summary>
    private async Task RunFlusherAsync(CancellationToken ct)
    {
        lock (_mu) _inFlusher = true;
        try
        {
            while (!ct.IsCancellationRequested)
            {
                await Task.Delay(TimeSpan.FromMilliseconds(250), ct);
                if (ct.IsCancellationRequested) break;
                FlushState();
            }
        }
        catch (OperationCanceledException) { /* expected */ }
        catch { /* best effort */ }
        finally
        {
            lock (_mu) _inFlusher = false;
        }
    }
}
