#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;

using DesktopProto = IGP.UnitySDK.Generated.IGPProtoContracts.DesktopSession;
using IGP.UnitySDK.Models;

namespace IGP.UnitySDK
{
    internal enum IGPDesktopSessionCommandType
    {
        NamedCommand = (int)DesktopProto.DesktopSessionCommandType.Unspecified,
        PingDesktopSession = (int)DesktopProto.DesktopSessionCommandType.PingDesktopSession,
        UnlockAchievement = (int)DesktopProto.DesktopSessionCommandType.UnlockAchievement,
        ReportAchievementProgress = (int)DesktopProto.DesktopSessionCommandType.ReportAchievementProgress,
        GetDesktopCapabilities = (int)DesktopProto.DesktopSessionCommandType.GetDesktopCapabilities,
        RequestGameAuthorization = (int)DesktopProto.DesktopSessionCommandType.RequestGameAuthorization,
        GetDesktopUserAvatarInfo = (int)DesktopProto.DesktopSessionCommandType.GetDesktopUserAvatarInfo,
        GetDesktopUserAvatar = (int)DesktopProto.DesktopSessionCommandType.GetDesktopUserAvatar,
        GetDesktopAntiAddictionStatus = (int)DesktopProto.DesktopSessionCommandType.GetDesktopAntiAddictionStatus,
        GetDesktopAntiAddictionEvent = (int)DesktopProto.DesktopSessionCommandType.GetDesktopAntiAddictionEvent,
        GetDesktopAntiAddictionAgeRange = (int)DesktopProto.DesktopSessionCommandType.GetDesktopAntiAddictionAgeRange,
        GetDesktopAntiAddictionRemainingTime = (int)DesktopProto.DesktopSessionCommandType.GetDesktopAntiAddictionRemainingTime,
        GetDesktopAntiAddictionProfile = (int)DesktopProto.DesktopSessionCommandType.GetDesktopAntiAddictionProfile,
        CreateDesktopAntiAddictionRealNameUrl = (int)DesktopProto.DesktopSessionCommandType.CreateDesktopAntiAddictionRealNameUrl,
        ClearAchievements = (int)DesktopProto.DesktopSessionCommandType.ClearAchievements,
        RequestHostedBootstrap = (int)DesktopProto.DesktopSessionCommandType.RequestHostedBootstrap,
    }

    internal enum IGPDesktopSessionServerMessageType
    {
        Attached,
        CommandResult,
        Detached,
        Error,
        AntiAddictionChanged,
        HostedBootstrapAvailable,
    }

    public sealed class IGPDesktopSessionAttachRequest
    {
        public string ProtocolVersion { get; set; } = IGPDesktopSessionProtocol.SupportedProtocolVersion;
        public string SdkVersion { get; set; } = string.Empty;
        public string Engine { get; set; } = "unity";
        public string EngineVersion { get; set; } = string.Empty;
        public int AppId { get; set; }
        public string ExecutablePath { get; set; } = string.Empty;
        public int ProcessId { get; set; }
    }

    public sealed class IGPDesktopSessionAttachResponse
    {
        public string DesktopProtocolVersion { get; set; } = string.Empty;
        public string DesktopSessionId { get; set; } = string.Empty;
        public string UserId { get; set; } = string.Empty;
        public string AccountId { get; set; } = string.Empty;
        public string LoginState { get; set; } = string.Empty;
        public int AppId { get; set; }
        public IGPDesktopCapabilitySet Capabilities { get; set; } = new IGPDesktopCapabilitySet();
        public IGPDesktopUserProfile? UserProfile { get; set; }
        public string ValidatedExecutablePath { get; set; } = string.Empty;
        public string InstallRoot { get; set; } = string.Empty;
        public string ChannelState { get; set; } = string.Empty;
        public string AttachState { get; set; } = string.Empty;
        public string AttachSource { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
        public IGPAntiAddictionStatus? AntiAddictionStatus { get; set; }
    }

    public sealed class IGPDesktopSessionCommandResult
    {
        public string RequestId { get; set; } = string.Empty;
        public bool Success { get; set; }
        public string Code { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
        public string ContentJson { get; set; } = string.Empty;
        public byte[] ContentBytes { get; set; } = Array.Empty<byte>();
    }

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

