// 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

using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

// ---------------------------------------------------------------------------
// FileStoreConfig
// ---------------------------------------------------------------------------

/// <summary>
/// Configuration for a file-backed JetStream stream store.
/// Mirrors <c>FileStoreConfig</c> in filestore.go.
/// </summary>
public sealed class FileStoreConfig
{
    /// <summary>Parent directory for all storage.</summary>
    public string StoreDir { get; set; } = string.Empty;

    /// <summary>
    /// File block size. Also represents the maximum per-block overhead.
    /// Defaults to <see cref="FileStoreDefaults.DefaultBlockSize"/>.
    /// </summary>
    public ulong BlockSize { get; set; }

    /// <summary>How long with no activity until the in-memory cache is expired.</summary>
    public TimeSpan CacheExpire { get; set; }

    /// <summary>How long with no activity until a message block's subject state is expired.</summary>
    public TimeSpan SubjectStateExpire { get; set; }

    /// <summary>How often the store syncs data to disk in the background.</summary>
    public TimeSpan SyncInterval { get; set; }

    /// <summary>When true, every write is immediately synced to disk.</summary>
    public bool SyncAlways { get; set; }

    /// <summary>When true, write operations may be batched and flushed asynchronously.</summary>
    public bool AsyncFlush { get; set; }

    /// <summary>Encryption cipher used when encrypting blocks.</summary>
    public StoreCipher Cipher { get; set; }

    /// <summary>Compression algorithm applied to stored blocks.</summary>
    public StoreCompression Compression { get; set; }

    // Internal reference to the owning server — not serialised.
    // Equivalent to srv *Server in Go; kept as object to avoid circular project deps.
    internal object? Server { get; set; }
}

// ---------------------------------------------------------------------------
// FileStreamInfo
// ---------------------------------------------------------------------------

/// <summary>
/// Remembers the creation time alongside the stream configuration.
/// Mirrors <c>FileStreamInfo</c> in filestore.go.
/// </summary>
public sealed class FileStreamInfo
{
    /// <summary>UTC time at which the stream was created.</summary>
    public DateTime Created { get; set; }

    /// <summary>Stream configuration.</summary>
    public StreamConfig Config { get; set; } = new();
}

// ---------------------------------------------------------------------------
// FileConsumerInfo
// ---------------------------------------------------------------------------

/// <summary>
/// Used for creating and restoring consumer stores from disk.
/// Mirrors <c>FileConsumerInfo</c> in filestore.go.
/// </summary>
public sealed class FileConsumerInfo
{
    /// <summary>UTC time at which the consumer was created.</summary>
    public DateTime Created { get; set; }

    /// <summary>Durable consumer name.</summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>Consumer configuration.</summary>
    public ConsumerConfig Config { get; set; } = new();
}

// ---------------------------------------------------------------------------
// Psi — per-subject index entry (internal)
// ---------------------------------------------------------------------------

/// <summary>
/// Per-subject index entry stored in the subject tree.
/// Mirrors the <c>psi</c> struct in filestore.go.
/// </summary>
internal sealed class Psi
{
    /// <summary>Total messages for this subject across all blocks.</summary>
    public ulong Total { get; set; }

    /// <summary>Index of the first block that holds messages for this subject.</summary>
    public uint Fblk { get; set; }

    /// <summary>Index of the last block that holds messages for this subject.</summary>
    public uint Lblk { get; set; }
}

// ---------------------------------------------------------------------------
// Cache — write-through and load cache (internal)
// ---------------------------------------------------------------------------

/// <summary>
/// Write-through caching layer also used when loading messages from disk.
/// Mirrors the <c>cache</c> struct in filestore.go.
/// </summary>
internal sealed class Cache
{
    /// <summary>Raw message data buffer.</summary>
    public byte[] Buf { get; set; } = Array.Empty<byte>();

    /// <summary>Write position into <see cref="Buf"/>.</summary>
    public int Wp { get; set; }

    /// <summary>Per-sequence byte offsets into <see cref="Buf"/>.</summary>
    public uint[] Idx { get; set; } = Array.Empty<uint>();

