#nullable enable
using IGP.UnitySDK.Models;
using IGP.UnitySDK;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using IGP.Multiplayer.Protocol;

using SessionProto = IGP.Multiplayer.Generated.IGPProtoContracts.ArenaHostSession;
using IGP.Multiplayer.Models;

namespace IGP.Multiplayer
{
    internal enum IGPHostSessionCommandType
    {
        LeaveRoom = (int)SessionProto.SessionCommandType.LeaveRoom,
        StartGame = (int)SessionProto.SessionCommandType.StartGame,
        RematchGame = (int)SessionProto.SessionCommandType.RematchGame,
        ChangeTeam = (int)SessionProto.SessionCommandType.ChangeTeam,
        RefreshRoom = (int)SessionProto.SessionCommandType.RefreshRoom,
        RequestDataPlane = (int)SessionProto.SessionCommandType.RequestDataPlane,
        UnlockAchievement = (int)SessionProto.SessionCommandType.UnlockAchievement,
        ReportAchievementProgress = (int)SessionProto.SessionCommandType.ReportAchievementProgress,
        ClearAchievements = (int)SessionProto.SessionCommandType.ClearAchievements,
    }

    internal enum IGPHostSessionServerMessageType
    {
        Attached,
        RoomSnapshot,
        CommandResult,
        RoomEvent,
        Detached,
        Error,
        DataPlane,
    }

    internal enum IGPHostedDataPlaneMode
    {
        Unspecified = (int)SessionProto.SessionDataPlaneMode.Unspecified,
        DirectKcp = (int)SessionProto.SessionDataPlaneMode.DirectKcp,
    }

    internal enum IGPHostSessionRoomEventType
    {
        PlayerJoined = (int)SessionProto.SessionRoomEventType.PlayerJoined,
        PlayerLeft = (int)SessionProto.SessionRoomEventType.PlayerLeft,
        HostTransferred = (int)SessionProto.SessionRoomEventType.HostTransferred,
        GameStarted = (int)SessionProto.SessionRoomEventType.GameStarted,
        GameEnded = (int)SessionProto.SessionRoomEventType.GameEnded,
    }

