// Copyright 2019-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/filestore.go (msgBlock struct and consumerFileStore struct)

using System.Text.Json;
using System.Threading.Channels;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

// ---------------------------------------------------------------------------
// MessageBlock
// ---------------------------------------------------------------------------

/// <summary>
/// Represents a single on-disk message block file together with its
/// in-memory cache and index state.
/// Mirrors the <c>msgBlock</c> struct in filestore.go.
/// </summary>
internal sealed class MessageBlock
{
    // ------------------------------------------------------------------
    // Identity fields — first/last use volatile-style access in Go via
    // atomic.LoadUint64 on the embedded msgId structs.
    // We replicate those as plain fields; callers must acquire _mu before
    // reading/writing unless using the Interlocked helpers on Seq/Ts.
    // ------------------------------------------------------------------

    /// <summary>First message in this block (sequence + nanosecond timestamp).</summary>
    public MsgId First;

    /// <summary>Last message in this block (sequence + nanosecond timestamp).</summary>
    public MsgId Last;

    // ------------------------------------------------------------------
    // Lock
    // ------------------------------------------------------------------

    /// <summary>Guards all mutable fields in this block.</summary>
    public readonly ReaderWriterLockSlim Mu = new(LockRecursionPolicy.NoRecursion);

    // ------------------------------------------------------------------
    // Back-reference
    // ------------------------------------------------------------------

    /// <summary>Owning file store.</summary>
    public JetStreamFileStore? Fs { get; set; }

    // ------------------------------------------------------------------
    // File I/O
    // ------------------------------------------------------------------

    /// <summary>Path to the <c>.blk</c> message data file.</summary>
    public string Mfn { get; set; } = string.Empty;

    /// <summary>Open file stream for the block data file (null when closed/idle).</summary>
    public FileStream? Mfd { get; set; }

    /// <summary>Path to the per-block encryption key file.</summary>
    public string Kfn { get; set; } = string.Empty;

    // ------------------------------------------------------------------
    // Compression
    // ------------------------------------------------------------------

    /// <summary>Effective compression algorithm when the block was last loaded.</summary>
    public StoreCompression Cmp { get; set; }

    /// <summary>
    /// Last index write size in bytes; used to detect whether re-compressing
    /// the block would save meaningful space.
    /// </summary>
    public long Liwsz { get; set; }

    // ------------------------------------------------------------------
    // Block identity
    // ------------------------------------------------------------------

    /// <summary>Monotonically increasing block index number (used in file names).</summary>
    public uint Index { get; set; }

    // ------------------------------------------------------------------
    // Counters
    // ------------------------------------------------------------------

    /// <summary>User-visible byte count (excludes deleted-message bytes).</summary>
    public ulong Bytes { get; set; }

    /// <summary>
    /// Total raw byte count including deleted messages.
    /// Used to decide when to roll to a new block.
    /// </summary>
    public ulong RBytes { get; set; }

    /// <summary>Byte count captured at the last compaction (0 if never compacted).</summary>
    public ulong CBytes { get; set; }

    /// <summary>User-visible message count (excludes deleted messages).</summary>
    public ulong Msgs { get; set; }

    // ------------------------------------------------------------------
    // Per-subject state
    // ------------------------------------------------------------------

    /// <summary>
    /// Optional per-subject state tree for this block.
    /// Lazily populated and expired when idle.
    /// Mirrors <c>mb.fss</c> in filestore.go.
    /// </summary>
    public SubjectTree<SimpleState>? Fss { get; set; }

    // ------------------------------------------------------------------
    // Deleted-sequence tracking
    // ------------------------------------------------------------------

    /// <summary>
    /// Set of deleted sequence numbers within this block.
    /// Uses the AVL-backed <see cref="SequenceSet"/> to match Go's <c>avl.SequenceSet</c>.
    /// </summary>
    public SequenceSet Dmap { get; set; } = new();

    // ------------------------------------------------------------------
    // Timestamps (nanosecond Unix times, matches Go int64)
    // ------------------------------------------------------------------

    /// <summary>Nanosecond timestamp of the last write to this block.</summary>
    public long Lwts { get; set; }

