#nullable enable
using IGP.UnitySDK.Models;
using IGP.UnitySDK;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using IGP.Multiplayer.Core;
using IGP.Multiplayer.Models;
using Newtonsoft.Json.Linq;

namespace IGP.Multiplayer
{
    public partial class IGPMultiplayerRuntime
    {
        private const int SceneGateMaxUtf8Bytes = 256;

        private bool TryHandleBusinessErrorMessage(Message message)
        {
            if (message == null || !string.Equals(message.type, "error", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            var code = ExtractStringFromContent(message.content, "code");
            if (string.IsNullOrWhiteSpace(code))
            {
                return false;
            }

            if (string.Equals(code, "STATE_NOT_FOUND", StringComparison.OrdinalIgnoreCase))
            {
                ApplyDefaultForMissingState(message.content);
                LogSdkWarning(
                    "realtime",
                    "business-state",
                    $"event=missing roomId={message.roomId} playerId={message.playerId} " +
                    $"scope={ExtractStringFromContent(message.content, "scope")} " +
                    $"key={ExtractStringFromContent(message.content, "key")} source=data-plane");
                PublishMultiplayerError(
                    code,
                    ExtractStringFromContent(message.content, "message"),
                    "arena",
                    IGPRealtimeTransport.Kcp,
                    false,
                    message.content);
                return true;
            }

            if (IsRealtimeBusinessError(code))
            {
                LogSdkWarning(
                    "realtime",
                    "business-error",
                    $"event=received code={code} roomId={message.roomId} " +
                    $"playerId={message.playerId} source=data-plane");
                PublishMultiplayerError(
                    code,
                    ExtractStringFromContent(message.content, "message"),
                    "arena",
                    IGPRealtimeTransport.Kcp,
                    false,
                    message.content);
                return true;
            }

            return false;
        }

        private static bool IsRealtimeBusinessError(string? code)
        {
            return string.Equals(code, "STATE_NOT_FOUND", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(code, "INVALID_STATE_SCOPE", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(code, "INVALID_REQUEST", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(code, "FORBIDDEN", StringComparison.OrdinalIgnoreCase);
        }

        private void ApplyDefaultForMissingState(object? content)
        {
            var scope = ExtractStringFromContent(content, "scope");
            if (!string.Equals(scope, "global", StringComparison.OrdinalIgnoreCase))
            {
                return;
            }

            var key = ExtractStringFromContent(content, "key");
            if (string.IsNullOrWhiteSpace(key))
            {
                return;
            }

            var defaultValue = GetDefaultGlobalStateValue(key);
            if (defaultValue == MissingStateDefaultSentinel.Value)
            {
                return;
            }

            roomData.globalState ??= new Dictionary<string, object>();
            roomData.globalState[key] = defaultValue!;
        }

        private object? GetDefaultGlobalStateValue(string key)
        {
            switch (key)
            {
                case "LobbyName":
                case "LobbyIntroduction":
                case "OwnerID":
                case "GameVersion":
                case "ArchiveID":
                case "ArchiveName":
                case "ArchiveHash":
                    return string.Empty;
                case "MiniGameInfo":
                    return null;
                case "MaxPlayerNumber":
                    return roomData?.maxPlayers ?? 0;
                case "GameIsRunning":
                case "IsRelease":
                    return false;
                case "GameModeID":
                    return 0;
                default:
                    return MissingStateDefaultSentinel.Value;
            }
        }

        private static string ExtractStringFromContent(object? content, string name)
        {
            if (content == null)
            {
                return string.Empty;
            }

            if (content is JObject jobj)
            {
                return jobj.Value<string>(name) ?? string.Empty;
            }

            if (content is IDictionary<string, object> dictionary &&
                dictionary.TryGetValue(name, out var value))
            {
                return value?.ToString() ?? string.Empty;
            }

            var property = content.GetType().GetProperty(name);
            return property?.GetValue(content)?.ToString() ?? string.Empty;
        }

        private Task SendReliableRealtimeMessageAsync(
            Message message,
            CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (!IsRealtimeReady || !IsKcpConnected || dataPlaneTransport == null)
            {
                LogSdkError(
                    "realtime",
                    "send",
                    $"event=send-failed reason=kcp-not-ready messageType={FormatNetworkLogValue(message.type)} " +
                    $"roomId={FormatNetworkLogValue(currentRoomId)} playerId={FormatNetworkLogValue(playerId)} " +
                    $"realtimeState={realtimeConnectionState} kcpConnected={IsKcpConnected} transportAttached={dataPlaneTransport != null}");
                throw new IGPSDKException(
                    "RoomNode realtime data plane is not connected. Start the configured multiplayer transport before using realtime APIs.",
                    IGPMultiplayerErrorCodes.ERR_HOSTED_REALTIME_NOT_READY);
            }

            var outbound = new Message
            {
                type = message.type,
                roomId = currentRoomId,
                playerId = playerId,
                targetPlayerId = message.targetPlayerId,
                content = message.content,
                reliable = true,
                timestamp = message.timestamp,
            };

            var result = TrySendP2PMessages(new[] { outbound }, isControl: false);
            if (result != IGPTransportSendResult.Accepted)
            {
                LogSdkError(
                    "realtime",
                    "send",
                    $"event=send-failed reason=queue-rejected result={result} messageType={FormatNetworkLogValue(message.type)} " +
                    $"roomId={FormatNetworkLogValue(currentRoomId)} playerId={FormatNetworkLogValue(playerId)}");
                throw new IGPSDKException($"RoomNode realtime message was not queued: {result}");
            }

            return Task.CompletedTask;
        }

        /// <summary>
        /// Sends an application message through the reliable RoomNode KCP data plane.
        /// </summary>
        public async Task SendMessageAsync(
            Message message,
            CancellationToken cancellationToken = default)
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message));
            }

            if (string.IsNullOrWhiteSpace(message.type))
            {
                throw new ArgumentException("Message type is required", nameof(message));
            }

            if (IGPReservedMessageTypes.IsReserved(message.type))
            {
                throw new IGPSDKException("Message type is reserved; use the explicit realtime API instead");
            }

            await SendReliableRealtimeMessageAsync(message, cancellationToken);
        }

        public async Task SetSceneGateAsync(string value)
        {
            if (value == null)
            {
                throw new ArgumentNullException(nameof(value));
            }

            if (Encoding.UTF8.GetByteCount(value) > SceneGateMaxUtf8Bytes)
            {
                throw new ArgumentException("Scene gate must not exceed 256 UTF-8 bytes", nameof(value));
            }

            await SendReliableRealtimeMessageAsync(
                Message.CreateSceneGateSet(currentRoomId, playerId, value),
                RuntimeCancellationToken);
            LogSdkInfo(
                "realtime",
                "scene-gate",
                $"event=set-queued roomId={FormatNetworkLogValue(currentRoomId)} " +
                $"playerId={FormatNetworkLogValue(playerId)} value={FormatNetworkLogValue(value)}");
        }

        public async Task GetSceneGateAsync(CancellationToken cancellationToken = default)
        {
            await SendReliableRealtimeMessageAsync(
                Message.CreateSceneGateGet(currentRoomId, playerId),
                cancellationToken);
            LogSdkInfo(
                "realtime",
                "scene-gate",
                $"event=get-queued roomId={FormatNetworkLogValue(currentRoomId)} " +
                $"playerId={FormatNetworkLogValue(playerId)}");
        }

        private void HandleSceneGateSet(Message message)
        {
            if (!(message.content is SceneGateSetContent content))
            {
                return;
            }

            ApplySceneGateValue(message, content.value, "set");
        }

        private void HandleSceneGateGet(Message message)
        {
            if (!(message.content is SceneGateGetContent content))
            {
                return;
            }

            ApplySceneGateValue(message, content.value, "get");
        }

        private void ApplySceneGateValue(Message message, string? value, string operation)
        {
            var previousRoom = CloneRoom(roomData);
            var previousValue = roomData.sceneGate ?? string.Empty;
            var currentValue = value ?? string.Empty;
            roomData.sceneGate = currentValue;
            LogSdkInfo(
                "realtime",
                "scene-gate",
                $"event={operation}-applied roomId={FormatNetworkLogValue(roomData.id)} " +
                $"playerId={FormatNetworkLogValue(message.playerId)} value={FormatNetworkLogValue(currentValue)} source=kcp");
            if (string.Equals(previousValue, currentValue, StringComparison.Ordinal))
            {
                return;
            }
            PublishRoomChangedIfNeeded(
                previousRoom,
                IGPRoomChangeKind.Updated,
                $"scene-gate-{operation}",
                IGPRoomChangeFlags.SceneGate);
        }

        public async Task SetStateAsync(
            string scope,
            string key,
            object? value,
            string? targetPlayerId = null,
            CancellationToken cancellationToken = default)
        {
            await SendReliableRealtimeMessageAsync(
                Message.CreateStateSet(currentRoomId, playerId, scope, key, value, targetPlayerId),
                cancellationToken);
        }

        public async Task GetStateAsync(
            string scope,
            string key,
            string? targetPlayerId = null,
            CancellationToken cancellationToken = default)
        {
            await SendReliableRealtimeMessageAsync(
                Message.CreateStateGet(currentRoomId, playerId, scope, key, targetPlayerId),
                cancellationToken);
        }

        public async Task ResetStateAsync(
            string scope,
            string[]? keysToExclude = null,
            CancellationToken cancellationToken = default)
        {
            await SendReliableRealtimeMessageAsync(
                Message.CreateStateReset(currentRoomId, playerId, scope, keysToExclude),
                cancellationToken);
        }

        public async Task RegisterRPCAsync(
            string name,
            CancellationToken cancellationToken = default)
        {
            await SendReliableRealtimeMessageAsync(
                Message.CreateRPCRegister(currentRoomId, playerId, name),
                cancellationToken);
        }

        public async Task UnregisterRPCAsync(
            string name,
            CancellationToken cancellationToken = default)
        {
            await SendReliableRealtimeMessageAsync(
                Message.CreateRPCUnregister(currentRoomId, playerId, name),
                cancellationToken);
        }

        public async Task<string> CallRPCAsync(
            string name,
            object? data = null,
            string mode = "all",
            string? requestId = null,
            CancellationToken cancellationToken = default)
        {
            requestId ??= Guid.NewGuid().ToString("N");
            await SendReliableRealtimeMessageAsync(
                Message.CreateRPCCall(currentRoomId, playerId, name, data, mode, requestId),
                cancellationToken);
            return requestId;
        }

        public async Task RespondRPCAsync(
            string targetPlayerId,
            string requestId,
            string name,
            object? data = null,
            string? error = null,
            CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(targetPlayerId))
            {
                throw new ArgumentException("RPC response target player ID is required", nameof(targetPlayerId));
            }

            await SendReliableRealtimeMessageAsync(
                Message.CreateRPCResponse(
                    currentRoomId,
                    playerId,
                    targetPlayerId,
                    requestId,
                    name,
                    data,
                    error),
                cancellationToken);
        }

        public async Task SetGlobalStateAsync(
            string key,
            object value,
            CancellationToken cancellationToken = default)
        {
            await SetStateAsync("global", key, value, cancellationToken: cancellationToken);
        }

        public async Task SetPlayerStateAsync(
            string key,
            object value,
            CancellationToken cancellationToken = default)
        {
            await SetStateAsync("player", key, value, cancellationToken: cancellationToken);
        }
    }
}