    internal sealed class IGPHostSessionAttachedResponse
    {
        public string RoomId { get; set; } = string.Empty;
        public string PlayerId { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionCommandResult
    {
        public string RequestId { get; set; } = string.Empty;
        public bool Success { get; set; }
        public string Message { get; set; } = string.Empty;
        public string ContentJson { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionDataPlaneResponse
    {
        public string RequestId { get; set; } = string.Empty;
        public bool Success { get; set; }
        public string Message { get; set; } = string.Empty;
        public IGPHostSessionDataPlaneDescriptor? DataPlane { get; set; }
    }

    internal sealed class IGPHostSessionDataPlaneDescriptor
    {
        public IGPHostedDataPlaneMode Mode { get; set; } = IGPHostedDataPlaneMode.Unspecified;
        public string Token { get; set; } = string.Empty;
        public string Host { get; set; } = string.Empty;
        public uint Port { get; set; }
        public ulong ExpiresAtUnixMs { get; set; }
        public long ExpiresAtUnixSeconds { get; set; }
        public int? reliableMessageMaxBytes { get; set; }
        public int? reliableChunkMaxBytes { get; set; }
        public int? kcpDataPlanePayloadMaxBytes { get; set; }
        public int? kcpFrameMaxBytes { get; set; }
        public string unreliableUdpHost { get; set; } = string.Empty;
        public uint unreliableUdpPort { get; set; }
        public string unreliableUdpToken { get; set; } = string.Empty;
        public ulong unreliableUdpExpiresAtUnixMs { get; set; }
        public int? unreliableUdpPayloadMaxBytes { get; set; }
        public string SdkDescriptorJson { get; set; } = string.Empty;
        public string TcpHost { get; set; } = string.Empty;
        public uint TcpPort { get; set; }
        public int? TcpFrameMaxBytes { get; set; }
    }

    internal sealed class IGPHostSessionRoomEvent
    {
        public IGPHostSessionRoomEventType EventType { get; set; }
        public string RoomId { get; set; } = string.Empty;
        public Player? Player { get; set; }
        public string PreviousHostId { get; set; } = string.Empty;
        public string CurrentHostId { get; set; } = string.Empty;
        public string PreviousStatus { get; set; } = string.Empty;
        public string CurrentStatus { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionDetachedEvent
    {
        public string Reason { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionErrorResponse
    {
        public string Code { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionSnapshotEvent
    {
        public string ConnectionStatus { get; set; } = "disconnected";
        public Room? Room { get; set; }
        public Player? CurrentPlayer { get; set; }
    }

    internal sealed class IGPHostSessionServerMessage
    {
        public IGPHostSessionServerMessageType Type { get; set; }
        public IGPHostSessionAttachedResponse? Attached { get; set; }
        public IGPHostSessionSnapshotEvent? RoomSnapshot { get; set; }
        public IGPHostSessionCommandResult? CommandResult { get; set; }
        public IGPHostSessionDataPlaneResponse? DataPlane { get; set; }
        public IGPHostSessionRoomEvent? RoomEvent { get; set; }
        public IGPHostSessionDetachedEvent? Detached { get; set; }
        public IGPHostSessionErrorResponse? Error { get; set; }
    }

    internal static class IGPHostSessionProtocol
    {
        private const int WireTypeVarint = 0;
        private const int WireTypeLengthDelimited = 2;

        public static byte[] EncodeAttachRequest(string secret, string roomId, string playerId)
        {
            var attachBytes = new List<byte>();
            WriteStringField(attachBytes, SessionProto.SessionAttachRequest.Secret, secret);
            WriteStringField(attachBytes, SessionProto.SessionAttachRequest.RoomId, roomId);
            WriteStringField(attachBytes, SessionProto.SessionAttachRequest.PlayerId, playerId);

            var envelope = new List<byte>();
            WriteMessageField(envelope, SessionProto.SessionClientMessage.Attach, attachBytes.ToArray());
            return envelope.ToArray();
        }

        public static byte[] EncodeCommandRequest(
            string requestId,
            IGPHostSessionCommandType command,
            bool? boolValue = null,
            string? stringValue = null,
            string? contentJson = null)
        {
            var commandBytes = new List<byte>();
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.RequestId, requestId);
            WriteVarintField(commandBytes, SessionProto.SessionCommandRequest.Command, (uint)command);
            if (boolValue.HasValue)
            {
                WriteVarintField(commandBytes, SessionProto.SessionCommandRequest.BoolValue, boolValue.Value ? 1u : 0u);
            }

            WriteOptionalStringField(commandBytes, SessionProto.SessionCommandRequest.StringValue, stringValue);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.ContentJson, contentJson);

            var envelope = new List<byte>();
            WriteMessageField(envelope, SessionProto.SessionClientMessage.Command, commandBytes.ToArray());
            return envelope.ToArray();
        }

        public static IGPHostSessionServerMessage DecodeServerMessage(byte[] payload)
        {
            if (payload == null || payload.Length == 0)
            {
                throw new IGPSDKException("Empty host session response");
            }

            var offset = 0;
            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionServerMessage.Attached:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.Attached,
                            Attached = DecodeAttached(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.RoomSnapshot:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.RoomSnapshot,
                            RoomSnapshot = DecodeRoomSnapshot(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.CommandResult:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.CommandResult,
                            CommandResult = DecodeCommandResult(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.RoomEvent:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.RoomEvent,
                            RoomEvent = DecodeRoomEvent(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.Detached:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.Detached,
                            Detached = DecodeDetached(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.Error:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.Error,
                            Error = DecodeError(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.DataPlane:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.DataPlane,
                            DataPlane = DecodeDataPlaneResponse(ReadLengthDelimited(payload, ref offset)),
                        };
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            throw new IGPSDKException("Unsupported host session response payload");
        }

        private static IGPHostSessionAttachedResponse DecodeAttached(byte[] payload)
        {
            var response = new IGPHostSessionAttachedResponse();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionAttachedResponse.RoomId:
                        response.RoomId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionAttachedResponse.PlayerId:
                        response.PlayerId = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return response;
        }

        private static IGPHostSessionSnapshotEvent DecodeRoomSnapshot(byte[] payload)
        {
            var snapshot = new IGPHostSessionSnapshotEvent();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionRoomSnapshotEvent.ConnectionStatus:
                        snapshot.ConnectionStatus = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomSnapshotEvent.Room:
                        snapshot.Room = DecodeRoom(ReadLengthDelimited(payload, ref offset));
                        break;
                    case SessionProto.SessionRoomSnapshotEvent.CurrentPlayer:
                        snapshot.CurrentPlayer = DecodePlayer(ReadLengthDelimited(payload, ref offset));
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return snapshot;
        }

        private static IGPHostSessionCommandResult DecodeCommandResult(byte[] payload)
        {
            var result = new IGPHostSessionCommandResult();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionCommandResponse.RequestId:
                        result.RequestId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionCommandResponse.Success:
                        result.Success = ReadVarint(payload, ref offset) == 1;
                        break;
                    case SessionProto.SessionCommandResponse.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionCommandResponse.ContentJson:
                        result.ContentJson = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionDetachedEvent DecodeDetached(byte[] payload)
        {
            var result = new IGPHostSessionDetachedEvent();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionDetachedEvent.Reason:
                        result.Reason = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionRoomEvent DecodeRoomEvent(byte[] payload)
        {
            var result = new IGPHostSessionRoomEvent();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionRoomEvent.EventType:
                        result.EventType = (IGPHostSessionRoomEventType)ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.RoomId:
                        result.RoomId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.Player:
                        result.Player = DecodePlayer(ReadLengthDelimited(payload, ref offset));
                        break;
                    case SessionProto.SessionRoomEvent.PreviousHostId:
                        result.PreviousHostId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.CurrentHostId:
                        result.CurrentHostId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.PreviousStatus:
                        result.PreviousStatus = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.CurrentStatus:
                        result.CurrentStatus = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionDataPlaneResponse DecodeDataPlaneResponse(byte[] payload)
        {
            var result = new IGPHostSessionDataPlaneResponse();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionDataPlaneResponse.RequestId:
                        result.RequestId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneResponse.Success:
                        result.Success = ReadVarint(payload, ref offset) == 1;
                        break;
                    case SessionProto.SessionDataPlaneResponse.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneResponse.Descriptor:
                        result.DataPlane = DecodeDataPlaneDescriptor(ReadLengthDelimited(payload, ref offset));
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionDataPlaneDescriptor DecodeDataPlaneDescriptor(byte[] payload)
        {
            var result = new IGPHostSessionDataPlaneDescriptor();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionDataPlaneDescriptor.Mode:
                        result.Mode = (IGPHostedDataPlaneMode)ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.Host:
                        result.Host = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.Port:
                        result.Port = ReadVarint32(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.Token:
                        result.Token = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.ExpiresAtUnixMs:
                        result.ExpiresAtUnixMs = ReadVarint64(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.ReliableMessageMaxBytes:
                        result.reliableMessageMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.ReliableChunkMaxBytes:
                        result.reliableChunkMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.KcpDataPlanePayloadMaxBytes:
                        result.kcpDataPlanePayloadMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.KcpFrameMaxBytes:
                        result.kcpFrameMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.UnreliableUdpHost:
                        result.unreliableUdpHost = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.UnreliableUdpPort:
                        result.unreliableUdpPort = ReadVarint32(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.UnreliableUdpToken:
                        result.unreliableUdpToken = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.UnreliableUdpExpiresAtUnixMs:
                        result.unreliableUdpExpiresAtUnixMs = ReadVarint64(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.UnreliableUdpPayloadMaxBytes:
                        result.unreliableUdpPayloadMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.SdkDescriptorJson:
                        result.SdkDescriptorJson = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            ApplySdkDataPlaneDescriptor(result);
            return result;
        }

        private static void ApplySdkDataPlaneDescriptor(IGPHostSessionDataPlaneDescriptor result)
        {
            if (string.IsNullOrWhiteSpace(result.SdkDescriptorJson))
            {
                throw new IGPSDKException(
                    "SDK data-plane descriptor is unavailable",
                    IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID);
            }

            IGPSdkDataPlaneDescriptor? descriptor;
            try
            {
                descriptor = JsonConvert.DeserializeObject<IGPSdkDataPlaneDescriptor>(result.SdkDescriptorJson);
            }
            catch (JsonException exception)
            {
                throw new IGPSDKException(
                    "Invalid SDK data-plane descriptor",
                    IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID,
                    exception);
            }

            if (descriptor == null)
            {
                throw new IGPSDKException(
                    "Invalid SDK data-plane descriptor",
                    IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID);
            }
            if (descriptor.Version != 1)
            {
                throw new IGPSDKException(
                    $"Unsupported SDK data-plane descriptor version: {descriptor.Version}",
                    IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID);
            }

            if (!descriptor.ExpiresAtUnixSeconds.HasValue || descriptor.ExpiresAtUnixSeconds.Value <= 0)
            {
                throw new IGPSDKException(
                    "Invalid SDK data-plane descriptor expiration",
                    IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID);
            }
            if (descriptor.ExpiresAtUnixSeconds.Value <= DateTimeOffset.UtcNow.ToUnixTimeSeconds())
            {
                throw new IGPSDKException("SDK data-plane descriptor has expired", IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID);
            }
            if (string.IsNullOrWhiteSpace(descriptor.Token) ||
                !string.Equals(descriptor.Token, result.Token, StringComparison.Ordinal))
            {
                throw new IGPSDKException("SDK data-plane descriptor token does not match the legacy view", IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID);
            }

            result.ExpiresAtUnixSeconds = descriptor.ExpiresAtUnixSeconds.Value;
            if (descriptor.Transports?.Tcp != null)
            {
                IGPSdkTcpDataPlaneDescriptor tcp = descriptor.Transports.Tcp;
                if (string.IsNullOrWhiteSpace(tcp.Host) ||
                    !tcp.Port.HasValue || tcp.Port.Value < 1 || tcp.Port.Value > ushort.MaxValue ||
                    !tcp.FrameMaxBytes.HasValue ||
                    tcp.FrameMaxBytes.Value <= IGPDataPlaneMessageCodec.MaxEnvelopeHeaderBytes ||
                    tcp.FrameMaxBytes.Value > IGPTcpClient.HardFrameMaxBytes)
                {
                    throw new IGPSDKException("Invalid TCP data-plane descriptor", IGPMultiplayerErrorCodes.ERR_TCP_DESCRIPTOR_INVALID);
                }

                result.TcpHost = tcp.Host;
                result.TcpPort = checked((uint)tcp.Port.Value);
                result.TcpFrameMaxBytes = tcp.FrameMaxBytes.Value;
            }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private sealed class IGPSdkDataPlaneDescriptor
        {
            [JsonProperty("version")]
            public int Version { get; set; }

            [JsonProperty("token")]
            public string Token { get; set; } = string.Empty;

            [JsonProperty("expiresAtUnixSeconds")]
            public long? ExpiresAtUnixSeconds { get; set; }

            [JsonProperty("transports")]
            public IGPSdkDataPlaneTransports? Transports { get; set; }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private sealed class IGPSdkDataPlaneTransports
        {
            [JsonProperty("tcp")]
            public IGPSdkTcpDataPlaneDescriptor? Tcp { get; set; }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private sealed class IGPSdkTcpDataPlaneDescriptor
        {
            [JsonProperty("host")]
            public string Host { get; set; } = string.Empty;

            [JsonProperty("port")]
            public int? Port { get; set; }

            [JsonProperty("frameMaxBytes")]
            public int? FrameMaxBytes { get; set; }
        }

        private static IGPHostSessionErrorResponse DecodeError(byte[] payload)
        {
            var result = new IGPHostSessionErrorResponse();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.SessionError.Code:
                        result.Code = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionError.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static Room DecodeRoom(byte[] payload)
        {
            var room = new Room
            {
                players = new Dictionary<string, Player>(),
            };
            var teams = new List<Team>();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.RoomSnapshot.Id:
                        room.id = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Code:
                        room.code = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Name:
                        room.name = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.GameId:
                        room.gameId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Status:
                        room.status = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.HostId:
                        room.hostId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.MaxPlayers:
                        room.maxPlayers = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.MapPublicId:
                        room.mapPublicId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.MapVersionId:
                        room.mapVersionId = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Players:
                    {
                        var player = DecodePlayer(ReadLengthDelimited(payload, ref offset));
                        if (!string.IsNullOrEmpty(player.id))
                        {
                            room.players[player.id] = player;
                        }
                        break;
                    }
                    case SessionProto.RoomSnapshot.Teams:
                        teams.Add(DecodeTeam(ReadLengthDelimited(payload, ref offset)));
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            room.teams = teams.ToArray();
            return room;
        }

        private static Player DecodePlayer(byte[] payload)
        {
            var player = new Player
            {
                state = new Dictionary<string, object>(),
            };
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.PlayerSnapshot.Id:
                        player.id = ReadString(payload, ref offset);
                        break;
                    case SessionProto.PlayerSnapshot.Name:
                        player.name = ReadString(payload, ref offset);
                        break;
                    case SessionProto.PlayerSnapshot.IsReady:
                        player.isReady = ReadVarint(payload, ref offset) == 1;
                        break;
                    case SessionProto.PlayerSnapshot.TeamId:
                        player.teamId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.PlayerSnapshot.IsBot:
                        player.isBot = ReadVarint(payload, ref offset) == 1;
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return player;
        }

        private static Team DecodeTeam(byte[] payload)
        {
            var team = new Team();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case SessionProto.TeamSnapshot.Id:
                        team.id = ReadString(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.Name:
                        team.name = ReadString(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.Color:
                        team.color = ReadString(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.MaxPlayers:
                        team.maxPlayers = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.PlayerCount:
                        team.playerCount = ReadVarint(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return team;
        }

        private static void WriteStringField(List<byte> buffer, int fieldNumber, string? value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return;
            }

            WriteTag(buffer, fieldNumber, WireTypeLengthDelimited);
            var valueBytes = Encoding.UTF8.GetBytes(value);
            WriteVarint(buffer, (uint)valueBytes.Length);
            buffer.AddRange(valueBytes);
        }

        private static void WriteOptionalStringField(List<byte> buffer, int fieldNumber, string? value)
        {
            if (value == null)
            {
                return;
            }

            WriteTag(buffer, fieldNumber, WireTypeLengthDelimited);
            var valueBytes = Encoding.UTF8.GetBytes(value);
            WriteVarint(buffer, (uint)valueBytes.Length);
            buffer.AddRange(valueBytes);
        }

        private static void WriteVarintField(List<byte> buffer, int fieldNumber, uint value)
        {
            WriteTag(buffer, fieldNumber, WireTypeVarint);
            WriteVarint(buffer, value);
        }

        private static void WriteMessageField(List<byte> buffer, int fieldNumber, byte[] value)
        {
            if (value == null || value.Length == 0)
            {
                return;
            }

            WriteTag(buffer, fieldNumber, WireTypeLengthDelimited);
            WriteVarint(buffer, (uint)value.Length);
            buffer.AddRange(value);
        }

        private static void WriteTag(List<byte> buffer, int fieldNumber, int wireType)
        {
            WriteVarint(buffer, (uint)((fieldNumber << 3) | wireType));
        }

        private static void WriteVarint(List<byte> buffer, uint value)
        {
            var current = value;
            while (current >= 0x80)
            {
                buffer.Add((byte)((current & 0x7F) | 0x80));
                current >>= 7;
            }

            buffer.Add((byte)current);
        }

        private static int ReadVarint(byte[] buffer, ref int offset)
        {
            var value = ReadVarint64(buffer, ref offset);
            if (value > int.MaxValue)
            {
                throw new IGPSDKException("Protobuf varint exceeds Int32 range");
            }

            return (int)value;
        }

        private static uint ReadVarint32(byte[] buffer, ref int offset)
        {
            var value = ReadVarint64(buffer, ref offset);
            if (value > uint.MaxValue)
            {
                throw new IGPSDKException("Protobuf varint exceeds UInt32 range");
            }

            return (uint)value;
        }

        private static ulong ReadVarint64(byte[] buffer, ref int offset)
        {
            ulong value = 0;

            for (var index = 0; index < 10; index++)
            {
                if (offset >= buffer.Length)
                {
                    throw new IGPSDKException("Invalid protobuf varint");
                }

                var current = buffer[offset++];
                if (index == 9 && (current & 0xFE) != 0)
                {
                    throw new IGPSDKException("Invalid protobuf varint");
                }

                value |= (ulong)(current & 0x7F) << (index * 7);
                if ((current & 0x80) == 0)
                {
                    return value;
                }
            }

            throw new IGPSDKException("Invalid protobuf varint");
        }

        private static byte[] ReadLengthDelimited(byte[] buffer, ref int offset)
        {
            var length = ReadVarint(buffer, ref offset);
            if (length > buffer.Length - offset)
            {
                throw new IGPSDKException("Invalid protobuf length-delimited field");
            }

            var result = new byte[length];
            Buffer.BlockCopy(buffer, offset, result, 0, length);
            offset += length;
            return result;
        }

        private static string ReadString(byte[] buffer, ref int offset)
        {
            var bytes = ReadLengthDelimited(buffer, ref offset);
            return Encoding.UTF8.GetString(bytes);
        }

        private static void SkipField(byte[] buffer, int wireType, ref int offset)
        {
            switch (wireType)
            {
                case WireTypeVarint:
                    ReadVarint64(buffer, ref offset);
                    return;
                case WireTypeLengthDelimited:
                    ReadLengthDelimited(buffer, ref offset);
                    return;
                default:
                    throw new IGPSDKException($"Unsupported protobuf wire type: {wireType}");
            }
        }
    }
}