    /// <summary>Nanosecond timestamp of the last load (cache fill) of this block.</summary>
    public long Llts { get; set; }

    /// <summary>Nanosecond timestamp of the last read from this block.</summary>
    public long Lrts { get; set; }

    /// <summary>Nanosecond timestamp of the last subject-state (fss) access.</summary>
    public long Lsts { get; set; }

    /// <summary>Last sequence that was looked up; used to detect linear scans.</summary>
    public ulong Llseq { get; set; }

    // ------------------------------------------------------------------
    // Cache
    // ------------------------------------------------------------------

    /// <summary>
    /// Active in-memory cache. May be null when evicted.
    /// Mirrors <c>mb.cache</c>; the elastic-pointer field (<c>mb.ecache</c>) is
    /// not ported — a plain nullable field is sufficient here.
    /// </summary>
    public Cache? CacheData { get; set; }

    /// <summary>Number of times the cache has been (re)loaded from disk.</summary>
    public ulong Cloads { get; set; }

    /// <summary>Cache buffer expiration duration for this block.</summary>
    public TimeSpan Cexp { get; set; }

    /// <summary>Per-block subject-state (fss) expiration duration.</summary>
    public TimeSpan Fexp { get; set; }

    /// <summary>Timer used to expire the cache buffer when idle.</summary>
    public Timer? Ctmr { get; set; }

    // ------------------------------------------------------------------
    // State flags
    // ------------------------------------------------------------------

    /// <summary>Whether the in-memory cache is currently populated.</summary>
    public bool HaveCache => CacheData != null;

    /// <summary>Whether this block has been closed and must not be written to.</summary>
    public bool Closed { get; set; }

    /// <summary>Whether this block has unflushed data that must be synced to disk.</summary>
    public bool NeedSync { get; set; }

    /// <summary>When true every write is immediately synced (SyncAlways mode).</summary>
    public bool SyncAlways { get; set; }

    /// <summary>When true compaction is suppressed for this block.</summary>
    public bool NoCompact { get; set; }

    /// <summary>
    /// When true the block's messages are not tracked in the per-subject index.
    /// Used for blocks that only contain tombstone or deleted markers.
    /// </summary>
    public bool NoTrack { get; set; }

    /// <summary>Whether a background flusher goroutine equivalent is running.</summary>
    public bool Flusher { get; set; }

    /// <summary>Whether a cache-load is currently in progress.</summary>
    public bool Loading { get; set; }

    // ------------------------------------------------------------------
    // Write error
    // ------------------------------------------------------------------

    /// <summary>Captured write error; non-null means the block is in a bad state.</summary>
    public Exception? Werr { get; set; }

    // ------------------------------------------------------------------
    // TTL / scheduling counters
    // ------------------------------------------------------------------

    /// <summary>Number of messages in this block that have a TTL set.</summary>
    public ulong Ttls { get; set; }

    /// <summary>Number of messages in this block that have a schedule set.</summary>
    public ulong Schedules { get; set; }

    // ------------------------------------------------------------------
    // Channels (equivalent to Go chan struct{})
    // ------------------------------------------------------------------

    /// <summary>Flush-request channel: signals the flusher to run.</summary>
    public Channel<byte>? Fch { get; set; }

    /// <summary>Quit channel: closing signals background goroutine equivalents to stop.</summary>
    public Channel<byte>? Qch { get; set; }

    // ------------------------------------------------------------------
    // Checksum
    // ------------------------------------------------------------------

    /// <summary>
    /// Last-check hash: 8-byte rolling checksum of the last validated record.
    /// Mirrors <c>mb.lchk [8]byte</c>.
    /// </summary>
    public byte[] Lchk { get; set; } = new byte[8];

    // ------------------------------------------------------------------
    // Test hook
    // ------------------------------------------------------------------

    /// <summary>When true, simulates a write failure. Used by unit tests only.</summary>
    public bool MockWriteErr { get; set; }
}

// ---------------------------------------------------------------------------
// ConsumerFileStore
// ---------------------------------------------------------------------------

