using System.Runtime.InteropServices;

namespace ZB.MOM.WW.CBDD.Bson;

/// <summary>
///     12-byte ObjectId compatible with MongoDB ObjectId.
///     Implemented as readonly struct for zero allocation.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 12)]
public readonly struct ObjectId : IEquatable<ObjectId>
{
    [FieldOffset(0)] private readonly int _timestamp;
    [FieldOffset(4)] private readonly long _randomAndCounter;

    /// <summary>
    ///     Empty ObjectId (all zeros)
    /// </summary>
    public static readonly ObjectId Empty = new(0, 0);

    /// <summary>
    ///     Maximum ObjectId (all 0xFF bytes) - useful for range queries
    /// </summary>
    public static readonly ObjectId MaxValue = new(int.MaxValue, long.MaxValue);

    /// <summary>
    ///     Initializes a new instance of the <see cref="ObjectId" /> struct from raw bytes.
    /// </summary>
    /// <param name="bytes">The 12-byte ObjectId value.</param>
    public ObjectId(ReadOnlySpan<byte> bytes)
    {
        if (bytes.Length != 12)
            throw new ArgumentException("ObjectId must be exactly 12 bytes", nameof(bytes));

        _timestamp = BitConverter.ToInt32(bytes[..4]);
        _randomAndCounter = BitConverter.ToInt64(bytes[4..12]);
    }

    /// <summary>
    ///     Initializes a new instance of the <see cref="ObjectId" /> struct from its components.
    /// </summary>
    /// <param name="timestamp">The Unix timestamp portion.</param>
    /// <param name="randomAndCounter">The random and counter portion.</param>
    public ObjectId(int timestamp, long randomAndCounter)
    {
        _timestamp = timestamp;
        _randomAndCounter = randomAndCounter;
    }

    /// <summary>
    ///     Creates a new ObjectId with current timestamp
    /// </summary>
    public static ObjectId NewObjectId()
    {
        var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        long random = Random.Shared.NextInt64();
        return new ObjectId(timestamp, random);
    }

    /// <summary>
    ///     Writes the ObjectId to the destination span (must be 12 bytes)
    /// </summary>
    /// <param name="destination">The destination span to write into.</param>
    public void WriteTo(Span<byte> destination)
    {
        if (destination.Length < 12)
            throw new ArgumentException("Destination must be at least 12 bytes", nameof(destination));

        BitConverter.TryWriteBytes(destination[..4], _timestamp);
        BitConverter.TryWriteBytes(destination[4..12], _randomAndCounter);
    }

    /// <summary>
    ///     Converts ObjectId to byte array
    /// </summary>
    public byte[] ToByteArray()
    {
        var bytes = new byte[12];
        WriteTo(bytes);
        return bytes;
    }

    /// <summary>
    ///     Gets timestamp portion as UTC DateTime
    /// </summary>
    public DateTime Timestamp => DateTimeOffset.FromUnixTimeSeconds(_timestamp).UtcDateTime;

    /// <summary>
    ///     Determines whether this instance and another <see cref="ObjectId" /> have the same value.
    /// </summary>
    /// <param name="other">The object to compare with this instance.</param>
    /// <returns><see langword="true" /> if the values are equal; otherwise, <see langword="false" />.</returns>
    public bool Equals(ObjectId other)
    {
        return _timestamp == other._timestamp && _randomAndCounter == other._randomAndCounter;
    }

    /// <inheritdoc />
    public override bool Equals(object? obj)
    {
        return obj is ObjectId other && Equals(other);
    }

    /// <inheritdoc />
    public override int GetHashCode()
    {
        return HashCode.Combine(_timestamp, _randomAndCounter);
    }

    public static bool operator ==(ObjectId left, ObjectId right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(ObjectId left, ObjectId right)
    {
        return !left.Equals(right);
    }

    /// <inheritdoc />
    public override string ToString()
    {
        Span<byte> bytes = stackalloc byte[12];
        WriteTo(bytes);
        return Convert.ToHexString(bytes).ToLowerInvariant();
    }
}