    internal sealed class IGPDesktopSessionErrorResponse
    {
        public string Code { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
        public string Category { get; set; } = string.Empty;
        public string ChannelState { get; set; } = string.Empty;
        public string AttachState { get; set; } = string.Empty;
    }

    internal sealed class IGPDesktopSessionServerMessage
    {
        public IGPDesktopSessionServerMessageType Type { get; set; }
        public IGPDesktopSessionAttachResponse? Attached { get; set; }
        public IGPDesktopSessionCommandResult? CommandResult { get; set; }
        public IGPDesktopSessionDetachedEvent? Detached { get; set; }
        public IGPDesktopSessionErrorResponse? Error { get; set; }
        public IGPAntiAddictionStatus? AntiAddictionChanged { get; set; }
        public IGPDesktopHostedBootstrapAvailableEvent? HostedBootstrapAvailable { get; set; }
    }

    internal static class IGPDesktopSessionProtocol
    {
        public const string SupportedProtocolVersion = "1";
        public const string HostedBootstrapAvailableRequestId = "desktop-hosted-bootstrap-available";
        public const string HostedBootstrapAvailableCode = "HOSTED_BOOTSTRAP_AVAILABLE";

        private const int DesktopCapabilityLyingBottleField = 7;
        private const int WireTypeVarint = 0;
        private const int WireTypeLengthDelimited = 2;