/// <summary>
/// File-backed implementation of <see cref="IConsumerStore"/>.
/// Persists consumer delivery and ack state to a directory under the stream's
/// <c>obs/</c> subdirectory.
/// Mirrors the <c>consumerFileStore</c> struct in filestore.go.
/// </summary>
public sealed class ConsumerFileStore : IConsumerStore
{
    // ------------------------------------------------------------------
    // Fields — mirrors consumerFileStore struct
    // ------------------------------------------------------------------

    private readonly object _mu = new();

    /// <summary>Back-reference to the owning file store.</summary>
    private readonly JetStreamFileStore _fs;

    /// <summary>Consumer metadata (name, created time, config).</summary>
    private FileConsumerInfo _cfg;

    /// <summary>Durable consumer name.</summary>
    private readonly string _name;

    /// <summary>Path to the consumer's state directory.</summary>
    private readonly string _odir;

    /// <summary>Path to the consumer state file (<c>o.dat</c>).</summary>
    private readonly string _ifn;

    /// <summary>Consumer delivery/ack state.</summary>
    private ConsumerState _state = new();

    /// <summary>Flush-request channel.</summary>
    private Channel<byte>? _fch;

    /// <summary>Quit channel.</summary>
    private Channel<byte>? _qch;

    /// <summary>Whether a background flusher is running.</summary>
    private bool _flusher;

    /// <summary>Whether a write is currently in progress.</summary>
    private bool _writing;

    /// <summary>Whether the state is dirty (pending flush).</summary>
    private bool _dirty;

    /// <summary>Whether this consumer store is closed.</summary>
    private bool _closed;

    // ------------------------------------------------------------------
    // Constructor
    // ------------------------------------------------------------------

    /// <summary>
    /// Creates a new file-backed consumer store.
    /// </summary>
    public ConsumerFileStore(JetStreamFileStore fs, FileConsumerInfo cfg, string name, string odir)
    {
        _fs = fs;
        _cfg = cfg;
        _name = name;
        _odir = odir;
        _ifn = Path.Combine(odir, FileStoreDefaults.ConsumerState);
        lock (_mu)
        {
            TryLoadStateLocked();
        }
    }

    // ------------------------------------------------------------------
    // IConsumerStore
    // ------------------------------------------------------------------

    /// <inheritdoc/>
    public void SetStarting(ulong sseq)
    {
        lock (_mu)
        {
            _state.Delivered.Stream = sseq;
            _state.AckFloor.Stream = sseq;
            PersistStateLocked();
        }
    }

    /// <inheritdoc/>
    public void UpdateStarting(ulong sseq)
    {
        lock (_mu)
        {
            if (sseq <= _state.Delivered.Stream)
                return;

            _state.Delivered.Stream = sseq;
            if (_cfg.Config.AckPolicy == AckPolicy.AckNone)
                _state.AckFloor.Stream = sseq;
            PersistStateLocked();
        }
    }

    /// <inheritdoc/>
    public void Reset(ulong sseq)
    {
        lock (_mu)
        {
            _state = new ConsumerState();
            _state.Delivered.Stream = sseq;
            _state.AckFloor.Stream = sseq;
            PersistStateLocked();
        }
    }

    /// <inheritdoc/>
    public bool HasState()
    {
        lock (_mu)
        {
            return _state.Delivered.Consumer != 0 ||
                   _state.Delivered.Stream != 0 ||
                   _state.Pending is { Count: > 0 } ||
                   _state.Redelivered is { Count: > 0 };
        }
    }