    /// <summary>First sequence number this cache covers.</summary>
    public ulong Fseq { get; set; }

    /// <summary>No-random-access flag: when true sequential access is assumed.</summary>
    public bool Nra { get; set; }
}

// ---------------------------------------------------------------------------
// MsgId — sequence + timestamp pair (internal)
// ---------------------------------------------------------------------------

/// <summary>
/// Pairs a message sequence number with its nanosecond timestamp.
/// Mirrors the <c>msgId</c> struct in filestore.go.
/// </summary>
internal struct MsgId
{
    /// <summary>Sequence number.</summary>
    public ulong Seq;

    /// <summary>Nanosecond Unix timestamp.</summary>
    public long Ts;
}

// ---------------------------------------------------------------------------
// CompressionInfo — compression metadata
// ---------------------------------------------------------------------------

/// <summary>
/// Compression metadata attached to a message block.
/// Mirrors <c>CompressionInfo</c> in filestore.go.
/// </summary>
public sealed class CompressionInfo
{
    /// <summary>Compression algorithm in use.</summary>
    public StoreCompression Type { get; set; }

    /// <summary>Original (uncompressed) size in bytes.</summary>
    public ulong Original { get; set; }

    /// <summary>Compressed size in bytes.</summary>
    public ulong Compressed { get; set; }

    /// <summary>
    /// Serialises compression metadata as a compact binary prefix.
    /// Format: 'c' 'm' 'p' &lt;algorithmByte&gt; &lt;uvarint originalSize&gt; &lt;uvarint compressedSize&gt;
    /// </summary>
    public byte[] MarshalMetadata()
    {
        Span<byte> scratch = stackalloc byte[32];
        var pos = 0;
        scratch[pos++] = (byte)'c';
        scratch[pos++] = (byte)'m';
        scratch[pos++] = (byte)'p';
        scratch[pos++] = (byte)Type;
        pos += WriteUVarInt(scratch[pos..], Original);
        pos += WriteUVarInt(scratch[pos..], Compressed);
        return scratch[..pos].ToArray();
    }

    /// <summary>
    /// Deserialises compression metadata from a binary buffer.
    /// Returns the number of bytes consumed, or 0 if the buffer does not start with the expected prefix.
    /// </summary>
    public int UnmarshalMetadata(byte[] b)
    {
        ArgumentNullException.ThrowIfNull(b);

        if (b.Length < 4 || b[0] != (byte)'c' || b[1] != (byte)'m' || b[2] != (byte)'p')
            return 0;

        Type = (StoreCompression)b[3];
        var pos = 4;

        if (!TryReadUVarInt(b.AsSpan(pos), out var original, out var used1))
            return 0;
        pos += used1;

        if (!TryReadUVarInt(b.AsSpan(pos), out var compressed, out var used2))
            return 0;
        pos += used2;

        Original = original;
        Compressed = compressed;
        return pos;
    }

    private static int WriteUVarInt(Span<byte> dest, ulong value)
    {
        var i = 0;
        while (value >= 0x80)
        {
            dest[i++] = (byte)(value | 0x80);
            value >>= 7;
        }
        dest[i++] = (byte)value;
        return i;
    }

    private static bool TryReadUVarInt(ReadOnlySpan<byte> src, out ulong value, out int used)
    {
        value = 0;
        used = 0;
        var shift = 0;
        foreach (var b in src)
        {
            value |= (ulong)(b & 0x7F) << shift;
            used++;
            if ((b & 0x80) == 0)
                return true;
            shift += 7;
            if (shift > 63)
                return false;
        }

        value = 0;
        used = 0;
        return false;
    }
}

// ---------------------------------------------------------------------------
// ErrBadMsg — corrupt/malformed message error (internal)
// ---------------------------------------------------------------------------

/// <summary>
/// Indicates a malformed or corrupt message was detected in a block file.
/// Mirrors the <c>errBadMsg</c> type in filestore.go.
/// </summary>
internal sealed class ErrBadMsg : Exception
{
    /// <summary>Path to the block file that contained the bad message.</summary>
    public string FileName { get; }