        public static byte[] EncodeAttachRequest(IGPDesktopSessionAttachRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            var attachBytes = new List<byte>();
            WriteStringField(attachBytes, DesktopProto.DesktopSessionAttachRequest.ProtocolVersion, request.ProtocolVersion);
            WriteStringField(attachBytes, DesktopProto.DesktopSessionAttachRequest.SdkVersion, request.SdkVersion);
            WriteStringField(attachBytes, DesktopProto.DesktopSessionAttachRequest.Engine, request.Engine);
            WriteStringField(attachBytes, DesktopProto.DesktopSessionAttachRequest.EngineVersion, request.EngineVersion);
            WriteVarintField(attachBytes, DesktopProto.DesktopSessionAttachRequest.AppId, (uint)request.AppId);
            WriteStringField(attachBytes, DesktopProto.DesktopSessionAttachRequest.ExecutablePath, request.ExecutablePath);
            if (request.ProcessId > 0)
            {
                WriteVarintField(attachBytes, DesktopProto.DesktopSessionAttachRequest.ProcessId, (uint)request.ProcessId);
            }

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

        public static byte[] EncodeCommandRequest(
            string requestId,
            IGPDesktopSessionCommandType command,
            string? stringValue = null,
            string? contentJson = null)
        {
            var commandBytes = new List<byte>();
            WriteStringField(commandBytes, DesktopProto.DesktopSessionCommandRequest.RequestId, requestId);
            WriteVarintField(commandBytes, DesktopProto.DesktopSessionCommandRequest.Command, (uint)command);
            WriteStringField(commandBytes, DesktopProto.DesktopSessionCommandRequest.StringValue, stringValue);
            WriteStringField(commandBytes, DesktopProto.DesktopSessionCommandRequest.ContentJson, contentJson);

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

        public static byte[] EncodeNamedCommandRequest(
            string requestId,
            string command,
            string? contentJson = null)
        {
            if (string.IsNullOrWhiteSpace(command))
            {
                throw new ArgumentException("Desktop session command is required", nameof(command));
            }

            var commandBytes = new List<byte>();
            WriteStringField(commandBytes, DesktopProto.DesktopSessionCommandRequest.RequestId, requestId);
            WriteStringField(commandBytes, DesktopProto.DesktopSessionCommandRequest.Command, command);
            WriteStringField(commandBytes, DesktopProto.DesktopSessionCommandRequest.ContentJson, contentJson);

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

        // 以下 Encode* server-side API 与 GameMaker bridge 的 DesktopSessionProtocol 字段布局保持完全一致，
        // 以便 Unity 测试夹具 / mock server 能产出真实 desktop 端可解析的 payload。
        public static byte[] EncodeAttachResponse(IGPDesktopSessionAttachResponse response)
        {
            if (response == null)
            {
                throw new ArgumentNullException(nameof(response));
            }

            var body = EncodeAttachResponsePayload(response);
            var envelope = new List<byte>();
            WriteMessageField(envelope, DesktopProto.DesktopSessionServerMessage.Attached, body);
            return envelope.ToArray();
        }

        public static byte[] EncodeCommandResult(IGPDesktopSessionCommandResult result)
        {
            if (result == null)
            {
                throw new ArgumentNullException(nameof(result));
            }

            var body = EncodeCommandResultPayload(result);
            var envelope = new List<byte>();
            WriteMessageField(envelope, DesktopProto.DesktopSessionServerMessage.CommandResult, body);
            return envelope.ToArray();
        }

        public static byte[] EncodeDetached(string? reason)
        {
            var body = new List<byte>();
            WriteStringField(body, DesktopProto.DesktopSessionDetachedEvent.Reason, reason);
            var envelope = new List<byte>();
            WriteMessageField(envelope, DesktopProto.DesktopSessionServerMessage.Detached, body.ToArray());
            return envelope.ToArray();
        }

        public static byte[] EncodeError(string code, string message)
        {
            return EncodeError(code, message, category: null, channelState: null, attachState: null);
        }

        public static byte[] EncodeError(
            string? code,
            string? message,
            string? category,
            string? channelState,
            string? attachState)
        {
            var body = new List<byte>();
            WriteStringField(body, DesktopProto.DesktopSessionError.Code, code);
            WriteStringField(body, DesktopProto.DesktopSessionError.Message, message);
            WriteStringField(body, DesktopProto.DesktopSessionError.Category, category);
            WriteStringField(body, DesktopProto.DesktopSessionError.ChannelState, channelState);
            WriteStringField(body, DesktopProto.DesktopSessionError.AttachState, attachState);
            var envelope = new List<byte>();
            WriteMessageField(envelope, DesktopProto.DesktopSessionServerMessage.Error, body.ToArray());
            return envelope.ToArray();
        }

        public static IGPDesktopSessionServerMessage DecodeServerMessage(byte[] payload)
        {
            if (payload == null || payload.Length == 0)
            {
                throw new IGPSDKException("Empty desktop 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 DesktopProto.DesktopSessionServerMessage.Attached:
                        return new IGPDesktopSessionServerMessage
                        {
                            Type = IGPDesktopSessionServerMessageType.Attached,
                            Attached = DecodeAttachResponse(ReadLengthDelimited(payload, ref offset)),
                        };
                    case DesktopProto.DesktopSessionServerMessage.CommandResult:
                        var commandResult = DecodeCommandResult(ReadLengthDelimited(payload, ref offset));
                        if (TryDecodeHostedBootstrapAvailable(commandResult, out var hostedBootstrapAvailable))
                        {
                            return new IGPDesktopSessionServerMessage
                            {
                                Type = IGPDesktopSessionServerMessageType.HostedBootstrapAvailable,
                                HostedBootstrapAvailable = hostedBootstrapAvailable,
                            };
                        }

                        return new IGPDesktopSessionServerMessage
                        {
                            Type = IGPDesktopSessionServerMessageType.CommandResult,
                            CommandResult = commandResult,
                        };
                    case DesktopProto.DesktopSessionServerMessage.Detached:
                        return new IGPDesktopSessionServerMessage
                        {
                            Type = IGPDesktopSessionServerMessageType.Detached,
                            Detached = DecodeDetached(ReadLengthDelimited(payload, ref offset)),
                        };
                    case DesktopProto.DesktopSessionServerMessage.Error:
                        return new IGPDesktopSessionServerMessage
                        {
                            Type = IGPDesktopSessionServerMessageType.Error,
                            Error = DecodeError(ReadLengthDelimited(payload, ref offset)),
                        };
                    case DesktopProto.DesktopSessionServerMessage.AntiAddictionChanged:
                        return new IGPDesktopSessionServerMessage
                        {
                            Type = IGPDesktopSessionServerMessageType.AntiAddictionChanged,
                            AntiAddictionChanged = DecodeAntiAddictionChangedEvent(ReadLengthDelimited(payload, ref offset)),
                        };
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

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

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

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

                switch (fieldNumber)
                {
                    case DesktopProto.DesktopSessionAttachedResponse.DesktopSessionId:
                        response.DesktopSessionId = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.UserId:
                        response.UserId = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.AccountId:
                        response.AccountId = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.LoginState:
                        response.LoginState = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.AppId:
                        response.AppId = ReadVarint(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.Capabilities:
                        response.Capabilities = DecodeCapabilitySet(ReadLengthDelimited(payload, ref offset));
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.ValidatedExecutablePath:
                        response.ValidatedExecutablePath = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.InstallRoot:
                        response.InstallRoot = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.Message:
                        response.Message = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.UserProfile:
                        response.UserProfile = DecodeUserProfile(ReadLengthDelimited(payload, ref offset));
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.ChannelState:
                        response.ChannelState = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.AttachState:
                        response.AttachState = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.AttachSource:
                        response.AttachSource = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.AntiAddictionStatusJson:
                        response.AntiAddictionStatus = DecodeAntiAddictionStatus(ReadLengthDelimited(payload, ref offset));
                        break;
                    case DesktopProto.DesktopSessionAttachedResponse.DesktopProtocolVersion:
                        response.DesktopProtocolVersion = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return response;
        }

        private static IGPDesktopCapabilitySet DecodeCapabilitySet(byte[] payload)
        {
            var capabilities = new IGPDesktopCapabilitySet();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case DesktopProto.DesktopSessionCapabilitySet.Achievements:
                        capabilities.achievements = ReadVarint(payload, ref offset) == 1;
                        break;
                    case 2:
                        SkipField(payload, wireType, ref offset);
                        break;
                    case DesktopProto.DesktopSessionCapabilitySet.UserContext:
                        capabilities.userContext = ReadVarint(payload, ref offset) == 1;
                        break;
                    case DesktopProto.DesktopSessionCapabilitySet.HostedBootstrap:
                        capabilities.hostedBootstrap = ReadVarint(payload, ref offset) == 1;
                        break;
                    case DesktopProto.DesktopSessionCapabilitySet.GameAuthorization:
                        capabilities.gameAuthorization = ReadVarint(payload, ref offset) == 1;
                        break;
                    case DesktopProto.DesktopSessionCapabilitySet.AntiAddiction:
                        capabilities.antiAddiction = ReadVarint(payload, ref offset) == 1;
                        break;
                    case DesktopCapabilityLyingBottleField:
                        capabilities.lyingBottle = ReadVarint(payload, ref offset) == 1;
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return capabilities;
        }

        private static IGPDesktopUserProfile DecodeUserProfile(byte[] payload)
        {
            var profile = new IGPDesktopUserProfile();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case DesktopProto.DesktopSessionUserProfile.Nickname:
                        profile.nickname = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionUserProfile.DisplayTag:
                        profile.displayTag = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionUserProfile.AvatarAvailable:
                        profile.avatarAvailable = ReadVarint(payload, ref offset) == 1;
                        break;
                    case DesktopProto.DesktopSessionUserProfile.AvatarVersion:
                        profile.avatarVersion = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return profile;
        }

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

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

                switch (fieldNumber)
                {
                    case DesktopProto.DesktopSessionCommandResponse.RequestId:
                        result.RequestId = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionCommandResponse.Success:
                        result.Success = ReadVarint(payload, ref offset) == 1;
                        break;
                    case DesktopProto.DesktopSessionCommandResponse.Code:
                        result.Code = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionCommandResponse.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionCommandResponse.ContentJson:
                        result.ContentJson = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionCommandResponse.ContentBytes:
                        result.ContentBytes = ReadLengthDelimited(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static bool TryDecodeHostedBootstrapAvailable(
            IGPDesktopSessionCommandResult result,
            out IGPDesktopHostedBootstrapAvailableEvent hostedBootstrapAvailable)
        {
            hostedBootstrapAvailable = new IGPDesktopHostedBootstrapAvailableEvent();
            if (!string.Equals(result.RequestId, HostedBootstrapAvailableRequestId, StringComparison.Ordinal))
            {
                return false;
            }

            if (!result.Success ||
                !string.Equals(result.Code, HostedBootstrapAvailableCode, StringComparison.Ordinal))
            {
                return false;
            }

            if (!string.IsNullOrWhiteSpace(result.ContentJson))
            {
                try
                {
                    hostedBootstrapAvailable = JsonConvert.DeserializeObject<IGPDesktopHostedBootstrapAvailableEvent>(
                        result.ContentJson) ?? new IGPDesktopHostedBootstrapAvailableEvent();
                }
                catch
                {
                    hostedBootstrapAvailable = new IGPDesktopHostedBootstrapAvailableEvent();
                }
            }

            return true;
        }

        private static IGPAntiAddictionStatus DecodeAntiAddictionStatus(byte[] payload)
        {
            if (payload == null || payload.Length == 0 || !LooksLikeJsonObject(payload))
            {
                return CreateDefaultAntiAddictionStatus();
            }

            var json = Encoding.UTF8.GetString(payload);
            if (string.IsNullOrWhiteSpace(json))
            {
                return CreateDefaultAntiAddictionStatus();
            }

            try
            {
                return JsonConvert.DeserializeObject<IGPAntiAddictionStatus>(json)
                    ?? CreateDefaultAntiAddictionStatus();
            }
            catch (JsonException)
            {
                return CreateDefaultAntiAddictionStatus();
            }
        }

        private static IGPAntiAddictionStatus DecodeAntiAddictionChangedEvent(byte[] payload)
        {
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case DesktopProto.DesktopSessionAntiAddictionChangedEvent.AntiAddictionStatusJson:
                        return DecodeAntiAddictionStatus(ReadLengthDelimited(payload, ref offset));
                    case DesktopProto.DesktopSessionAntiAddictionChangedEvent.AntiAddictionEventJson:
                        SkipField(payload, wireType, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return CreateDefaultAntiAddictionStatus();
        }

        private static bool LooksLikeJsonObject(byte[] payload)
        {
            var offset = 0;
            if (payload.Length >= 3 && payload[0] == 0xEF && payload[1] == 0xBB && payload[2] == 0xBF)
            {
                offset = 3;
            }

            while (offset < payload.Length)
            {
                var current = payload[offset];
                if (current == (byte)' ' || current == (byte)'\t' || current == (byte)'\r' || current == (byte)'\n')
                {
                    offset++;
                    continue;
                }

                return current == (byte)'{';
            }

            return false;
        }

        private static IGPAntiAddictionStatus CreateDefaultAntiAddictionStatus()
        {
            return new IGPAntiAddictionStatus
            {
                enabled = false,
                canPlayNow = true,
                state = nameof(IGPAntiAddictionState.Disabled),
                ageBand = nameof(IGPAgeBand.Unknown),
            };
        }

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

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

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

            return result;
        }

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

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

                switch (fieldNumber)
                {
                    case DesktopProto.DesktopSessionError.Code:
                        result.Code = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionError.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionError.Category:
                        result.Category = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionError.ChannelState:
                        result.ChannelState = ReadString(payload, ref offset);
                        break;
                    case DesktopProto.DesktopSessionError.AttachState:
                        result.AttachState = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static byte[] EncodeAttachResponsePayload(IGPDesktopSessionAttachResponse response)
        {
            var bytes = new List<byte>();
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.DesktopSessionId, response.DesktopSessionId);
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.UserId, response.UserId);
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.AccountId, response.AccountId);
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.LoginState, response.LoginState);
            if (response.AppId > 0)
            {
                WriteVarintField(bytes, DesktopProto.DesktopSessionAttachedResponse.AppId, (uint)response.AppId);
            }

            if (response.Capabilities != null)
            {
                WriteMessageField(bytes, DesktopProto.DesktopSessionAttachedResponse.Capabilities, EncodeCapabilitySet(response.Capabilities));
            }

            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.ValidatedExecutablePath, response.ValidatedExecutablePath);
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.InstallRoot, response.InstallRoot);
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.Message, response.Message);
            if (response.UserProfile != null)
            {
                WriteMessageField(bytes, DesktopProto.DesktopSessionAttachedResponse.UserProfile, EncodeUserProfile(response.UserProfile));
            }

            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.ChannelState, response.ChannelState);
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.AttachState, response.AttachState);
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.AttachSource, response.AttachSource);
            if (response.AntiAddictionStatus != null)
            {
                WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.AntiAddictionStatusJson, JsonConvert.SerializeObject(response.AntiAddictionStatus));
            }
            var desktopProtocolVersion = string.IsNullOrWhiteSpace(response.DesktopProtocolVersion)
                ? SupportedProtocolVersion
                : response.DesktopProtocolVersion;
            WriteStringField(bytes, DesktopProto.DesktopSessionAttachedResponse.DesktopProtocolVersion, desktopProtocolVersion);
            return bytes.ToArray();
        }