    /// <inheritdoc/>
    public void UpdateDelivered(ulong dseq, ulong sseq, ulong dc, long ts)
    {
        lock (_mu)
        {
            if (_closed)
                throw StoreErrors.ErrStoreClosed;

            if (dc != 1 && _cfg.Config.AckPolicy == AckPolicy.AckNone)
                throw StoreErrors.ErrNoAckPolicy;

            if (dseq <= _state.AckFloor.Consumer)
                return;

            if (_cfg.Config.AckPolicy != AckPolicy.AckNone)
            {
                _state.Pending ??= new Dictionary<ulong, Pending>();

                if (sseq <= _state.Delivered.Stream)
                {
                    if (_state.Pending.TryGetValue(sseq, out var pending) && pending != null)
                        pending.Timestamp = ts;
                }
                else
                {
                    _state.Pending[sseq] = new Pending { Sequence = dseq, Timestamp = ts };
                }

                if (dseq > _state.Delivered.Consumer)
                    _state.Delivered.Consumer = dseq;
                if (sseq > _state.Delivered.Stream)
                    _state.Delivered.Stream = sseq;

                if (dc > 1)
                {
                    var maxdc = (ulong)_cfg.Config.MaxDeliver;
                    if (maxdc > 0 && dc > maxdc)
                        _state.Pending.Remove(sseq);

                    _state.Redelivered ??= new Dictionary<ulong, ulong>();
                    if (!_state.Redelivered.TryGetValue(sseq, out var cur) || cur < dc - 1)
                        _state.Redelivered[sseq] = dc - 1;
                }
            }
            else
            {
                if (dseq > _state.Delivered.Consumer)
                {
                    _state.Delivered.Consumer = dseq;
                    _state.AckFloor.Consumer = dseq;
                }
                if (sseq > _state.Delivered.Stream)
                {
                    _state.Delivered.Stream = sseq;
                    _state.AckFloor.Stream = sseq;
                }
            }

            PersistStateLocked();
        }
    }

    /// <inheritdoc/>
    public void UpdateAcks(ulong dseq, ulong sseq)
    {
        lock (_mu)
        {
            if (_closed)
                throw StoreErrors.ErrStoreClosed;

            if (_cfg.Config.AckPolicy == AckPolicy.AckNone)
                throw StoreErrors.ErrNoAckPolicy;

            if (dseq <= _state.AckFloor.Consumer)
                return;

            if (_state.Pending == null || !_state.Pending.ContainsKey(sseq))
            {
                _state.Redelivered?.Remove(sseq);
                throw StoreErrors.ErrStoreMsgNotFound;
            }

            if (_cfg.Config.AckPolicy == AckPolicy.AckAll)
            {
                var sgap = sseq - _state.AckFloor.Stream;
                _state.AckFloor.Consumer = dseq;
                _state.AckFloor.Stream = sseq;

                if (sgap > (ulong)_state.Pending.Count)
                {
                    var toRemove = new List<ulong>();
                    foreach (var kv in _state.Pending)
                        if (kv.Key <= sseq)
                            toRemove.Add(kv.Key);
                    foreach (var key in toRemove)
                    {
                        _state.Pending.Remove(key);
                        _state.Redelivered?.Remove(key);
                    }
                }
                else
                {
                    for (var seq = sseq; seq > sseq - sgap && _state.Pending.Count > 0; seq--)
                    {
                        _state.Pending.Remove(seq);
                        _state.Redelivered?.Remove(seq);
                        if (seq == 0)
                            break;
                    }
                }

                PersistStateLocked();
                return;
            }

            if (_state.Pending.TryGetValue(sseq, out var pending) && pending != null)
            {
                _state.Pending.Remove(sseq);
                if (dseq > pending.Sequence && pending.Sequence > 0)
                    dseq = pending.Sequence;
            }

            if (_state.Pending.Count == 0)
            {
                _state.AckFloor.Consumer = _state.Delivered.Consumer;
                _state.AckFloor.Stream = _state.Delivered.Stream;
            }
            else if (dseq == _state.AckFloor.Consumer + 1)
            {
                _state.AckFloor.Consumer = dseq;
                _state.AckFloor.Stream = sseq;

                if (_state.Delivered.Consumer > dseq)
                {
                    for (var ss = sseq + 1; ss <= _state.Delivered.Stream; ss++)
                    {
                        if (_state.Pending.TryGetValue(ss, out var p) && p != null)
                        {
                            if (p.Sequence > 0)
                            {
                                _state.AckFloor.Consumer = p.Sequence - 1;
                                _state.AckFloor.Stream = ss - 1;
                            }
                            break;
                        }
                    }
                }
            }

            _state.Redelivered?.Remove(sseq);
            PersistStateLocked();
        }
    }

    /// <inheritdoc/>
    public void UpdateConfig(ConsumerConfig cfg)
    {
        lock (_mu)
        {
            _cfg.Config = cfg;
            PersistStateLocked();
        }
    }