    /// <summary>Optional additional detail about the corruption.</summary>
    public string Detail { get; }

    public ErrBadMsg(string fileName, string detail = "")
        : base(BuildMessage(fileName, detail))
    {
        FileName = fileName;
        Detail = detail;
    }

    /// <summary>
    /// Returns the formatted error text.
    /// Mirrors Go's <c>errBadMsg.Error()</c>.
    /// </summary>
    internal string Error()
        => Message;

    private static string BuildMessage(string fileName, string detail)
    {
        var baseName = Path.GetFileName(fileName);
        return string.IsNullOrEmpty(detail)
            ? $"malformed or corrupt message in {baseName}"
            : $"malformed or corrupt message in {baseName}: {detail}";
    }
}

// ---------------------------------------------------------------------------
// FileStoreDefaults — well-known constants
// ---------------------------------------------------------------------------

/// <summary>
/// Well-known constants from filestore.go, exposed for cross-assembly use.
/// </summary>
public static class FileStoreDefaults
{
    // Magic / version markers written into block files.

    /// <summary>Magic byte used to identify file-store block files.</summary>
    public const byte FileStoreMagic = 22;

    /// <summary>Current block file version.</summary>
    public const byte FileStoreVersion = 1;

    /// <summary>New-format index version.</summary>
    internal const byte NewVersion = 2;

    /// <summary>Header length in bytes for block records.</summary>
    internal const int HdrLen = 2;

    // Directory names

    /// <summary>Top-level directory that holds per-stream subdirectories.</summary>
    public const string StreamsDir = "streams";

    /// <summary>Directory that holds in-flight batch data for a stream.</summary>
    public const string BatchesDir = "batches";

    /// <summary>Directory that holds message block files.</summary>
    public const string MsgDir = "msgs";

    /// <summary>Temporary directory name used during a full purge.</summary>
    public const string PurgeDir = "__msgs__";

    /// <summary>Temporary directory name for the new message block during purge.</summary>
    public const string NewMsgDir = "__new_msgs__";

    /// <summary>Directory name that holds per-consumer state.</summary>
    public const string ConsumerDir = "obs";

    // File name patterns

    /// <summary>Format string for block file names (<c>{index}.blk</c>).</summary>
    public const string BlkScan = "{0}.blk";

    /// <summary>Suffix for active block files.</summary>
    public const string BlkSuffix = ".blk";

    /// <summary>Format string for compacted-block staging files (<c>{index}.new</c>).</summary>
    public const string NewScan = "{0}.new";

    /// <summary>Format string for index files (<c>{index}.idx</c>).</summary>
    public const string IndexScan = "{0}.idx";

    /// <summary>Format string for per-block encryption-key files (<c>{index}.key</c>).</summary>
    public const string KeyScan = "{0}.key";

    /// <summary>Glob pattern used to find orphaned key files.</summary>
    public const string KeyScanAll = "*.key";

    /// <summary>Suffix for temporary rewrite/compression staging files.</summary>
    public const string BlkTmpSuffix = ".tmp";

    // Meta files

    /// <summary>Stream / consumer metadata file name.</summary>
    public const string JetStreamMetaFile = "meta.inf";

    /// <summary>Checksum file for the metadata file.</summary>
    public const string JetStreamMetaFileSum = "meta.sum";

    /// <summary>Encrypted metadata key file name.</summary>
    public const string JetStreamMetaFileKey = "meta.key";

    /// <summary>Full stream-state snapshot file name.</summary>
    public const string StreamStateFile = "index.db";

    /// <summary>Encoded TTL hash-wheel persistence file name.</summary>
    public const string TtlStreamStateFile = "thw.db";

    /// <summary>Encoded message-scheduling persistence file name.</summary>
    public const string MsgSchedulingStreamStateFile = "sched.db";

    /// <summary>Consumer state file name inside a consumer directory.</summary>
    public const string ConsumerState = "o.dat";

    // Block size defaults (bytes)

    /// <summary>Default block size for large (limits-based) streams: 8 MB.</summary>
    public const ulong DefaultLargeBlockSize = 8 * 1024 * 1024;

