using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;

namespace ZB.MOM.WW.CBDD.Bson.IO;

/// <summary>
///     Zero-allocation BSON reader using ReadOnlySpan&lt;byte&gt;.
///     Implemented as ref struct to ensure stack-only allocation.
/// </summary>
public ref struct BsonSpanReader
{
    private ReadOnlySpan<byte> _buffer;
    private readonly ConcurrentDictionary<ushort, string> _keys;

    /// <summary>
    ///     Initializes a new instance of the <see cref="BsonSpanReader" /> struct.
    /// </summary>
    /// <param name="buffer">The BSON buffer to read.</param>
    /// <param name="keys">The reverse key dictionary used for compressed element headers.</param>
    public BsonSpanReader(ReadOnlySpan<byte> buffer, ConcurrentDictionary<ushort, string> keys)
    {
        _buffer = buffer;
        Position = 0;
        _keys = keys;
    }

    /// <summary>
    ///     Gets the current read position in the buffer.
    /// </summary>
    public int Position { get; private set; }

    /// <summary>
    ///     Gets the number of unread bytes remaining in the buffer.
    /// </summary>
    public int Remaining => _buffer.Length - Position;

    /// <summary>
    ///     Reads the document size (first 4 bytes of a BSON document)
    /// </summary>
    public int ReadDocumentSize()
    {
        if (Remaining < 4)
            throw new InvalidOperationException("Not enough bytes to read document size");

        int size = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4));
        Position += 4;
        return size;
    }

    /// <summary>
    ///     Reads a BSON element type
    /// </summary>
    public BsonType ReadBsonType()
    {
        if (Remaining < 1)
            throw new InvalidOperationException("Not enough bytes to read BSON type");

        var type = (BsonType)_buffer[Position];
        Position++;
        return type;
    }

    /// <summary>
    ///     Reads a C-style null-terminated string (e-name in BSON spec)
    /// </summary>
    public string ReadCString()
    {
        int start = Position;
        while (Position < _buffer.Length && _buffer[Position] != 0)
            Position++;

        if (Position >= _buffer.Length)
            throw new InvalidOperationException("Unterminated C-string");

        var nameBytes = _buffer.Slice(start, Position - start);
        Position++; // Skip null terminator

        return Encoding.UTF8.GetString(nameBytes);
    }

    /// <summary>
    ///     Reads a C-string into a destination span. Returns the number of bytes written.
    /// </summary>
    /// <param name="destination">The destination character span.</param>
    public int ReadCString(Span<char> destination)
    {
        int start = Position;
        while (Position < _buffer.Length && _buffer[Position] != 0)
            Position++;

        if (Position >= _buffer.Length)
            throw new InvalidOperationException("Unterminated C-string");

        var nameBytes = _buffer.Slice(start, Position - start);
        Position++; // Skip null terminator

        return Encoding.UTF8.GetChars(nameBytes, destination);
    }

    /// <summary>
    ///     Reads a BSON string (4-byte length + UTF-8 bytes + null terminator)
    /// </summary>
    public string ReadString()
    {
        int length = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4));
        Position += 4;

        if (length < 1)
            throw new InvalidOperationException("Invalid string length");

        var stringBytes = _buffer.Slice(Position, length - 1); // Exclude null terminator
        Position += length;

        return Encoding.UTF8.GetString(stringBytes);
    }

    /// <summary>
    ///     Reads a 32-bit integer.
    /// </summary>
    public int ReadInt32()
    {
        if (Remaining < 4)
            throw new InvalidOperationException("Not enough bytes to read Int32");

        int value = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4));
        Position += 4;
        return value;
    }

    /// <summary>
    ///     Reads a 64-bit integer.
    /// </summary>
    public long ReadInt64()
    {
        if (Remaining < 8)
            throw new InvalidOperationException("Not enough bytes to read Int64");

        long value = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(Position, 8));
        Position += 8;
        return value;
    }

    /// <summary>
    ///     Reads a double-precision floating point value.
    /// </summary>
    public double ReadDouble()
    {
        if (Remaining < 8)
            throw new InvalidOperationException("Not enough bytes to read Double");

        double value = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(Position, 8));
        Position += 8;
        return value;
    }

    /// <summary>
    ///     Reads spatial coordinates from a BSON array [X, Y].
    ///     Returns a (double, double) tuple.
    /// </summary>
    public (double, double) ReadCoordinates()
    {
        // Skip array size (4 bytes)
        Position += 4;

        // Skip element 0 header: Type(1) + Name("0\0") (3 bytes)
        Position += 3;
        double x = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(Position, 8));
        Position += 8;

        // Skip element 1 header: Type(1) + Name("1\0") (3 bytes)
        Position += 3;
        double y = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(Position, 8));
        Position += 8;

        // Skip end of array marker (1 byte)
        Position++;

        return (x, y);
    }

    /// <summary>
    ///     Reads a Decimal128 value.
    /// </summary>
    public decimal ReadDecimal128()
    {
        if (Remaining < 16)
            throw new InvalidOperationException("Not enough bytes to read Decimal128");

        var bits = new int[4];
        bits[0] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4));
        bits[1] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position + 4, 4));
        bits[2] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position + 8, 4));
        bits[3] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position + 12, 4));
        Position += 16;

        return new decimal(bits);
    }

    /// <summary>
    ///     Reads a boolean value.
    /// </summary>
    public bool ReadBoolean()
    {
        if (Remaining < 1)
            throw new InvalidOperationException("Not enough bytes to read Boolean");

        bool value = _buffer[Position] != 0;
        Position++;
        return value;
    }

    /// <summary>
    ///     Reads a BSON DateTime (UTC milliseconds since Unix epoch)
    /// </summary>
    public DateTime ReadDateTime()
    {
        long milliseconds = ReadInt64();
        return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).UtcDateTime;
    }

    /// <summary>
    ///     Reads a BSON DateTime as DateTimeOffset (UTC milliseconds since Unix epoch)
    /// </summary>
    public DateTimeOffset ReadDateTimeOffset()
    {
        long milliseconds = ReadInt64();
        return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds);
    }

    /// <summary>
    ///     Reads a TimeSpan from BSON Int64 (ticks)
    /// </summary>
    public TimeSpan ReadTimeSpan()
    {
        long ticks = ReadInt64();
        return TimeSpan.FromTicks(ticks);
    }

    /// <summary>
    ///     Reads a DateOnly from BSON Int32 (day number)
    /// </summary>
    public DateOnly ReadDateOnly()
    {
        int dayNumber = ReadInt32();
        return DateOnly.FromDayNumber(dayNumber);
    }

    /// <summary>
    ///     Reads a TimeOnly from BSON Int64 (ticks)
    /// </summary>
    public TimeOnly ReadTimeOnly()
    {
        long ticks = ReadInt64();
        return new TimeOnly(ticks);
    }

    /// <summary>
    ///     Reads a GUID value.
    /// </summary>
    public Guid ReadGuid()
    {
        return Guid.Parse(ReadString());
    }

    /// <summary>
    ///     Reads a BSON ObjectId (12 bytes)
    /// </summary>
    public ObjectId ReadObjectId()
    {
        if (Remaining < 12)
            throw new InvalidOperationException("Not enough bytes to read ObjectId");

        var oidBytes = _buffer.Slice(Position, 12);
        Position += 12;
        return new ObjectId(oidBytes);
    }

    /// <summary>
    ///     Reads binary data (subtype + length + bytes)
    /// </summary>
    /// <param name="subtype">When this method returns, contains the BSON binary subtype.</param>
    public ReadOnlySpan<byte> ReadBinary(out byte subtype)
    {
        int length = ReadInt32();

        if (Remaining < 1)
            throw new InvalidOperationException("Not enough bytes to read binary subtype");

        subtype = _buffer[Position];
        Position++;

        if (Remaining < length)
            throw new InvalidOperationException("Not enough bytes to read binary data");

        var data = _buffer.Slice(Position, length);
        Position += length;
        return data;
    }

    /// <summary>
    ///     Skips the current value based on type
    /// </summary>
    /// <param name="type">The BSON type of the value to skip.</param>
    public void SkipValue(BsonType type)
    {
        switch (type)
        {
            case BsonType.Double:
                Position += 8;
                break;
            case BsonType.String:
                int stringLength = ReadInt32();
                Position += stringLength;
                break;
            case BsonType.Document:
            case BsonType.Array:
                int docLength = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4));
                Position += docLength;
                break;
            case BsonType.Binary:
                int binaryLength = ReadInt32();
                Position += 1 + binaryLength; // subtype + data
                break;
            case BsonType.ObjectId:
                Position += 12;
                break;
            case BsonType.Boolean:
                Position += 1;
                break;
            case BsonType.DateTime:
            case BsonType.Int64:
            case BsonType.Timestamp:
                Position += 8;
                break;
            case BsonType.Decimal128:
                Position += 16;
                break;
            case BsonType.Int32:
                Position += 4;
                break;
            case BsonType.Null:
                // No data
                break;
            default:
                throw new NotSupportedException($"Skipping type {type} not supported");
        }
    }

    /// <summary>
    ///     Reads a single byte.
    /// </summary>
    public byte ReadByte()
    {
        if (Remaining < 1)
            throw new InvalidOperationException("Not enough bytes to read byte");
        byte value = _buffer[Position];
        Position++;
        return value;
    }

    /// <summary>
    ///     Peeks a 32-bit integer at the current position without advancing.
    /// </summary>
    public int PeekInt32()
    {
        if (Remaining < 4)
            throw new InvalidOperationException("Not enough bytes to peek Int32");
        return BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4));
    }

    /// <summary>
    ///     Reads an element header key identifier and resolves it to a key name.
    /// </summary>
    public string ReadElementHeader()
    {
        if (Remaining < 2)
            throw new InvalidOperationException("Not enough bytes to read BSON element key ID");

        ushort id = BinaryPrimitives.ReadUInt16LittleEndian(_buffer.Slice(Position, 2));
        Position += 2;

        if (!_keys.TryGetValue(id, out string? key))
            throw new InvalidOperationException($"BSON Key ID {id} not found in reverse key dictionary.");

        return key;
    }

    /// <summary>
    ///     Returns a span containing all unread bytes.
    /// </summary>
    public ReadOnlySpan<byte> RemainingBytes()
    {
        return _buffer[Position..];
    }
}