        private static byte[] EncodeCommandResultPayload(IGPDesktopSessionCommandResult result)
        {
            var bytes = new List<byte>();
            WriteStringField(bytes, DesktopProto.DesktopSessionCommandResponse.RequestId, result.RequestId);
            WriteVarintField(bytes, DesktopProto.DesktopSessionCommandResponse.Success, result.Success ? 1u : 0u);
            WriteStringField(bytes, DesktopProto.DesktopSessionCommandResponse.Code, result.Code);
            WriteStringField(bytes, DesktopProto.DesktopSessionCommandResponse.Message, result.Message);
            WriteStringField(bytes, DesktopProto.DesktopSessionCommandResponse.ContentJson, result.ContentJson);
            if (result.ContentBytes != null && result.ContentBytes.Length > 0)
            {
                WriteMessageField(bytes, DesktopProto.DesktopSessionCommandResponse.ContentBytes, result.ContentBytes);
            }

            return bytes.ToArray();
        }

        private static byte[] EncodeCapabilitySet(IGPDesktopCapabilitySet capabilities)
        {
            var bytes = new List<byte>();
            if (capabilities.achievements)
            {
                WriteVarintField(bytes, DesktopProto.DesktopSessionCapabilitySet.Achievements, 1);
            }

            if (capabilities.userContext)
            {
                WriteVarintField(bytes, DesktopProto.DesktopSessionCapabilitySet.UserContext, 1);
            }

            if (capabilities.hostedBootstrap)
            {
                WriteVarintField(bytes, DesktopProto.DesktopSessionCapabilitySet.HostedBootstrap, 1);
            }

            if (capabilities.gameAuthorization)
            {
                WriteVarintField(bytes, DesktopProto.DesktopSessionCapabilitySet.GameAuthorization, 1);
            }

            if (capabilities.antiAddiction)
            {
                WriteVarintField(bytes, DesktopProto.DesktopSessionCapabilitySet.AntiAddiction, 1);
            }

            if (capabilities.lyingBottle)
            {
                WriteVarintField(bytes, DesktopCapabilityLyingBottleField, 1);
            }

            return bytes.ToArray();
        }