    /// <summary>Default block size for work-queue / interest streams: 4 MB.</summary>
    public const ulong DefaultMediumBlockSize = 4 * 1024 * 1024;

    /// <summary>Default block size used by mirrors/sources: 1 MB.</summary>
    public const ulong DefaultSmallBlockSize = 1 * 1024 * 1024;

    /// <summary>Tiny pool block size (256 KB) — avoids large allocations at low write rates.</summary>
    public const ulong DefaultTinyBlockSize = 256 * 1024;

    /// <summary>Maximum encrypted-head block size: 2 MB.</summary>
    public const ulong MaximumEncryptedBlockSize = 2 * 1024 * 1024;

    /// <summary>Default block size for KV-based streams (same as medium).</summary>
    public const ulong DefaultKvBlockSize = DefaultMediumBlockSize;

    /// <summary>Hard upper limit on block size.</summary>
    public const ulong MaxBlockSize = DefaultLargeBlockSize;

    /// <summary>Minimum allowed block size: 32 KiB.</summary>
    public const ulong FileStoreMinBlkSize = 32 * 1000;

    /// <summary>Maximum allowed block size (same as <see cref="MaxBlockSize"/>).</summary>
    public const ulong FileStoreMaxBlkSize = MaxBlockSize;

    /// <summary>
    /// Default block size exposed publicly; resolves to <see cref="DefaultSmallBlockSize"/> (1 MB)
    /// to match the spec note in the porting plan.
    /// </summary>
    public const ulong DefaultBlockSize = DefaultSmallBlockSize;

    // Timing defaults

    /// <summary>Default duration before an idle cache buffer is expired: 10 seconds.</summary>
    public static readonly TimeSpan DefaultCacheBufferExpiration = TimeSpan.FromSeconds(10);

    /// <summary>Default interval for background disk sync: 2 minutes.</summary>
    public static readonly TimeSpan DefaultSyncInterval = TimeSpan.FromMinutes(2);

    /// <summary>Default idle timeout before file descriptors are closed: 30 seconds.</summary>
    public static readonly TimeSpan CloseFdsIdle = TimeSpan.FromSeconds(30);

    /// <summary>Default expiration time for idle per-block subject state: 2 minutes.</summary>
    public static readonly TimeSpan DefaultFssExpiration = TimeSpan.FromMinutes(2);

    // Thresholds

    /// <summary>Minimum coalesce size for write batching: 16 KiB.</summary>
    public const int CoalesceMinimum = 16 * 1024;

    /// <summary>Maximum wait time when gathering messages to flush: 8 ms.</summary>
    public static readonly TimeSpan MaxFlushWait = TimeSpan.FromMilliseconds(8);

    /// <summary>Minimum block size before compaction is attempted: 2 MB.</summary>
    public const int CompactMinimum = 2 * 1024 * 1024;

    /// <summary>Threshold above which a record length is considered corrupt: 32 MB.</summary>
    public const int RlBadThresh = 32 * 1024 * 1024;

    /// <summary>Size of the per-record hash checksum in bytes.</summary>
    public const int RecordHashSize = 8;

    // Encryption key size minimums

    /// <summary>Minimum size of a metadata encryption key: 64 bytes.</summary>
    internal const int MinMetaKeySize = 64;

    /// <summary>Minimum size of a block encryption key: 64 bytes.</summary>
    internal const int MinBlkKeySize = 64;

    // Cache-index bit flags

    /// <summary>Bit set in a cache index slot to mark that the checksum has been validated.</summary>
    internal const uint Cbit = 1u << 31;

    /// <summary>Bit set in a cache index slot to mark the message as deleted.</summary>
    internal const uint Dbit = 1u << 30;

    /// <summary>Bit set in a record length field to indicate the record has headers.</summary>
    internal const uint Hbit = 1u << 31;

    /// <summary>Bit set in a sequence number to mark an erased message.</summary>
    internal const ulong Ebit = 1UL << 63;

    /// <summary>Bit set in a sequence number to mark a tombstone.</summary>
    internal const ulong Tbit = 1UL << 62;
}