    /// <inheritdoc/>
    public void Update(ConsumerState state)
    {
        ArgumentNullException.ThrowIfNull(state);

        if (state.AckFloor.Consumer > state.Delivered.Consumer)
            throw new InvalidOperationException("bad ack floor for consumer");
        if (state.AckFloor.Stream > state.Delivered.Stream)
            throw new InvalidOperationException("bad ack floor for stream");

        lock (_mu)
        {
            if (_closed)
                throw StoreErrors.ErrStoreClosed;

            if (state.Delivered.Consumer < _state.Delivered.Consumer ||
                state.AckFloor.Stream < _state.AckFloor.Stream)
                throw new InvalidOperationException("old update ignored");

            _state = CloneState(state, copyCollections: true);
            PersistStateLocked();
        }
    }

    /// <inheritdoc/>
    public (ConsumerState? State, Exception? Error) State()
    {
        lock (_mu)
        {
            if (_closed)
                return (null, StoreErrors.ErrStoreClosed);
            return (CloneState(_state, copyCollections: true), null);
        }
    }

    /// <inheritdoc/>
    public (ConsumerState? State, Exception? Error) BorrowState()
    {
        lock (_mu)
        {
            if (_closed)
                return (null, StoreErrors.ErrStoreClosed);
            return (CloneState(_state, copyCollections: false), null);
        }
    }

    /// <inheritdoc/>
    public byte[] EncodedState()
    {
        lock (_mu)
        {
            if (_closed)
                throw StoreErrors.ErrStoreClosed;
            return JsonSerializer.SerializeToUtf8Bytes(CloneState(_state, copyCollections: true));
        }
    }

    /// <inheritdoc/>
    public StorageType Type() => StorageType.FileStorage;

    /// <inheritdoc/>
    public void Stop()
    {
        lock (_mu)
        {
            if (_closed)
                return;
            PersistStateLocked();
            _closed = true;
        }
        _fs.RemoveConsumer(this);
    }

    /// <inheritdoc/>
    public void Delete()
    {
        Stop();
        if (Directory.Exists(_odir))
            Directory.Delete(_odir, recursive: true);
    }

    /// <inheritdoc/>
    public void StreamDelete()
        => Stop();

    private void TryLoadStateLocked()
    {
        if (!File.Exists(_ifn))
            return;

        try
        {
            var raw = File.ReadAllBytes(_ifn);
            var loaded = JsonSerializer.Deserialize<ConsumerState>(raw);
            if (loaded != null)
                _state = CloneState(loaded, copyCollections: true);
        }
        catch (Exception)
        {
            _state = new ConsumerState();
        }
    }

    private void PersistStateLocked()
    {
        if (_closed)
            return;

        Directory.CreateDirectory(_odir);
        var encoded = JsonSerializer.SerializeToUtf8Bytes(CloneState(_state, copyCollections: true));
        File.WriteAllBytes(_ifn, encoded);
        _dirty = false;
    }

    private static ConsumerState CloneState(ConsumerState state, bool copyCollections)
    {
        var clone = new ConsumerState
        {
            Delivered = new SequencePair
            {
                Consumer = state.Delivered.Consumer,
                Stream = state.Delivered.Stream,
            },
            AckFloor = new SequencePair
            {
                Consumer = state.AckFloor.Consumer,
                Stream = state.AckFloor.Stream,
            },
        };

        if (state.Pending is { Count: > 0 })
        {
            clone.Pending = new Dictionary<ulong, Pending>(state.Pending.Count);
            foreach (var kv in state.Pending)
            {
                clone.Pending[kv.Key] = new Pending
                {
                    Sequence = kv.Value.Sequence,
                    Timestamp = kv.Value.Timestamp,
                };
            }
        }
        else if (!copyCollections)
        {
            clone.Pending = state.Pending;
        }

        if (state.Redelivered is { Count: > 0 })
            clone.Redelivered = new Dictionary<ulong, ulong>(state.Redelivered);
        else if (!copyCollections)
            clone.Redelivered = state.Redelivered;

        return clone;
    }
}