        private static byte[] EncodeUserProfile(IGPDesktopUserProfile profile)
        {
            var bytes = new List<byte>();
            WriteStringField(bytes, DesktopProto.DesktopSessionUserProfile.Nickname, profile.nickname);
            WriteStringField(bytes, DesktopProto.DesktopSessionUserProfile.DisplayTag, profile.displayTag);
            if (profile.avatarAvailable)
            {
                WriteVarintField(bytes, DesktopProto.DesktopSessionUserProfile.AvatarAvailable, 1);
            }

            WriteStringField(bytes, DesktopProto.DesktopSessionUserProfile.AvatarVersion, profile.avatarVersion);
            return bytes.ToArray();
        }

        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 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 = 0;
            var shift = 0;

            while (offset < buffer.Length)
            {
                var current = buffer[offset++];
                value |= (current & 0x7F) << shift;
                if ((current & 0x80) == 0)
                {
                    return value;
                }

                shift += 7;
            }

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

        private static byte[] ReadLengthDelimited(byte[] buffer, ref int offset)
        {
            var length = ReadVarint(buffer, ref offset);
            if (length < 0 || offset + length > buffer.Length)
            {
                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:
                    ReadVarint(buffer, ref offset);
                    return;
                case WireTypeLengthDelimited:
                    ReadLengthDelimited(buffer, ref offset);
                    return;
                default:
                    throw new IGPSDKException($"Unsupported protobuf wire type: {wireType}");
            }
        }
    }
}
